chatccc 0.2.243 → 0.2.245

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.
Files changed (65) hide show
  1. package/README.md +6 -6
  2. package/bin/cccagent.mjs +17 -17
  3. package/deepccc-agent/README.md +14 -8
  4. package/deepccc-agent/bin/deepccc.mjs +26 -26
  5. package/deepccc-agent/os-prompts/darwin.md +8 -0
  6. package/deepccc-agent/os-prompts/linux.md +8 -0
  7. package/deepccc-agent/os-prompts/win32.md +9 -0
  8. package/deepccc-agent/package-lock.json +2 -2
  9. package/deepccc-agent/package.json +63 -62
  10. package/deepccc-agent/src/__tests__/chat-session.test.ts +682 -578
  11. package/deepccc-agent/src/__tests__/cli-json.test.ts +49 -49
  12. package/deepccc-agent/src/__tests__/config.test.ts +26 -26
  13. package/deepccc-agent/src/__tests__/context.test.ts +319 -319
  14. package/deepccc-agent/src/__tests__/file-tools.test.ts +240 -240
  15. package/deepccc-agent/src/__tests__/permissions.test.ts +195 -195
  16. package/deepccc-agent/src/__tests__/privacy.test.ts +8 -5
  17. package/deepccc-agent/src/__tests__/progress-reducer.test.ts +121 -121
  18. package/deepccc-agent/src/__tests__/session-search.test.ts +262 -262
  19. package/deepccc-agent/src/__tests__/session-select.test.ts +116 -116
  20. package/deepccc-agent/src/__tests__/sigint.test.ts +56 -56
  21. package/deepccc-agent/src/__tests__/skills.test.ts +284 -284
  22. package/deepccc-agent/src/__tests__/terminal-renderer.test.ts +247 -247
  23. package/deepccc-agent/src/__tests__/web-tools.test.ts +220 -220
  24. package/deepccc-agent/src/cli.ts +7 -6
  25. package/deepccc-agent/src/config.ts +88 -84
  26. package/deepccc-agent/src/context.ts +465 -465
  27. package/deepccc-agent/src/file-log.ts +38 -38
  28. package/deepccc-agent/src/index.ts +103 -36
  29. package/deepccc-agent/src/proc-tree-kill.ts +61 -61
  30. package/deepccc-agent/src/progress/cards-helpers.ts +76 -76
  31. package/deepccc-agent/src/progress/reducer.ts +113 -113
  32. package/deepccc-agent/src/progress/terminal-renderer.ts +294 -294
  33. package/deepccc-agent/src/progress/view.ts +77 -77
  34. package/deepccc-agent/src/raw-stream-log.ts +124 -124
  35. package/deepccc-agent/src/session-search.ts +370 -370
  36. package/deepccc-agent/src/session-select.ts +48 -48
  37. package/deepccc-agent/src/sigint.ts +50 -50
  38. package/deepccc-agent/src/skills.ts +205 -205
  39. package/deepccc-agent/src/web-tools.ts +313 -313
  40. package/deepccc-agent/tsconfig.build.json +13 -13
  41. package/deepccc-agent/tsconfig.json +13 -13
  42. package/deepccc-agent/vitest.config.ts +7 -7
  43. package/package.json +1 -1
  44. package/src/__tests__/builtin-chat-session.test.ts +522 -522
  45. package/src/__tests__/builtin-config.test.ts +26 -26
  46. package/src/__tests__/builtin-context.test.ts +319 -319
  47. package/src/__tests__/builtin-file-tools.test.ts +240 -240
  48. package/src/__tests__/builtin-permissions.test.ts +211 -211
  49. package/src/__tests__/builtin-session-search.test.ts +262 -262
  50. package/src/__tests__/builtin-session-select.test.ts +116 -116
  51. package/src/__tests__/builtin-sigint.test.ts +56 -56
  52. package/src/__tests__/builtin-skills.test.ts +284 -284
  53. package/src/__tests__/builtin-web-tools.test.ts +220 -220
  54. package/src/__tests__/ccc-adapter.test.ts +15 -0
  55. package/src/__tests__/config.test.ts +17 -17
  56. package/src/__tests__/progress-reducer.test.ts +121 -121
  57. package/src/__tests__/session-ccc-config.test.ts +45 -45
  58. package/src/__tests__/session.test.ts +369 -306
  59. package/src/adapters/adapter-interface.ts +8 -2
  60. package/src/adapters/ccc-adapter.ts +149 -145
  61. package/src/config-utils.ts +13 -13
  62. package/src/config.ts +13 -13
  63. package/src/progress/reducer.ts +113 -113
  64. package/src/session-chat-binding.ts +83 -83
  65. package/src/session.ts +323 -320
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,
@@ -174,20 +174,20 @@ function platformForChat(chatId: string): PlatformAdapter | null {
174
174
  return chatPlatformMap.get(chatId) ?? platformRef;
175
175
  }
176
176
 
177
- const DEFAULT_PROCESS_MONITOR_INTERVAL_MS = 5000;
178
- const DEFAULT_AVATAR_REFRESH_INTERVAL_MS = 5 * 60 * 1000;
179
- const DEFAULT_RESPONSE_STALL_TIMEOUT_MS = 3 * 60 * 1000;
177
+ const DEFAULT_PROCESS_MONITOR_INTERVAL_MS = 5000;
178
+ const DEFAULT_AVATAR_REFRESH_INTERVAL_MS = 5 * 60 * 1000;
179
+ const DEFAULT_RESPONSE_STALL_TIMEOUT_MS = 3 * 60 * 1000;
180
180
  const DEFAULT_RESPONSE_STALL_CHECK_INTERVAL_MS = 5000;
181
181
  const DEFAULT_FINAL_RESPONSE_CLOSE_TIMEOUT_MS = 10_000;
182
182
  export const RESPONSE_STALL_RECOVERY_PROMPT = "完成了吗?如果没完成继续";
183
183
  export const RESPONSE_STALL_RECOVERY_NOTICE =
184
184
  `检测到会话停滞,正在自动确认并继续。\n\n${RESPONSE_STALL_RECOVERY_PROMPT}`;
185
- export const RESPONSE_STALL_RECOVERY_EXHAUSTED_NOTICE =
186
- "⚠️ 自动续跑仍连续 3 分钟没有生成新回复,本次不再自动继续。";
185
+ export const RESPONSE_STALL_RECOVERY_EXHAUSTED_NOTICE =
186
+ "⚠️ 自动续跑仍连续 3 分钟没有生成新回复,本次不再自动继续。";
187
187
  const RESPONSE_STALL_RECOVERY_DELAY_MS = 200;
