chatccc 0.2.230 → 0.2.232

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.
@@ -54,6 +54,11 @@ export function summarizeToolResult(content: unknown, maxChars = 120): string {
54
54
  */
55
55
  export function reduceProgress(prev: ProgressView, event: ChatEvent): ProgressView {
56
56
  switch (event.type) {
57
+ case "status":
58
+ return withProgressView(prev, {
59
+ headerTitle: event.phase === "compacting" ? "压缩上下文中..." : "生成回复中...",
60
+ });
61
+
57
62
  case "text":
58
63
  // accumulated 是全文累积,直接全量替换,天然幂等
59
64
  return withProgressView(prev, { text: event.accumulated });
package/src/session.ts CHANGED
@@ -41,14 +41,14 @@ import { createCccAdapter } from "./adapters/ccc-adapter.ts";
41
41
  import { killProcessTree } from "./adapters/proc-tree-kill.ts";
42
42
  import { resourceMonitor, registerProcess, unregisterProcess } from "./adapters/resource-monitor.ts";
43
43
  import { buildImSkillsPromptCached, exportSkillSubDocs, clearImSkillsPromptCache } from "./im-skills.ts";
44
- import type { PlatformAdapter } from "./platform-adapter.ts";
45
- import { hasResponseStalled, observeResponseProgress } from "./response-stall.ts";
46
- import {
47
- classifyTerminalError,
48
- formatTerminalErrorNotice,
49
- formatTerminalErrorReason,
50
- type TerminalErrorInfo,
51
- } from "./terminal-error.ts";
44
+ import type { PlatformAdapter } from "./platform-adapter.ts";
45
+ import { hasResponseStalled, observeResponseProgress } from "./response-stall.ts";
46
+ import {
47
+ classifyTerminalError,
48
+ formatTerminalErrorNotice,
49
+ formatTerminalErrorReason,
50
+ type TerminalErrorInfo,
51
+ } from "./terminal-error.ts";
52
52
  import {
53
53
  MAX_PROCESSED,
54
54
  clearFeishuMessageLedgerMemory,
@@ -181,8 +181,8 @@ const DEFAULT_FINAL_RESPONSE_CLOSE_TIMEOUT_MS = 10_000;
181
181
  export const RESPONSE_STALL_RECOVERY_PROMPT = "完成了吗?如果没完成继续";
182
182
  export const RESPONSE_STALL_RECOVERY_NOTICE =
183
183
  `检测到会话停滞,正在自动确认并继续。\n\n${RESPONSE_STALL_RECOVERY_PROMPT}`;
184
- export const RESPONSE_STALL_RECOVERY_EXHAUSTED_NOTICE =
185
- "⚠️ 自动续跑仍连续 3 分钟没有启动进展或新回复,本次不再自动继续。";
184
+ export const RESPONSE_STALL_RECOVERY_EXHAUSTED_NOTICE =
185
+ "⚠️ 自动续跑仍连续 3 分钟没有生成新回复,本次不再自动继续。";
186
186
  const RESPONSE_STALL_RECOVERY_DELAY_MS = 200;
187
187
  let processMonitorIntervalMs = DEFAULT_PROCESS_MONITOR_INTERVAL_MS;
188
188
  let responseStallTimeoutMs = DEFAULT_RESPONSE_STALL_TIMEOUT_MS;
@@ -311,18 +311,18 @@ function scheduleFinalResponseCloseGuard(
311
311
  runningPrompt.finalResponseCloseTimer = handle;
312
312
  }
313
313
 
314
- function formatTerminalHeader(
315
- status: "running" | "done" | "stopped" | "error" | "auto_ended",
316
- terminalError?: TerminalErrorInfo,
317
- ): {
314
+ function formatTerminalHeader(
315
+ status: "running" | "done" | "stopped" | "error" | "auto_ended",
316
+ terminalError?: TerminalErrorInfo,
317
+ ): {
318
318
  title: string;
319
319
  template?: string;
320
320
  } {
321
321
  if (status === "auto_ended") return { title: "已自动结束 · 3分钟无新内容", template: "orange" };
322
322
  if (status === "stopped") return { title: "已停止", template: "red" };
323
- if (status === "error") {
324
- return { title: terminalError ? `异常结束 · ${terminalError.title}` : "异常结束", template: "red" };
325
- }
323
+ if (status === "error") {
324
+ return { title: terminalError ? `异常结束 · ${terminalError.title}` : "异常结束", template: "red" };
325
+ }
326
326
  return { title: "完成" };
327
327
  }
328
328
 
@@ -330,59 +330,59 @@ function turnFinalStatus(status: "running" | "done" | "stopped" | "error" | "aut
330
330
  return status === "stopped" || status === "error" || status === "auto_ended" ? "stopped" : "done";
331
331
  }
332
332
 
333
- function formatAutoEndedReply(finalReply: string): string {
334
- const reason = "⚠️ 已自动结束:连续 3 分钟没有启动进展或回复字符变化。";
333
+ function formatAutoEndedReply(finalReply: string): string {
334
+ const reason = "⚠️ 已自动结束:生成回复阶段连续 3 分钟没有字符变化。";
335
335
  return finalReply
336
336
  ? `${reason}以下回复可能不完整。\n\n${finalReply}`
337
337
  : `${reason}本轮没有可发送的回复内容。`;
338
338
  }
339
339
 
340
340
  /**
341
- * 只监控用户无法判断是否仍有进展的两个阶段。思考、工具调用和搜索可能合法地
342
- * 长时间不产生回复字符,由资源监控负责识别真正僵死,不能在这里误杀。
341
+ * 只监控明确的回复生成阶段。启动、压缩、思考、工具调用和搜索都有各自的
342
+ * 状态或资源保护,不能把它们消耗的时间算入回复停滞窗口。
343
343
  */
344
- function monitorsOutputProgress(kind: AgentActivityKind): boolean {
345
- return kind === "starting" || kind === "responding";
346
- }
347
-
348
- function formatTerminalReply(
349
- status: "running" | "done" | "stopped" | "error" | "auto_ended",
350
- finalReply: string,
351
- terminalError?: TerminalErrorInfo,
352
- ): string | null {
353
- if (status === "auto_ended") return formatAutoEndedReply(finalReply);
354
- if (status === "error") {
355
- const error = terminalError ?? {
356
- kind: "unknown" as const,
357
- title: "原因未记录",
358
- message: "当前状态中没有可用的错误详情,请查看运行日志。",
359
- occurredAt: Date.now(),
360
- };
361
- return formatTerminalErrorNotice(error, finalReply);
362
- }
363
- return finalReply || null;
364
- }
365
-
366
- function formatTerminalCardContent(state: {
367
- status: "running" | "done" | "stopped" | "error" | "auto_ended";
368
- accumulatedContent: string;
369
- finalReply: string;
370
- terminalError?: TerminalErrorInfo;
371
- }): string {
372
- const content = state.accumulatedContent + state.finalReply;
373
- if (state.status !== "error") return content;
374
-
375
- const error = state.terminalError ?? {
376
- kind: "unknown" as const,
377
- title: "原因未记录",
378
- message: "当前状态中没有可用的错误详情,请查看运行日志。",
379
- occurredAt: Date.now(),
380
- };
381
- const reason = formatTerminalErrorReason(error);
382
- return content.trim()
383
- ? `${content}\n\n${reason}\n以上内容可能不完整。`
384
- : reason;
385
- }
344
+ function monitorsOutputProgress(kind: AgentActivityKind): boolean {
345
+ return kind === "responding";
346
+ }
347
+
348
+ function formatTerminalReply(
349
+ status: "running" | "done" | "stopped" | "error" | "auto_ended",
350
+ finalReply: string,
351
+ terminalError?: TerminalErrorInfo,
352
+ ): string | null {
353
+ if (status === "auto_ended") return formatAutoEndedReply(finalReply);
354
+ if (status === "error") {
355
+ const error = terminalError ?? {
356
+ kind: "unknown" as const,
357
+ title: "原因未记录",
358
+ message: "当前状态中没有可用的错误详情,请查看运行日志。",
359
+ occurredAt: Date.now(),
360
+ };
361
+ return formatTerminalErrorNotice(error, finalReply);
362
+ }
363
+ return finalReply || null;
364
+ }
365
+
366
+ function formatTerminalCardContent(state: {
367
+ status: "running" | "done" | "stopped" | "error" | "auto_ended";
368
+ accumulatedContent: string;
369
+ finalReply: string;
370
+ terminalError?: TerminalErrorInfo;
371
+ }): string {
372
+ const content = state.accumulatedContent + state.finalReply;
373
+ if (state.status !== "error") return content;
374
+
375
+ const error = state.terminalError ?? {
376
+ kind: "unknown" as const,
377
+ title: "原因未记录",
378
+ message: "当前状态中没有可用的错误详情,请查看运行日志。",
379
+ occurredAt: Date.now(),
380
+ };
381
+ const reason = formatTerminalErrorReason(error);
382
+ return content.trim()
383
+ ? `${content}\n\n${reason}\n以上内容可能不完整。`
384
+ : reason;
385
+ }
386
386
 
387
387
  function isCardKitSequenceConflict(err: unknown): boolean {
388
388
  return err instanceof Error && err.message.includes("300317");
@@ -948,14 +948,16 @@ export function accumulateBlockContent(
948
948
  // 覆盖而非追加:适配器已保证这是一段完整最终文本(如 Cursor 流末快照)
949
949
  state.finalCompleteText = block.text;
950
950
  break;
951
- case "compact_boundary": {
951
+ case "compact_boundary": {
952
952
  const triggerLabel = block.trigger === "manual" ? "手动" : "自动"; // 手动 / 自动
953
953
  state.accumulatedContent +=
954
954
  `\n\n🔄 上下文压缩(${triggerLabel}): **${block.pre_tokens}** → **${block.post_tokens}** tokens\n`; // 🔄 / →
955
- break;
956
- }
957
- }
958
- }
955
+ break;
956
+ }
957
+ case "agent_status":
958
+ break;
959
+ }
960
+ }
959
961
 
960
962
  // ---------------------------------------------------------------------------
961
963
  // switchChatBinding — /newh、/session N 共用的事务式"切换 chat 绑定"
@@ -1144,7 +1146,7 @@ export async function initClaudeSession(tool: string, overrideCwd?: string, chat
1144
1146
  return { sessionId, cwd };
1145
1147
  }
1146
1148
 
1147
- export async function resumeAndPrompt(
1149
+ export async function resumeAndPrompt(
1148
1150
  sessionId: string,
1149
1151
  userText: string,
1150
1152
  platform: PlatformAdapter,
@@ -1152,25 +1154,25 @@ export async function resumeAndPrompt(
1152
1154
  msgTimestamp: number,
1153
1155
  tool: string,
1154
1156
  traceId?: string,
1155
- ): Promise<SessionRunOutcome> {
1156
- return runAgentSession(sessionId, userText, platform, chatId, msgTimestamp, tool, traceId);
1157
- }
1157
+ ): Promise<SessionRunOutcome> {
1158
+ return runAgentSession(sessionId, userText, platform, chatId, msgTimestamp, tool, traceId);
1159
+ }
1158
1160
 
1159
1161
  // ---------------------------------------------------------------------------
1160
1162
  // runAgentSession — session 中心的 agent prompt(文件持久化 + display 解耦)
1161
1163
  // ---------------------------------------------------------------------------
1162
1164
 
1163
- interface RunAgentSessionOptions {
1165
+ interface RunAgentSessionOptions {
1164
1166
  /**
1165
1167
  * 标记本轮是 response-stall 后的唯一一次内部续跑。若本轮再次因相同原因
1166
1168
  * 停滞,不再递归创建第三轮,避免服务异常期间无限消耗 token。
1167
1169
  */
1168
1170
  autoRecovery?: boolean;
1169
- }
1170
-
1171
- export type SessionRunOutcome = "busy" | "done" | "stopped" | "error" | "auto_ended";
1171
+ }
1172
+
1173
+ export type SessionRunOutcome = "busy" | "done" | "stopped" | "error" | "auto_ended";
1172
1174
 
1173
- export async function runAgentSession(
1175
+ export async function runAgentSession(
1174
1176
  sessionId: string,
1175
1177
  userText: string,
1176
1178
  platform: PlatformAdapter,
@@ -1179,7 +1181,7 @@ export async function runAgentSession(
1179
1181
  tool: string,
1180
1182
  traceId?: string,
1181
1183
  options: RunAgentSessionOptions = {},
1182
- ): Promise<SessionRunOutcome> {
1184
+ ): Promise<SessionRunOutcome> {
1183
1185
  const tid = traceId ?? "";
1184
1186
 
1185
1187
  // runAgentSession 是飞书用户消息、队列消息与内部自动恢复共同使用的唯一
@@ -1206,7 +1208,7 @@ export async function runAgentSession(
1206
1208
  ? "当前正在生成回复中,请等待完成后再发送消息。也可以发送 /stop 结束,已完成的步骤不会丢失。"
1207
1209
  : "该会话正在生成回复中,请等待完成后再发送消息。也可以发送 /stop 结束,已完成的步骤不会丢失。";
1208
1210
  await platform.sendText(_chatId, busyMsg).catch(() => {});
1209
- return "busy";
1211
+ return "busy";
1210
1212
  }
1211
1213
 
1212
1214
  // 立即标记活跃,确保 /sessions、isSessionRunning 等查询在异步准备阶段就能看到运行状态。
@@ -1334,7 +1336,7 @@ export async function runAgentSession(
1334
1336
  // 再开始缓存问题对应的任务"。
1335
1337
  const prevState = await readStreamState(sessionId);
1336
1338
  if (prevState && prevState.status !== "running") {
1337
- const prevTerminalReply = formatTerminalReply(prevState.status, prevState.finalReply, prevState.terminalError);
1339
+ const prevTerminalReply = formatTerminalReply(prevState.status, prevState.finalReply, prevState.terminalError);
1338
1340
  const displayChatId = pickDisplayChat(sessionId);
1339
1341
  if (displayChatId) {
1340
1342
  const pp = platformForChat(displayChatId);
@@ -1353,8 +1355,8 @@ export async function runAgentSession(
1353
1355
  setSessionChatAvatar(pp, displayChatId, prevState.tool, "idle", sessionId).catch(() => {});
1354
1356
  } else {
1355
1357
  const nextSeq = display.sequence + 1;
1356
- const { title: headerTitle, template: headerTemplate } = formatTerminalHeader(prevState.status, prevState.terminalError);
1357
- const cardContent = truncateContent(formatTerminalCardContent(prevState)) || " ";
1358
+ const { title: headerTitle, template: headerTemplate } = formatTerminalHeader(prevState.status, prevState.terminalError);
1359
+ const cardContent = truncateContent(formatTerminalCardContent(prevState)) || " ";
1358
1360
  const doneCard = buildProgressCard(progressView({ text: cardContent, status: "done", showStop: false, headerTitle, headerTemplate }));
1359
1361
  await pp.cardUpdate(display.cardId, doneCard, nextSeq).catch(err => {
1360
1362
  console.error(`[${ts()}] [DISPLAY] prevState final cardUpdate failed: ${(err as Error).message}`);
@@ -1447,22 +1449,21 @@ export async function runAgentSession(
1447
1449
 
1448
1450
  let lastFileWrite = Date.now();
1449
1451
  const FILE_WRITE_INTERVAL_MS = 2000;
1450
- const toolCallMap = new Map<string, { name: string; input: unknown }>();
1451
- let streamErrored = false;
1452
- let streamTerminalError: TerminalErrorInfo | undefined;
1453
- let runOutcome: SessionRunOutcome = "error";
1454
-
1455
- const runningPrompt = activePrompts.get(sessionId);
1456
- if (runningPrompt) {
1457
- // 必须在消费第一个事件前建立零字符基线。部分 CLI 卡死时只启动了进程,
1458
- // 甚至一个事件都不会 yield;若等循环体更新进度,这种会话会永久停在
1459
- // “正在启动 Agent”,也永远触发不了三分钟保护。
1460
- runningPrompt.responseProgress = observeResponseProgress(
1461
- undefined,
1462
- true,
1463
- 0,
1464
- activityTracker.activity.startedAt,
1465
- );
1452
+ const toolCallMap = new Map<string, { name: string; input: unknown }>();
1453
+ let streamErrored = false;
1454
+ let streamTerminalError: TerminalErrorInfo | undefined;
1455
+ let runOutcome: SessionRunOutcome = "error";
1456
+
1457
+ const runningPrompt = activePrompts.get(sessionId);
1458
+ if (runningPrompt) {
1459
+ // 在消费第一个事件前建立阶段感知的零字符基线;启动阶段不计时,只有后续
1460
+ // 收到明确的 responding 状态后才会启动三分钟回复停滞保护。
1461
+ runningPrompt.responseProgress = observeResponseProgress(
1462
+ undefined,
1463
+ monitorsOutputProgress(activityTracker.activity.kind),
1464
+ 0,
1465
+ activityTracker.activity.startedAt,
1466
+ );
1466
1467
 
1467
1468
  const checkResponseStall = async () => {
1468
1469
  const current = activePrompts.get(sessionId);
@@ -1532,7 +1533,7 @@ export async function runAgentSession(
1532
1533
  current.controller.abort();
1533
1534
  await killProcessTree(current.processPid);
1534
1535
  console.warn(
1535
- `[${ts()}] [RESPONSE-STALL] Session ${sessionId} auto-ended after 3 minutes without startup or reply progress`,
1536
+ `[${ts()}] [RESPONSE-STALL] Session ${sessionId} auto-ended after 3 minutes without reply progress`,
1536
1537
  );
1537
1538
  };
1538
1539
 
@@ -1624,10 +1625,10 @@ export async function runAgentSession(
1624
1625
  });
1625
1626
  }
1626
1627
  }
1627
- } catch (streamErr) {
1628
- streamErrored = true;
1629
- streamTerminalError = classifyTerminalError(streamErr);
1630
- console.error(`[${ts()}] [STREAM] Error in stream loop for ${sessionId}: ${(streamErr as Error).message}`);
1628
+ } catch (streamErr) {
1629
+ streamErrored = true;
1630
+ streamTerminalError = classifyTerminalError(streamErr);
1631
+ console.error(`[${ts()}] [STREAM] Error in stream loop for ${sessionId}: ${(streamErr as Error).message}`);
1631
1632
  } finally {
1632
1633
  // 标记 prompt 结束
1633
1634
  resourceMonitor.off("stuck", onResourceStuck);
@@ -1675,24 +1676,24 @@ export async function runAgentSession(
1675
1676
  : wasStopped
1676
1677
  ? "stopped"
1677
1678
  : "done";
1678
- const finalReply = pickFinalReply(state).trim();
1679
- const terminalError = streamTerminalError
1680
- ?? (wasAbnormalExit
1681
- ? {
1682
- kind: "process" as const,
1683
- title: "Agent 进程意外退出",
1684
- message: "Agent CLI 进程已退出。若回复不完整,请重新发送上一条指令。",
1685
- occurredAt: Date.now(),
1686
- }
1687
- : wasResourceStuck
1688
- ? {
1689
- kind: "resource" as const,
1690
- title: "Agent 进程失去响应",
1691
- message: "Agent 进程长时间没有 CPU 或内存变化,已被强制停止。若回复不完整,请重新发送上一条指令。",
1692
- occurredAt: Date.now(),
1693
- }
1694
- : undefined);
1695
- runOutcome = finalStatus;
1679
+ const finalReply = pickFinalReply(state).trim();
1680
+ const terminalError = streamTerminalError
1681
+ ?? (wasAbnormalExit
1682
+ ? {
1683
+ kind: "process" as const,
1684
+ title: "Agent 进程意外退出",
1685
+ message: "Agent CLI 进程已退出。若回复不完整,请重新发送上一条指令。",
1686
+ occurredAt: Date.now(),
1687
+ }
1688
+ : wasResourceStuck
1689
+ ? {
1690
+ kind: "resource" as const,
1691
+ title: "Agent 进程失去响应",
1692
+ message: "Agent 进程长时间没有 CPU 或内存变化,已被强制停止。若回复不完整,请重新发送上一条指令。",
1693
+ occurredAt: Date.now(),
1694
+ }
1695
+ : undefined);
1696
+ runOutcome = finalStatus;
1696
1697
 
1697
1698
  // stop-stuck-loop 接口可能在 fire-and-forget 中已写入带 final_reply 的
1698
1699
  // stream state,finally 不应覆盖它。同时保留 stuckAt 标记,防止
@@ -1722,9 +1723,9 @@ export async function runAgentSession(
1722
1723
  cwd,
1723
1724
  tool,
1724
1725
  ...(preserveStuckAt ? { stuckAt: preserveStuckAt } : {}),
1725
- ...(autoEndedAt !== undefined ? { autoEndedAt } : {}),
1726
- ...(terminalError ? { terminalError } : {}),
1727
- });
1726
+ ...(autoEndedAt !== undefined ? { autoEndedAt } : {}),
1727
+ ...(terminalError ? { terminalError } : {}),
1728
+ });
1728
1729
 
1729
1730
  // display loop 下一轮会读到最终状态并发送消息
1730
1731
 
@@ -1795,9 +1796,9 @@ export async function runAgentSession(
1795
1796
  autoRecoveryTarget = { chatId: activeAutoEnded, platform: pp };
1796
1797
  }
1797
1798
  }
1798
- console.warn(`[${ts()}] Session ${sessionId} auto-ended after stalled startup or response output (content chunks: ${state.chunkCount})`);
1799
+ console.warn(`[${ts()}] Session ${sessionId} auto-ended after stalled response output (content chunks: ${state.chunkCount})`);
1799
1800
  if (tid) logTrace(tid, "SESSION_END", { sessionId, outcome: "response_stall", chunks: state.chunkCount });
1800
- } else if (wasAbnormalExit) {
1801
+ } else if (wasAbnormalExit) {
1801
1802
  for (const cid of finalizationChatIds) {
1802
1803
  const finfo = sessionInfoMap.get(cid);
1803
1804
  await recordSessionRegistry({
@@ -1812,55 +1813,55 @@ export async function runAgentSession(
1812
1813
  }
1813
1814
  const activeErr = getLastActiveChat(sessionId) ?? finalizationChatIds[0];
1814
1815
  if (activeErr) setSessionChatAvatar(platform, activeErr, tool, "idle", sessionId).catch(() => {});
1815
- console.log(`[${ts()}] Session ${sessionId} process exited unexpectedly (content chunks: ${state.chunkCount})`);
1816
- if (tid) logTrace(tid, "SESSION_END", { sessionId, outcome: "process_missing", chunks: state.chunkCount });
1817
- } else if (streamErrored || wasResourceStuck) {
1818
- for (const cid of finalizationChatIds) {
1819
- const finfo = sessionInfoMap.get(cid);
1820
- await recordSessionRegistry({
1821
- chatId: cid,
1822
- sessionId,
1823
- tool,
1824
- turnCount: finfo?.turnCount ?? nextTurnCount,
1825
- lastContextTokens: finfo?.lastContextTokens ?? nextContextTokens,
1826
- startTime: finfo?.startTime ?? now,
1827
- running: false,
1828
- });
1829
- }
1830
- const activeErr = getLastActiveChat(sessionId) ?? finalizationChatIds[0];
1831
- if (activeErr) {
1832
- const pp = platformForChat(activeErr) ?? platform;
1833
- const terminalState = await readStreamState(sessionId);
1834
- if (
1835
- terminalError
1836
- && !displayCards.has(activeErr)
1837
- && (!terminalState || !isFinalReplySentForTurn(terminalState))
1838
- ) {
1839
- await sendFinalReplyTextOnce(
1840
- pp,
1841
- activeErr,
1842
- sessionId,
1843
- nextTurnCount,
1844
- formatTerminalErrorNotice(terminalError, finalReplyToWrite),
1845
- );
1846
- }
1847
- setSessionChatAvatar(pp, activeErr, tool, "idle", sessionId).catch(() => {});
1848
- }
1849
- const errorOutcome = wasResourceStuck ? "resource_stuck" : "stream_error";
1850
- console.error(
1851
- `[${ts()}] Session ${sessionId} ended with ${errorOutcome}` +
1852
- `${terminalError ? ` (${terminalError.kind}: ${terminalError.title})` : ""} (content chunks: ${state.chunkCount})`,
1853
- );
1854
- if (tid) {
1855
- logTrace(tid, "SESSION_END", {
1856
- sessionId,
1857
- outcome: errorOutcome,
1858
- errorKind: terminalError?.kind,
1859
- errorTitle: terminalError?.title,
1860
- chunks: state.chunkCount,
1861
- });
1862
- }
1863
- } else {
1816
+ console.log(`[${ts()}] Session ${sessionId} process exited unexpectedly (content chunks: ${state.chunkCount})`);
1817
+ if (tid) logTrace(tid, "SESSION_END", { sessionId, outcome: "process_missing", chunks: state.chunkCount });
1818
+ } else if (streamErrored || wasResourceStuck) {
1819
+ for (const cid of finalizationChatIds) {
1820
+ const finfo = sessionInfoMap.get(cid);
1821
+ await recordSessionRegistry({
1822
+ chatId: cid,
1823
+ sessionId,
1824
+ tool,
1825
+ turnCount: finfo?.turnCount ?? nextTurnCount,
1826
+ lastContextTokens: finfo?.lastContextTokens ?? nextContextTokens,
1827
+ startTime: finfo?.startTime ?? now,
1828
+ running: false,
1829
+ });
1830
+ }
1831
+ const activeErr = getLastActiveChat(sessionId) ?? finalizationChatIds[0];
1832
+ if (activeErr) {
1833
+ const pp = platformForChat(activeErr) ?? platform;
1834
+ const terminalState = await readStreamState(sessionId);
1835
+ if (
1836
+ terminalError
1837
+ && !displayCards.has(activeErr)
1838
+ && (!terminalState || !isFinalReplySentForTurn(terminalState))
1839
+ ) {
1840
+ await sendFinalReplyTextOnce(
1841
+ pp,
1842
+ activeErr,
1843
+ sessionId,
1844
+ nextTurnCount,
1845
+ formatTerminalErrorNotice(terminalError, finalReplyToWrite),
1846
+ );
1847
+ }
1848
+ setSessionChatAvatar(pp, activeErr, tool, "idle", sessionId).catch(() => {});
1849
+ }
1850
+ const errorOutcome = wasResourceStuck ? "resource_stuck" : "stream_error";
1851
+ console.error(
1852
+ `[${ts()}] Session ${sessionId} ended with ${errorOutcome}` +
1853
+ `${terminalError ? ` (${terminalError.kind}: ${terminalError.title})` : ""} (content chunks: ${state.chunkCount})`,
1854
+ );
1855
+ if (tid) {
1856
+ logTrace(tid, "SESSION_END", {
1857
+ sessionId,
1858
+ outcome: errorOutcome,
1859
+ errorKind: terminalError?.kind,
1860
+ errorTitle: terminalError?.title,
1861
+ chunks: state.chunkCount,
1862
+ });
1863
+ }
1864
+ } else {
1864
1865
  for (const cid of finalizationChatIds) {
1865
1866
  const finfo = sessionInfoMap.get(cid);
1866
1867
  await recordSessionRegistry({
@@ -1972,9 +1973,9 @@ export async function runAgentSession(
1972
1973
  } finally {
1973
1974
  clearSessionFinalizing(sessionId);
1974
1975
  }
1975
- }
1976
- return runOutcome;
1977
- }
1976
+ }
1977
+ return runOutcome;
1978
+ }
1978
1979
 
1979
1980
  // ---------------------------------------------------------------------------
1980
1981
  // startUnifiedDisplayLoop — 全局统一 display 循环,遍历 displayCards 更新卡片
@@ -2051,13 +2052,13 @@ export function startUnifiedDisplayLoop(): void {
2051
2052
  if (activePrompts.has(sessionId)) continue;
2052
2053
 
2053
2054
  const tail = "━━━ 回答结束 ━━━";
2054
- const finalMsg = state.status === "auto_ended"
2055
- ? formatAutoEndedReply(remaining)
2056
- : state.status === "error"
2057
- ? formatTerminalReply(state.status, remaining, state.terminalError) ?? tail
2058
- : remaining
2059
- ? remaining + "\n" + tail
2060
- : tail;
2055
+ const finalMsg = state.status === "auto_ended"
2056
+ ? formatAutoEndedReply(remaining)
2057
+ : state.status === "error"
2058
+ ? formatTerminalReply(state.status, remaining, state.terminalError) ?? tail
2059
+ : remaining
2060
+ ? remaining + "\n" + tail
2061
+ : tail;
2061
2062
  if (!isFinalReplySentForTurn(state)) {
2062
2063
  await sendFinalReplyTextOnce(p, chatId, sessionId, state.turnCount, finalMsg);
2063
2064
  }
@@ -2079,8 +2080,8 @@ export function startUnifiedDisplayLoop(): void {
2079
2080
  let terminalCardUpdateAccepted = terminalCardAlreadyUpdated;
2080
2081
  if (!terminalCardAlreadyUpdated) {
2081
2082
  const nextSeq = display.sequence + 1;
2082
- const { title: headerTitle, template: headerTemplate } = formatTerminalHeader(state.status, state.terminalError);
2083
- const cardContent = truncateContent(formatTerminalCardContent(state)) || " ";
2083
+ const { title: headerTitle, template: headerTemplate } = formatTerminalHeader(state.status, state.terminalError);
2084
+ const cardContent = truncateContent(formatTerminalCardContent(state)) || " ";
2084
2085
  const doneCard = buildProgressCard(progressView({ text: cardContent, status: "done", showStop: false, headerTitle, headerTemplate }));
2085
2086
  await p.cardUpdate(display.cardId, doneCard, nextSeq).then(() => {
2086
2087
  display.sequence = nextSeq;
@@ -2107,19 +2108,19 @@ export function startUnifiedDisplayLoop(): void {
2107
2108
  }
2108
2109
 
2109
2110
  let terminalTextDelivered = true;
2110
- const terminalReply = formatTerminalReply(state.status, state.finalReply, state.terminalError);
2111
- const errorWasDeliveredByCard =
2112
- state.status === "error"
2113
- && !state.finalReply.trim()
2114
- && terminalCardUpdateAccepted;
2115
- if (errorWasDeliveredByCard) {
2116
- if (!isFinalReplySentForTurn(state)) {
2117
- await markFinalReplySent(sessionId, state.turnCount);
2118
- }
2119
- } else if (terminalReply) {
2120
- if (!isFinalReplySentForTurn(state)) {
2121
- terminalTextDelivered = await sendFinalReplyTextOnce(p, chatId, sessionId, state.turnCount, terminalReply);
2122
- }
2111
+ const terminalReply = formatTerminalReply(state.status, state.finalReply, state.terminalError);
2112
+ const errorWasDeliveredByCard =
2113
+ state.status === "error"
2114
+ && !state.finalReply.trim()
2115
+ && terminalCardUpdateAccepted;
2116
+ if (errorWasDeliveredByCard) {
2117
+ if (!isFinalReplySentForTurn(state)) {
2118
+ await markFinalReplySent(sessionId, state.turnCount);
2119
+ }
2120
+ } else if (terminalReply) {
2121
+ if (!isFinalReplySentForTurn(state)) {
2122
+ terminalTextDelivered = await sendFinalReplyTextOnce(p, chatId, sessionId, state.turnCount, terminalReply);
2123
+ }
2123
2124
  } else if (state.accumulatedContent.trim()) {
2124
2125
  const short = truncateContent(state.accumulatedContent, 30, 4000);
2125
2126
  terminalTextDelivered = await p.sendText(chatId, `[生成过程]\n${short}`).then((ok) => ok !== false).catch(() => false);