188
- let processMonitorIntervalMs = DEFAULT_PROCESS_MONITOR_INTERVAL_MS;
189
- let avatarRefreshIntervalMs = DEFAULT_AVATAR_REFRESH_INTERVAL_MS;
190
- let responseStallTimeoutMs = DEFAULT_RESPONSE_STALL_TIMEOUT_MS;
188
+ let processMonitorIntervalMs = DEFAULT_PROCESS_MONITOR_INTERVAL_MS;
189
+ let avatarRefreshIntervalMs = DEFAULT_AVATAR_REFRESH_INTERVAL_MS;
190
+ let responseStallTimeoutMs = DEFAULT_RESPONSE_STALL_TIMEOUT_MS;
191
191
  let responseStallCheckIntervalMs = DEFAULT_RESPONSE_STALL_CHECK_INTERVAL_MS;
192
192
  let finalResponseCloseTimeoutMs = DEFAULT_FINAL_RESPONSE_CLOSE_TIMEOUT_MS;
193
193
  let isProcessAliveImpl = (pid: number): boolean => {
@@ -218,17 +218,17 @@ export function _setProcessMonitorIntervalForTest(ms: number): void {
218
218
  processMonitorIntervalMs = ms;
219
219
  }
220
220
 
221
- export function _resetProcessMonitorIntervalForTest(): void {
222
- processMonitorIntervalMs = DEFAULT_PROCESS_MONITOR_INTERVAL_MS;
223
- }
224
-
225
- export function _setAvatarRefreshIntervalForTest(ms: number): void {
226
- avatarRefreshIntervalMs = ms;
227
- }
228
-
229
- export function _resetAvatarRefreshIntervalForTest(): void {
230
- avatarRefreshIntervalMs = DEFAULT_AVATAR_REFRESH_INTERVAL_MS;
231
- }
221
+ export function _resetProcessMonitorIntervalForTest(): void {
222
+ processMonitorIntervalMs = DEFAULT_PROCESS_MONITOR_INTERVAL_MS;
223
+ }
224
+
225
+ export function _setAvatarRefreshIntervalForTest(ms: number): void {
226
+ avatarRefreshIntervalMs = ms;
227
+ }
228
+
229
+ export function _resetAvatarRefreshIntervalForTest(): void {
230
+ avatarRefreshIntervalMs = DEFAULT_AVATAR_REFRESH_INTERVAL_MS;
231
+ }
232
232
 
233
233
  export function _setResponseStallTimeoutForTest(ms: number): void {
234
234
  responseStallTimeoutMs = ms;
@@ -261,21 +261,21 @@ function clearPromptProcessMonitor(sessionId: string): void {
261
261
  prompt.processMonitor = undefined;
262
262
  }
263
263
 
264
- function clearPromptResponseStallMonitor(sessionId: string): void {
265
- const prompt = activePrompts.get(sessionId);
266
- if (!prompt?.responseStallMonitor) return;
267
- clearInterval(prompt.responseStallMonitor);
268
- prompt.responseStallMonitor = undefined;
269
- }
270
-
271
- function clearPromptAvatarRefreshTimer(sessionId: string): void {
272
- const prompt = activePrompts.get(sessionId);
273
- if (!prompt?.avatarRefreshTimer) return;
274
- clearInterval(prompt.avatarRefreshTimer);
275
- prompt.avatarRefreshTimer = undefined;
276
- }
277
-
278
- function clearPromptFinalResponseCloseTimer(sessionId: string): void {
264
+ function clearPromptResponseStallMonitor(sessionId: string): void {
265
+ const prompt = activePrompts.get(sessionId);
266
+ if (!prompt?.responseStallMonitor) return;
267
+ clearInterval(prompt.responseStallMonitor);
268
+ prompt.responseStallMonitor = undefined;
269
+ }
270
+
271
+ function clearPromptAvatarRefreshTimer(sessionId: string): void {
272
+ const prompt = activePrompts.get(sessionId);
273
+ if (!prompt?.avatarRefreshTimer) return;
274
+ clearInterval(prompt.avatarRefreshTimer);
275
+ prompt.avatarRefreshTimer = undefined;
276
+ }
277
+
278
+ function clearPromptFinalResponseCloseTimer(sessionId: string): void {
279
279
  const prompt = activePrompts.get(sessionId);
280
280
  if (!prompt?.finalResponseCloseTimer) return;
281
281
  clearTimeout(prompt.finalResponseCloseTimer);
@@ -328,18 +328,18 @@ function scheduleFinalResponseCloseGuard(
328
328
  runningPrompt.finalResponseCloseTimer = handle;
329
329
  }
330
330
 
331
- function formatTerminalHeader(
332
- status: "running" | "done" | "stopped" | "error" | "auto_ended",
333
- terminalError?: TerminalErrorInfo,
334
- ): {
331
+ function formatTerminalHeader(
332
+ status: "running" | "done" | "stopped" | "error" | "auto_ended",
333
+ terminalError?: TerminalErrorInfo,
334
+ ): {
335
335
  title: string;
336
336
  template?: string;
337
337
  } {
338
338
  if (status === "auto_ended") return { title: "已自动结束 · 3分钟无新内容", template: "orange" };
339
339
  if (status === "stopped") return { title: "已停止", template: "red" };
340
- if (status === "error") {
341
- return { title: terminalError ? `异常结束 · ${terminalError.title}` : "异常结束", template: "red" };
342
- }
340
+ if (status === "error") {
341
+ return { title: terminalError ? `异常结束 · ${terminalError.title}` : "异常结束", template: "red" };
342
+ }
343
343
  return { title: "完成" };
344
344
  }
345
345
 
@@ -347,59 +347,59 @@ function turnFinalStatus(status: "running" | "done" | "stopped" | "error" | "aut
347
347
  return status === "stopped" || status === "error" || status === "auto_ended" ? "stopped" : "done";
348
348
  }
349
349
 
350
- function formatAutoEndedReply(finalReply: string): string {
351
- const reason = "⚠️ 已自动结束:生成回复阶段连续 3 分钟没有字符变化。";
350
+ function formatAutoEndedReply(finalReply: string): string {
351
+ const reason = "⚠️ 已自动结束:生成回复阶段连续 3 分钟没有字符变化。";
352
352
  return finalReply
353
353
  ? `${reason}以下回复可能不完整。\n\n${finalReply}`
354
354
  : `${reason}本轮没有可发送的回复内容。`;
355
355
  }
356
356
 
357
357
  /**
358
- * 只监控明确的回复生成阶段。启动、压缩、思考、工具调用和搜索都有各自的
359
- * 状态或资源保护,不能把它们消耗的时间算入回复停滞窗口。
358
+ * 只监控明确的回复生成阶段。启动、压缩、思考、工具调用和搜索都有各自的
359
+ * 状态或资源保护,不能把它们消耗的时间算入回复停滞窗口。
360
360
  */
361
- function monitorsOutputProgress(kind: AgentActivityKind): boolean {
362
- return kind === "responding";
363
- }
364
-
365
- function formatTerminalReply(
366
- status: "running" | "done" | "stopped" | "error" | "auto_ended",
367
- finalReply: string,
368
- terminalError?: TerminalErrorInfo,
369
- ): string | null {
370
- if (status === "auto_ended") return formatAutoEndedReply(finalReply);
371
- if (status === "error") {
372
- const error = terminalError ?? {
373
- kind: "unknown" as const,
374
- title: "原因未记录",
375
- message: "当前状态中没有可用的错误详情,请查看运行日志。",
376
- occurredAt: Date.now(),
377
- };
378
- return formatTerminalErrorNotice(error, finalReply);
379
- }
380
- return finalReply || null;
381
- }
382
-
383
- function formatTerminalCardContent(state: {
384
- status: "running" | "done" | "stopped" | "error" | "auto_ended";
385
- accumulatedContent: string;
386
- finalReply: string;
387
- terminalError?: TerminalErrorInfo;
388
- }): string {
389
- const content = state.accumulatedContent + state.finalReply;
390
- if (state.status !== "error") return content;
391
-
392
- const error = state.terminalError ?? {
393
- kind: "unknown" as const,
394
- title: "原因未记录",
395
- message: "当前状态中没有可用的错误详情,请查看运行日志。",
396
- occurredAt: Date.now(),
397
- };
398
- const reason = formatTerminalErrorReason(error);
399
- return content.trim()
400
- ? `${content}\n\n${reason}\n以上内容可能不完整。`
401
- : reason;
402
- }
361
+ function monitorsOutputProgress(kind: AgentActivityKind): boolean {
362
+ return kind === "responding";
363
+ }
364
+
365
+ function formatTerminalReply(
366
+ status: "running" | "done" | "stopped" | "error" | "auto_ended",
367
+ finalReply: string,
368
+ terminalError?: TerminalErrorInfo,
369
+ ): string | null {
370
+ if (status === "auto_ended") return formatAutoEndedReply(finalReply);
371
+ if (status === "error") {
372
+ const error = terminalError ?? {
373
+ kind: "unknown" as const,
374
+ title: "原因未记录",
375
+ message: "当前状态中没有可用的错误详情,请查看运行日志。",
376
+ occurredAt: Date.now(),
377
+ };
378
+ return formatTerminalErrorNotice(error, finalReply);
379
+ }
380
+ return finalReply || null;
381
+ }
382
+
383
+ function formatTerminalCardContent(state: {
384
+ status: "running" | "done" | "stopped" | "error" | "auto_ended";
385
+ accumulatedContent: string;
386
+ finalReply: string;
387
+ terminalError?: TerminalErrorInfo;
388
+ }): string {
389
+ const content = state.accumulatedContent + state.finalReply;
390
+ if (state.status !== "error") return content;
391
+
392
+ const error = state.terminalError ?? {
393
+ kind: "unknown" as const,
394
+ title: "原因未记录",
395
+ message: "当前状态中没有可用的错误详情,请查看运行日志。",
396
+ occurredAt: Date.now(),
397
+ };
398
+ const reason = formatTerminalErrorReason(error);
399
+ return content.trim()
400
+ ? `${content}\n\n${reason}\n以上内容可能不完整。`
401
+ : reason;
402
+ }
403
403
 
404
404
  function isCardKitSequenceConflict(err: unknown): boolean {
405
405
  return err instanceof Error && err.message.includes("300317");
@@ -530,11 +530,11 @@ export function resetState(): void {
530
530
  clearFeishuMessageLedgerMemory();
531
531
  lastMsgTimestamps.clear();
532
532
  chatPlatformMap.clear();
533
- for (const prompt of activePrompts.values()) {
534
- if (prompt.processMonitor) clearInterval(prompt.processMonitor);
535
- if (prompt.responseStallMonitor) clearInterval(prompt.responseStallMonitor);
536
- if (prompt.avatarRefreshTimer) clearInterval(prompt.avatarRefreshTimer);
537
- if (prompt.finalResponseCloseTimer) clearTimeout(prompt.finalResponseCloseTimer);
533
+ for (const prompt of activePrompts.values()) {
534
+ if (prompt.processMonitor) clearInterval(prompt.processMonitor);
535
+ if (prompt.responseStallMonitor) clearInterval(prompt.responseStallMonitor);
536
+ if (prompt.avatarRefreshTimer) clearInterval(prompt.avatarRefreshTimer);
537
+ if (prompt.finalResponseCloseTimer) clearTimeout(prompt.finalResponseCloseTimer);
538
538
  }
539
539
  activePrompts.clear();
540
540
  displayCards.clear();
@@ -600,7 +600,7 @@ export function getEffectiveFastModeForTool(tool: string, sessionId?: string): b
600
600
  return config.codex.fastMode;
601
601
  }
602
602
 
603
- function setSessionChatAvatar(
603
+ function setSessionChatAvatar(
604
604
  platform: PlatformAdapter,
605
605
  chatId: string,
606
606
  tool: string,
@@ -609,59 +609,59 @@ function setSessionChatAvatar(
609
609
  ): Promise<void> {
610
610
  return getEffectiveFastModeForTool(tool, sessionId)
611
611
  ? platform.setChatAvatar(chatId, tool, status, { fastMode: true })
612
- : platform.setChatAvatar(chatId, tool, status);
613
- }
614
-
615
- async function refreshBusySessionAvatar(
616
- sessionId: string,
617
- tool: string,
618
- fallbackPlatform: PlatformAdapter,
619
- ): Promise<void> {
620
- const chatId = pickDisplayChat(sessionId) ?? getLastActiveChat(sessionId) ?? getChatsForSession(sessionId)[0];
621
- if (!chatId) return;
622
- const platform = platformForChat(chatId) ?? fallbackPlatform;
623
- await setSessionChatAvatar(platform, chatId, tool, "busy", sessionId);
624
- }
625
-
626
- function startPromptAvatarRefresh(
627
- sessionId: string,
628
- tool: string,
629
- fallbackPlatform: PlatformAdapter,
630
- runningPrompt: NonNullable<ReturnType<typeof activePrompts.get>>,
631
- ): void {
632
- let refreshInFlight = false;
633
- const timer = setInterval(() => {
634
- const current = activePrompts.get(sessionId);
635
- if (!current || current !== runningPrompt) {
636
- clearInterval(timer);
637
- if (runningPrompt.avatarRefreshTimer === timer) {
638
- runningPrompt.avatarRefreshTimer = undefined;
639
- }
640
- return;
641
- }
642
- if (
643
- refreshInFlight
644
- || current.stopped
645
- || current.abnormalExit
646
- || current.resourceStuck
647
- || current.autoEnded
648
- || current.finalResponseObserved
649
- ) {
650
- return;
651
- }
652
-
653
- refreshInFlight = true;
654
- void refreshBusySessionAvatar(sessionId, tool, fallbackPlatform)
655
- .catch((err) => {
656
- console.warn(`[${ts()}] [AVATAR] Periodic refresh failed for ${sessionId}: ${(err as Error).message}`);
657
- })
658
- .finally(() => {
659
- refreshInFlight = false;
660
- });
661
- }, avatarRefreshIntervalMs);
662
- timer.unref?.();
663
- runningPrompt.avatarRefreshTimer = timer;
664
- }
612
+ : platform.setChatAvatar(chatId, tool, status);
613
+ }
614
+
615
+ async function refreshBusySessionAvatar(
616
+ sessionId: string,
617
+ tool: string,
618
+ fallbackPlatform: PlatformAdapter,
619
+ ): Promise<void> {
620
+ const chatId = pickDisplayChat(sessionId) ?? getLastActiveChat(sessionId) ?? getChatsForSession(sessionId)[0];
621
+ if (!chatId) return;
622
+ const platform = platformForChat(chatId) ?? fallbackPlatform;
623
+ await setSessionChatAvatar(platform, chatId, tool, "busy", sessionId);
624
+ }
625
+
626
+ function startPromptAvatarRefresh(
627
+ sessionId: string,
628
+ tool: string,
629
+ fallbackPlatform: PlatformAdapter,
630
+ runningPrompt: NonNullable<ReturnType<typeof activePrompts.get>>,
631
+ ): void {
632
+ let refreshInFlight = false;
633
+ const timer = setInterval(() => {
634
+ const current = activePrompts.get(sessionId);
635
+ if (!current || current !== runningPrompt) {
636
+ clearInterval(timer);
637
+ if (runningPrompt.avatarRefreshTimer === timer) {
638
+ runningPrompt.avatarRefreshTimer = undefined;
639
+ }
640
+ return;
641
+ }
642
+ if (
643
+ refreshInFlight
644
+ || current.stopped
645
+ || current.abnormalExit
646
+ || current.resourceStuck
647
+ || current.autoEnded
648
+ || current.finalResponseObserved
649
+ ) {
650
+ return;
651
+ }
652
+
653
+ refreshInFlight = true;
654
+ void refreshBusySessionAvatar(sessionId, tool, fallbackPlatform)
655
+ .catch((err) => {
656
+ console.warn(`[${ts()}] [AVATAR] Periodic refresh failed for ${sessionId}: ${(err as Error).message}`);
657
+ })
658
+ .finally(() => {
659
+ refreshInFlight = false;
660
+ });
661
+ }, avatarRefreshIntervalMs);
662
+ timer.unref?.();
663
+ runningPrompt.avatarRefreshTimer = timer;
664
+ }
665
665
 
666
666
  /** 为指定 session 设置模型覆盖(/model <name>) */
667
667
  export function setSessionModelOverride(sessionId: string, model: string): void {
@@ -707,13 +707,13 @@ export function getAdapterForTool(tool: string, sessionId?: string): ToolAdapter
707
707
  effort: effectiveEffort || undefined,
708
708
  fastMode: effectiveFastMode,
709
709
  });
710
- } else if (tool === "ccc") {
711
- adapter = createCccAdapter({
712
- apiKey: config.ccc.DEEPSEEK_API_KEY,
713
- baseURL: config.ccc.DEEPSEEK_BASE_URL,
714
- model: effectiveModel || undefined,
715
- effort: effectiveEffort || undefined,
716
- });
710
+ } else if (tool === "ccc") {
711
+ adapter = createCccAdapter({
712
+ apiKey: config.ccc.DEEPSEEK_API_KEY,
713
+ baseURL: config.ccc.DEEPSEEK_BASE_URL,
714
+ model: effectiveModel || undefined,
715
+ effort: effectiveEffort || undefined,
716
+ });
717
717
  } else {
718
718
  adapter = createClaudeAdapter({
719
719
  model: effectiveModel,
@@ -1019,16 +1019,16 @@ export function accumulateBlockContent(
1019
1019
  // 覆盖而非追加:适配器已保证这是一段完整最终文本(如 Cursor 流末快照)
1020
1020
  state.finalCompleteText = block.text;
1021
1021
  break;
1022
- case "compact_boundary": {
1022
+ case "compact_boundary": {
1023
1023
  const triggerLabel = block.trigger === "manual" ? "手动" : "自动"; // 手动 / 自动
1024
1024
  state.accumulatedContent +=
1025
1025
  `\n\n🔄 上下文压缩(${triggerLabel}): **${block.pre_tokens}** → **${block.post_tokens}** tokens\n`; // 🔄 / →
1026
- break;
1027
- }
1028
- case "agent_status":
1029
- break;
1030
- }
1031
- }
1026
+ break;
1027
+ }
1028
+ case "agent_status":
1029
+ break;
1030
+ }
1031
+ }
1032
1032
 
1033
1033
  // ---------------------------------------------------------------------------
1034
1034
  // switchChatBinding — /newh、/session N 共用的事务式"切换 chat 绑定"
@@ -1217,7 +1217,7 @@ export async function initClaudeSession(tool: string, overrideCwd?: string, chat
1217
1217
  return { sessionId, cwd };
1218
1218
  }
1219
1219
 
1220
- export async function resumeAndPrompt(
1220
+ export async function resumeAndPrompt(
1221
1221
  sessionId: string,
1222
1222
  userText: string,
1223
1223
  platform: PlatformAdapter,
@@ -1225,25 +1225,25 @@ export async function resumeAndPrompt(
1225
1225
  msgTimestamp: number,
1226
1226
  tool: string,
1227
1227
  traceId?: string,
1228
- ): Promise<SessionRunOutcome> {
1229
- return runAgentSession(sessionId, userText, platform, chatId, msgTimestamp, tool, traceId);
1230
- }
1228
+ ): Promise<SessionRunOutcome> {
1229
+ return runAgentSession(sessionId, userText, platform, chatId, msgTimestamp, tool, traceId);
1230
+ }
1231
1231
 
1232
1232
  // ---------------------------------------------------------------------------
1233
1233
  // runAgentSession — session 中心的 agent prompt(文件持久化 + display 解耦)
1234
1234
  // ---------------------------------------------------------------------------
1235
1235
 
1236
- interface RunAgentSessionOptions {
1236
+ interface RunAgentSessionOptions {
1237
1237
  /**
1238
1238
  * 标记本轮是 response-stall 后的唯一一次内部续跑。若本轮再次因相同原因
1239
1239
  * 停滞,不再递归创建第三轮,避免服务异常期间无限消耗 token。
1240
1240
  */
1241
1241
  autoRecovery?: boolean;
1242
- }
1243
-
1244
- export type SessionRunOutcome = "busy" | "done" | "stopped" | "error" | "auto_ended";
1242
+ }
1243
+
1244
+ export type SessionRunOutcome = "busy" | "done" | "stopped" | "error" | "auto_ended";
1245
1245
 
1246
- export async function runAgentSession(
1246
+ export async function runAgentSession(
1247
1247
  sessionId: string,
1248
1248
  userText: string,
1249
1249
  platform: PlatformAdapter,
@@ -1252,7 +1252,7 @@ export async function runAgentSession(
1252
1252
  tool: string,
1253
1253
  traceId?: string,
1254
1254
  options: RunAgentSessionOptions = {},
1255
- ): Promise<SessionRunOutcome> {
1255
+ ): Promise<SessionRunOutcome> {
1256
1256
  const tid = traceId ?? "";
1257
1257
 
1258
1258
  // runAgentSession 是飞书用户消息、队列消息与内部自动恢复共同使用的唯一
@@ -1279,7 +1279,7 @@ export async function runAgentSession(
1279
1279
  ? "当前正在生成回复中,请等待完成后再发送消息。也可以发送 /stop 结束,已完成的步骤不会丢失。"
1280
1280
  : "该会话正在生成回复中,请等待完成后再发送消息。也可以发送 /stop 结束,已完成的步骤不会丢失。";
1281
1281
  await platform.sendText(_chatId, busyMsg).catch(() => {});
1282
- return "busy";
1282
+ return "busy";
1283
1283
  }
1284
1284
 
1285
1285
  // 立即标记活跃,确保 /sessions、isSessionRunning 等查询在异步准备阶段就能看到运行状态。
@@ -1407,7 +1407,7 @@ export async function runAgentSession(
1407
1407
  // 再开始缓存问题对应的任务"。
1408
1408
  const prevState = await readStreamState(sessionId);
1409
1409
  if (prevState && prevState.status !== "running") {
1410
- const prevTerminalReply = formatTerminalReply(prevState.status, prevState.finalReply, prevState.terminalError);
1410
+ const prevTerminalReply = formatTerminalReply(prevState.status, prevState.finalReply, prevState.terminalError);
1411
1411
  const displayChatId = pickDisplayChat(sessionId);
1412
1412
  if (displayChatId) {
1413
1413
  const pp = platformForChat(displayChatId);
@@ -1426,8 +1426,8 @@ export async function runAgentSession(
1426
1426
  setSessionChatAvatar(pp, displayChatId, prevState.tool, "idle", sessionId).catch(() => {});
1427
1427
  } else {
1428
1428
  const nextSeq = display.sequence + 1;
1429
- const { title: headerTitle, template: headerTemplate } = formatTerminalHeader(prevState.status, prevState.terminalError);
1430
- const cardContent = truncateContent(formatTerminalCardContent(prevState)) || " ";
1429
+ const { title: headerTitle, template: headerTemplate } = formatTerminalHeader(prevState.status, prevState.terminalError);
1430
+ const cardContent = truncateContent(formatTerminalCardContent(prevState)) || " ";
1431
1431
  const doneCard = buildProgressCard(progressView({ text: cardContent, status: "done", showStop: false, headerTitle, headerTemplate }));
1432
1432
  await pp.cardUpdate(display.cardId, doneCard, nextSeq).catch(err => {
1433
1433
  console.error(`[${ts()}] [DISPLAY] prevState final cardUpdate failed: ${(err as Error).message}`);
@@ -1506,7 +1506,7 @@ export async function runAgentSession(
1506
1506
  }
1507
1507
 
1508
1508
  // 设置最后活跃群头像为 busy
1509
- refreshBusySessionAvatar(sessionId, tool, platform).catch(() => {});
1509
+ refreshBusySessionAvatar(sessionId, tool, platform).catch(() => {});
1510
1510
 
1511
1511
  const state: AccumulatorState = {
1512
1512
  accumulatedContent: "",
@@ -1517,23 +1517,24 @@ export async function runAgentSession(
1517
1517
 
1518
1518
  let lastFileWrite = Date.now();
1519
1519
  const FILE_WRITE_INTERVAL_MS = 2000;
1520
- const toolCallMap = new Map<string, { name: string; input: unknown }>();
1521
- let streamErrored = false;
1522
- let streamTerminalError: TerminalErrorInfo | undefined;
1523
- let runOutcome: SessionRunOutcome = "error";
1524
-
1525
- const runningPrompt = activePrompts.get(sessionId);
1526
- if (runningPrompt) {
1527
- startPromptAvatarRefresh(sessionId, tool, platform, runningPrompt);
1528
-
1529
- // 在消费第一个事件前建立阶段感知的零字符基线;启动阶段不计时,只有后续
1530
- // 收到明确的 responding 状态后才会启动三分钟回复停滞保护。
1531
- runningPrompt.responseProgress = observeResponseProgress(
1532
- undefined,
1533
- monitorsOutputProgress(activityTracker.activity.kind),
1534
- 0,
1535
- activityTracker.activity.startedAt,
1536
- );
1520
+ const toolCallMap = new Map<string, { name: string; input: unknown }>();
1521
+ let streamErrored = false;
1522
+ let streamTerminalError: TerminalErrorInfo | undefined;
1523
+ let runOutcome: SessionRunOutcome = "error";
1524
+ const responseStallDetectionEnabled = adapter.responseStallDetectionEnabled !== false;
1525
+
1526
+ const runningPrompt = activePrompts.get(sessionId);
1527
+ if (runningPrompt) {
1528
+ startPromptAvatarRefresh(sessionId, tool, platform, runningPrompt);
1529
+
1530
+ // 在消费第一个事件前建立阶段感知的零字符基线;启动阶段不计时,只有后续
1531
+ // 收到明确的 responding 状态后才会启动三分钟回复停滞保护。
1532
+ runningPrompt.responseProgress = observeResponseProgress(
1533
+ undefined,
1534
+ monitorsOutputProgress(activityTracker.activity.kind),
1535
+ 0,
1536
+ activityTracker.activity.startedAt,
1537
+ );
1537
1538
 
1538
1539
  const checkResponseStall = async () => {
1539
1540
  const current = activePrompts.get(sessionId);
@@ -1603,18 +1604,20 @@ export async function runAgentSession(
1603
1604
  current.controller.abort();
1604
1605
  await killProcessTree(current.processPid);
1605
1606
  console.warn(
1606
- `[${ts()}] [RESPONSE-STALL] Session ${sessionId} auto-ended after 3 minutes without reply progress`,
1607
+ `[${ts()}] [RESPONSE-STALL] Session ${sessionId} auto-ended after 3 minutes without reply progress`,
1607
1608
  );
1608
1609
  };
1609
1610
 
1610
- const responseStallMonitor = setInterval(() => {
1611
- void checkResponseStall().catch((err) => {
1612
- console.warn(`[${ts()}] [RESPONSE-STALL] check failed for ${sessionId}: ${(err as Error).message}`);
1613
- });
1614
- }, responseStallCheckIntervalMs);
1615
- responseStallMonitor.unref?.();
1616
- runningPrompt.responseStallMonitor = responseStallMonitor;
1617
- }
1611
+ if (responseStallDetectionEnabled) {
1612
+ const responseStallMonitor = setInterval(() => {
1613
+ void checkResponseStall().catch((err) => {
1614
+ console.warn(`[${ts()}] [RESPONSE-STALL] check failed for ${sessionId}: ${(err as Error).message}`);
1615
+ });
1616
+ }, responseStallCheckIntervalMs);
1617
+ responseStallMonitor.unref?.();
1618
+ runningPrompt.responseStallMonitor = responseStallMonitor;
1619
+ }
1620
+ }
1618
1621
 
1619
1622
  try {
1620
1623
  for await (const unifiedMsg of adapter.prompt(sessionId, userTextWithCapabilities, cwd, controller.signal, {
@@ -1664,7 +1667,7 @@ export async function runAgentSession(
1664
1667
  }
1665
1668
 
1666
1669
  const prompt = activePrompts.get(sessionId);
1667
- if (prompt && !prompt.autoEnded) {
1670
+ if (prompt && !prompt.autoEnded) {
1668
1671
  const totalChars = state.accumulatedContent.length + pickFinalReply(state).length;
1669
1672
  prompt.responseProgress = observeResponseProgress(
1670
1673
  // starting → responding 本身是一次有效进展,即便首个文本块仍为空也应
@@ -1695,10 +1698,10 @@ export async function runAgentSession(
1695
1698
  });
1696
1699
  }
1697
1700
  }
1698
- } catch (streamErr) {
1699
- streamErrored = true;
1700
- streamTerminalError = classifyTerminalError(streamErr);
1701
- console.error(`[${ts()}] [STREAM] Error in stream loop for ${sessionId}: ${(streamErr as Error).message}`);
1701
+ } catch (streamErr) {
1702
+ streamErrored = true;
1703
+ streamTerminalError = classifyTerminalError(streamErr);
1704
+ console.error(`[${ts()}] [STREAM] Error in stream loop for ${sessionId}: ${(streamErr as Error).message}`);
1702
1705
  } finally {
1703
1706
  // 标记 prompt 结束
1704
1707
  resourceMonitor.off("stuck", onResourceStuck);
@@ -1712,10 +1715,10 @@ export async function runAgentSession(
1712
1715
  const wasAutoEnded = timeoutTriggered && !completedAtTimeoutBoundary;
1713
1716
  const wasAutoRecovery = prompt?.autoRecovery ?? false;
1714
1717
  const autoEndedAt = wasAutoEnded ? prompt?.autoEndedAt : undefined;
1715
- clearPromptResponseStallMonitor(sessionId);
1716
- clearPromptProcessMonitor(sessionId);
1717
- clearPromptAvatarRefreshTimer(sessionId);
1718
- clearPromptFinalResponseCloseTimer(sessionId);
1718
+ clearPromptResponseStallMonitor(sessionId);
1719
+ clearPromptProcessMonitor(sessionId);
1720
+ clearPromptAvatarRefreshTimer(sessionId);
1721
+ clearPromptFinalResponseCloseTimer(sessionId);
1719
1722
  markSessionFinalizing(sessionId);
1720
1723
  activePrompts.delete(sessionId);
1721
1724
 
@@ -1747,24 +1750,24 @@ export async function runAgentSession(
1747
1750
  : wasStopped
1748
1751
  ? "stopped"
1749
1752
  : "done";
1750
- const finalReply = pickFinalReply(state).trim();
1751
- const terminalError = streamTerminalError
1752
- ?? (wasAbnormalExit
1753
- ? {
1754
- kind: "process" as const,
1755
- title: "Agent 进程意外退出",
1756
- message: "Agent CLI 进程已退出。若回复不完整,请重新发送上一条指令。",
1757
- occurredAt: Date.now(),
1758
- }
1759
- : wasResourceStuck
1760
- ? {
1761
- kind: "resource" as const,
1762
- title: "Agent 进程失去响应",
1763
- message: "Agent 进程长时间没有 CPU 或内存变化,已被强制停止。若回复不完整,请重新发送上一条指令。",
1764
- occurredAt: Date.now(),
1765
- }
1766
- : undefined);
1767
- runOutcome = finalStatus;
1753
+ const finalReply = pickFinalReply(state).trim();
1754
+ const terminalError = streamTerminalError
1755
+ ?? (wasAbnormalExit
1756
+ ? {
1757
+ kind: "process" as const,
1758
+ title: "Agent 进程意外退出",
1759
+ message: "Agent CLI 进程已退出。若回复不完整,请重新发送上一条指令。",
1760
+ occurredAt: Date.now(),
1761
+ }
1762
+ : wasResourceStuck
1763
+ ? {
1764
+ kind: "resource" as const,
1765
+ title: "Agent 进程失去响应",
1766
+ message: "Agent 进程长时间没有 CPU 或内存变化,已被强制停止。若回复不完整,请重新发送上一条指令。",
1767
+ occurredAt: Date.now(),
1768
+ }
1769
+ : undefined);
1770
+ runOutcome = finalStatus;
1768
1771
 
1769
1772
  // stop-stuck-loop 接口可能在 fire-and-forget 中已写入带 final_reply 的
1770
1773
  // stream state,finally 不应覆盖它。同时保留 stuckAt 标记,防止
@@ -1794,9 +1797,9 @@ export async function runAgentSession(
1794
1797
  cwd,
1795
1798
  tool,
1796
1799
  ...(preserveStuckAt ? { stuckAt: preserveStuckAt } : {}),
1797
- ...(autoEndedAt !== undefined ? { autoEndedAt } : {}),
1798
- ...(terminalError ? { terminalError } : {}),
1799
- });
1800
+ ...(autoEndedAt !== undefined ? { autoEndedAt } : {}),
1801
+ ...(terminalError ? { terminalError } : {}),
1802
+ });
1800
1803
 
1801
1804
  // display loop 下一轮会读到最终状态并发送消息
1802
1805
 
@@ -1867,9 +1870,9 @@ export async function runAgentSession(
1867
1870
  autoRecoveryTarget = { chatId: activeAutoEnded, platform: pp };
1868
1871
  }
1869
1872
  }
1870
- console.warn(`[${ts()}] Session ${sessionId} auto-ended after stalled response output (content chunks: ${state.chunkCount})`);
1873
+ console.warn(`[${ts()}] Session ${sessionId} auto-ended after stalled response output (content chunks: ${state.chunkCount})`);
1871
1874
  if (tid) logTrace(tid, "SESSION_END", { sessionId, outcome: "response_stall", chunks: state.chunkCount });
1872
- } else if (wasAbnormalExit) {
1875
+ } else if (wasAbnormalExit) {
1873
1876
  for (const cid of finalizationChatIds) {
1874
1877
  const finfo = sessionInfoMap.get(cid);
1875
1878
  await recordSessionRegistry({
@@ -1884,55 +1887,55 @@ export async function runAgentSession(
1884
1887
  }
1885
1888
  const activeErr = getLastActiveChat(sessionId) ?? finalizationChatIds[0];
1886
1889
  if (activeErr) setSessionChatAvatar(platform, activeErr, tool, "idle", sessionId).catch(() => {});
1887
- console.log(`[${ts()}] Session ${sessionId} process exited unexpectedly (content chunks: ${state.chunkCount})`);
1888
- if (tid) logTrace(tid, "SESSION_END", { sessionId, outcome: "process_missing", chunks: state.chunkCount });
1889
- } else if (streamErrored || wasResourceStuck) {
1890
- for (const cid of finalizationChatIds) {
1891
- const finfo = sessionInfoMap.get(cid);
1892
- await recordSessionRegistry({
1893
- chatId: cid,
1894
- sessionId,
1895
- tool,
1896
- turnCount: finfo?.turnCount ?? nextTurnCount,
1897
- lastContextTokens: finfo?.lastContextTokens ?? nextContextTokens,
1898
- startTime: finfo?.startTime ?? now,
1899
- running: false,
1900
- });
1901
- }
1902
- const activeErr = getLastActiveChat(sessionId) ?? finalizationChatIds[0];
1903
- if (activeErr) {
1904
- const pp = platformForChat(activeErr) ?? platform;
1905
- const terminalState = await readStreamState(sessionId);
1906
- if (
1907
- terminalError
1908
- && !displayCards.has(activeErr)
1909
- && (!terminalState || !isFinalReplySentForTurn(terminalState))
1910
- ) {
1911
- await sendFinalReplyTextOnce(
1912
- pp,
1913
- activeErr,
1914
- sessionId,
1915
- nextTurnCount,
1916
- formatTerminalErrorNotice(terminalError, finalReplyToWrite),
1917
- );
1918
- }
1919
- setSessionChatAvatar(pp, activeErr, tool, "idle", sessionId).catch(() => {});
1920
- }
1921
- const errorOutcome = wasResourceStuck ? "resource_stuck" : "stream_error";
1922
- console.error(
1923
- `[${ts()}] Session ${sessionId} ended with ${errorOutcome}` +
1924
- `${terminalError ? ` (${terminalError.kind}: ${terminalError.title})` : ""} (content chunks: ${state.chunkCount})`,
1925
- );
1926
- if (tid) {
1927
- logTrace(tid, "SESSION_END", {
1928
- sessionId,
1929
- outcome: errorOutcome,
1930
- errorKind: terminalError?.kind,
1931
- errorTitle: terminalError?.title,
1932
- chunks: state.chunkCount,
1933
- });
1934
- }
1935
- } else {
1890
+ console.log(`[${ts()}] Session ${sessionId} process exited unexpectedly (content chunks: ${state.chunkCount})`);
1891
+ if (tid) logTrace(tid, "SESSION_END", { sessionId, outcome: "process_missing", chunks: state.chunkCount });
1892
+ } else if (streamErrored || wasResourceStuck) {
1893
+ for (const cid of finalizationChatIds) {
1894
+ const finfo = sessionInfoMap.get(cid);
1895
+ await recordSessionRegistry({
1896
+ chatId: cid,
1897
+ sessionId,
1898
+ tool,
1899
+ turnCount: finfo?.turnCount ?? nextTurnCount,
1900
+ lastContextTokens: finfo?.lastContextTokens ?? nextContextTokens,
1901
+ startTime: finfo?.startTime ?? now,
1902
+ running: false,
1903
+ });
1904
+ }
1905
+ const activeErr = getLastActiveChat(sessionId) ?? finalizationChatIds[0];
1906
+ if (activeErr) {
1907
+ const pp = platformForChat(activeErr) ?? platform;
1908
+ const terminalState = await readStreamState(sessionId);
1909
+ if (
1910
+ terminalError
1911
+ && !displayCards.has(activeErr)
1912
+ && (!terminalState || !isFinalReplySentForTurn(terminalState))
1913
+ ) {
1914
+ await sendFinalReplyTextOnce(
1915
+ pp,
1916
+ activeErr,
1917
+ sessionId,
1918
+ nextTurnCount,
1919
+ formatTerminalErrorNotice(terminalError, finalReplyToWrite),
1920
+ );
1921
+ }
1922
+ setSessionChatAvatar(pp, activeErr, tool, "idle", sessionId).catch(() => {});
1923
+ }
1924
+ const errorOutcome = wasResourceStuck ? "resource_stuck" : "stream_error";
1925
+ console.error(
1926
+ `[${ts()}] Session ${sessionId} ended with ${errorOutcome}` +
1927
+ `${terminalError ? ` (${terminalError.kind}: ${terminalError.title})` : ""} (content chunks: ${state.chunkCount})`,
1928
+ );
1929
+ if (tid) {
1930
+ logTrace(tid, "SESSION_END", {
1931
+ sessionId,
1932
+ outcome: errorOutcome,
1933
+ errorKind: terminalError?.kind,
1934
+ errorTitle: terminalError?.title,
1935
+ chunks: state.chunkCount,
1936
+ });
1937
+ }
1938
+ } else {
1936
1939
  for (const cid of finalizationChatIds) {
1937
1940
  const finfo = sessionInfoMap.get(cid);
1938
1941
  await recordSessionRegistry({
@@ -2044,9 +2047,9 @@ export async function runAgentSession(
2044
2047
  } finally {
2045
2048
  clearSessionFinalizing(sessionId);
2046
2049
  }
2047
- }
2048
- return runOutcome;
2049
- }
2050
+ }
2051
+ return runOutcome;
2052
+ }
2050
2053
 
2051
2054
  // ---------------------------------------------------------------------------
2052
2055
  // startUnifiedDisplayLoop — 全局统一 display 循环,遍历 displayCards 更新卡片
@@ -2123,13 +2126,13 @@ export function startUnifiedDisplayLoop(): void {
2123
2126
  if (activePrompts.has(sessionId)) continue;
2124
2127
 
2125
2128
  const tail = "━━━ 回答结束 ━━━";
2126
- const finalMsg = state.status === "auto_ended"
2127
- ? formatAutoEndedReply(remaining)
2128
- : state.status === "error"
2129
- ? formatTerminalReply(state.status, remaining, state.terminalError) ?? tail
2130
- : remaining
2131
- ? remaining + "\n" + tail
2132
- : tail;
2129
+ const finalMsg = state.status === "auto_ended"
2130
+ ? formatAutoEndedReply(remaining)
2131
+ : state.status === "error"
2132
+ ? formatTerminalReply(state.status, remaining, state.terminalError) ?? tail
2133
+ : remaining
2134
+ ? remaining + "\n" + tail
2135
+ : tail;
2133
2136
  if (!isFinalReplySentForTurn(state)) {
2134
2137
  await sendFinalReplyTextOnce(p, chatId, sessionId, state.turnCount, finalMsg);
2135
2138
  }
@@ -2151,8 +2154,8 @@ export function startUnifiedDisplayLoop(): void {
2151
2154
  let terminalCardUpdateAccepted = terminalCardAlreadyUpdated;
2152
2155
  if (!terminalCardAlreadyUpdated) {
2153
2156
  const nextSeq = display.sequence + 1;
2154
- const { title: headerTitle, template: headerTemplate } = formatTerminalHeader(state.status, state.terminalError);
2155
- const cardContent = truncateContent(formatTerminalCardContent(state)) || " ";
2157
+ const { title: headerTitle, template: headerTemplate } = formatTerminalHeader(state.status, state.terminalError);
2158
+ const cardContent = truncateContent(formatTerminalCardContent(state)) || " ";
2156
2159
  const doneCard = buildProgressCard(progressView({ text: cardContent, status: "done", showStop: false, headerTitle, headerTemplate }));
2157
2160
  await p.cardUpdate(display.cardId, doneCard, nextSeq).then(() => {
2158
2161
  display.sequence = nextSeq;
@@ -2179,19 +2182,19 @@ export function startUnifiedDisplayLoop(): void {
2179
2182
  }
2180
2183
 
2181
2184
  let terminalTextDelivered = true;
2182
- const terminalReply = formatTerminalReply(state.status, state.finalReply, state.terminalError);
2183
- const errorWasDeliveredByCard =
2184
- state.status === "error"
2185
- && !state.finalReply.trim()
2186
- && terminalCardUpdateAccepted;
2187
- if (errorWasDeliveredByCard) {
2188
- if (!isFinalReplySentForTurn(state)) {
2189
- await markFinalReplySent(sessionId, state.turnCount);
2190
- }
2191
- } else if (terminalReply) {
2192
- if (!isFinalReplySentForTurn(state)) {
2193
- terminalTextDelivered = await sendFinalReplyTextOnce(p, chatId, sessionId, state.turnCount, terminalReply);
2194
- }
2185
+ const terminalReply = formatTerminalReply(state.status, state.finalReply, state.terminalError);
2186
+ const errorWasDeliveredByCard =
2187
+ state.status === "error"
2188
+ && !state.finalReply.trim()
2189
+ && terminalCardUpdateAccepted;
2190
+ if (errorWasDeliveredByCard) {
2191
+ if (!isFinalReplySentForTurn(state)) {
2192
+ await markFinalReplySent(sessionId, state.turnCount);
2193
+ }
2194
+ } else if (terminalReply) {
2195
+ if (!isFinalReplySentForTurn(state)) {
2196
+ terminalTextDelivered = await sendFinalReplyTextOnce(p, chatId, sessionId, state.turnCount, terminalReply);
2197
+ }
2195
2198
  } else if (state.accumulatedContent.trim()) {
2196
2199
  const short = truncateContent(state.accumulatedContent, 30, 4000);
2197
2200
  terminalTextDelivered = await p.sendText(chatId, `[生成过程]\n${short}`).then((ok) => ok !== false).catch(() => false);
@@ -2422,11 +2425,11 @@ export function stopSession(sessionId: string): boolean {
2422
2425
  }
2423
2426
  return false;
2424
2427
  }
2425
- prompt.stopped = true;
2426
- clearPromptResponseStallMonitor(sessionId);
2427
- clearPromptProcessMonitor(sessionId);
2428
- clearPromptAvatarRefreshTimer(sessionId);
2429
- clearPromptFinalResponseCloseTimer(sessionId);
2428
+ prompt.stopped = true;
2429
+ clearPromptResponseStallMonitor(sessionId);
2430
+ clearPromptProcessMonitor(sessionId);
2431
+ clearPromptAvatarRefreshTimer(sessionId);
2432
+ clearPromptFinalResponseCloseTimer(sessionId);
2430
2433
  cancelQueuedMessage(sessionId);
2431
2434
 
2432
2435
  // 先发起整棵进程树清理,再触发 close/abort。Windows 上 CLI 由