chatccc 0.2.245 → 0.2.247

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/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,15 @@ 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
+ // 留空("")不传 → ChatSession 跟随 DeepCCC 内核配置(~/.deepccc/config.json 或 DEEPCCC_PROVIDER)
717
+ ...(config.ccc.provider ? { provider: config.ccc.provider } : {}),
718
+ });
717
719
  } else {
718
720
  adapter = createClaudeAdapter({
719
721
  model: effectiveModel,
@@ -1019,16 +1021,16 @@ export function accumulateBlockContent(
1019
1021
  // 覆盖而非追加:适配器已保证这是一段完整最终文本(如 Cursor 流末快照)
1020
1022
  state.finalCompleteText = block.text;
1021
1023
  break;
1022
- case "compact_boundary": {
1024
+ case "compact_boundary": {
1023
1025
  const triggerLabel = block.trigger === "manual" ? "手动" : "自动"; // 手动 / 自动
1024
1026
  state.accumulatedContent +=
1025
1027
  `\n\n🔄 上下文压缩(${triggerLabel}): **${block.pre_tokens}** → **${block.post_tokens}** tokens\n`; // 🔄 / →
1026
- break;
1027
- }
1028
- case "agent_status":
1029
- break;
1030
- }
1031
- }
1028
+ break;
1029
+ }
1030
+ case "agent_status":
1031
+ break;
1032
+ }
1033
+ }
1032
1034
 
1033
1035
  // ---------------------------------------------------------------------------
1034
1036
  // switchChatBinding — /newh、/session N 共用的事务式"切换 chat 绑定"
@@ -1217,7 +1219,7 @@ export async function initClaudeSession(tool: string, overrideCwd?: string, chat
1217
1219
  return { sessionId, cwd };
1218
1220
  }
1219
1221
 
1220
- export async function resumeAndPrompt(
1222
+ export async function resumeAndPrompt(
1221
1223
  sessionId: string,
1222
1224
  userText: string,
1223
1225
  platform: PlatformAdapter,
@@ -1225,25 +1227,25 @@ export async function resumeAndPrompt(
1225
1227
  msgTimestamp: number,
1226
1228
  tool: string,
1227
1229
  traceId?: string,
1228
- ): Promise<SessionRunOutcome> {
1229
- return runAgentSession(sessionId, userText, platform, chatId, msgTimestamp, tool, traceId);
1230
- }
1230
+ ): Promise<SessionRunOutcome> {
1231
+ return runAgentSession(sessionId, userText, platform, chatId, msgTimestamp, tool, traceId);
1232
+ }
1231
1233
 
1232
1234
  // ---------------------------------------------------------------------------
1233
1235
  // runAgentSession — session 中心的 agent prompt(文件持久化 + display 解耦)
1234
1236
  // ---------------------------------------------------------------------------
1235
1237
 
1236
- interface RunAgentSessionOptions {
1238
+ interface RunAgentSessionOptions {
1237
1239
  /**
1238
1240
  * 标记本轮是 response-stall 后的唯一一次内部续跑。若本轮再次因相同原因
1239
1241
  * 停滞,不再递归创建第三轮,避免服务异常期间无限消耗 token。
1240
1242
  */
1241
1243
  autoRecovery?: boolean;
1242
- }
1243
-
1244
- export type SessionRunOutcome = "busy" | "done" | "stopped" | "error" | "auto_ended";
1244
+ }
1245
+
1246
+ export type SessionRunOutcome = "busy" | "done" | "stopped" | "error" | "auto_ended";
1245
1247
 
1246
- export async function runAgentSession(
1248
+ export async function runAgentSession(
1247
1249
  sessionId: string,
1248
1250
  userText: string,
1249
1251
  platform: PlatformAdapter,
@@ -1252,7 +1254,7 @@ export async function runAgentSession(
1252
1254
  tool: string,
1253
1255
  traceId?: string,
1254
1256
  options: RunAgentSessionOptions = {},
1255
- ): Promise<SessionRunOutcome> {
1257
+ ): Promise<SessionRunOutcome> {
1256
1258
  const tid = traceId ?? "";
1257
1259
 
1258
1260
  // runAgentSession 是飞书用户消息、队列消息与内部自动恢复共同使用的唯一
@@ -1279,7 +1281,7 @@ export async function runAgentSession(
1279
1281
  ? "当前正在生成回复中,请等待完成后再发送消息。也可以发送 /stop 结束,已完成的步骤不会丢失。"
1280
1282
  : "该会话正在生成回复中,请等待完成后再发送消息。也可以发送 /stop 结束,已完成的步骤不会丢失。";
1281
1283
  await platform.sendText(_chatId, busyMsg).catch(() => {});
1282
- return "busy";
1284
+ return "busy";
1283
1285
  }
1284
1286
 
1285
1287
  // 立即标记活跃,确保 /sessions、isSessionRunning 等查询在异步准备阶段就能看到运行状态。
@@ -1407,7 +1409,7 @@ export async function runAgentSession(
1407
1409
  // 再开始缓存问题对应的任务"。
1408
1410
  const prevState = await readStreamState(sessionId);
1409
1411
  if (prevState && prevState.status !== "running") {
1410
- const prevTerminalReply = formatTerminalReply(prevState.status, prevState.finalReply, prevState.terminalError);
1412
+ const prevTerminalReply = formatTerminalReply(prevState.status, prevState.finalReply, prevState.terminalError);
1411
1413
  const displayChatId = pickDisplayChat(sessionId);
1412
1414
  if (displayChatId) {
1413
1415
  const pp = platformForChat(displayChatId);
@@ -1426,8 +1428,8 @@ export async function runAgentSession(
1426
1428
  setSessionChatAvatar(pp, displayChatId, prevState.tool, "idle", sessionId).catch(() => {});
1427
1429
  } else {
1428
1430
  const nextSeq = display.sequence + 1;
1429
- const { title: headerTitle, template: headerTemplate } = formatTerminalHeader(prevState.status, prevState.terminalError);
1430
- const cardContent = truncateContent(formatTerminalCardContent(prevState)) || " ";
1431
+ const { title: headerTitle, template: headerTemplate } = formatTerminalHeader(prevState.status, prevState.terminalError);
1432
+ const cardContent = truncateContent(formatTerminalCardContent(prevState)) || " ";
1431
1433
  const doneCard = buildProgressCard(progressView({ text: cardContent, status: "done", showStop: false, headerTitle, headerTemplate }));
1432
1434
  await pp.cardUpdate(display.cardId, doneCard, nextSeq).catch(err => {
1433
1435
  console.error(`[${ts()}] [DISPLAY] prevState final cardUpdate failed: ${(err as Error).message}`);
@@ -1506,7 +1508,7 @@ export async function runAgentSession(
1506
1508
  }
1507
1509
 
1508
1510
  // 设置最后活跃群头像为 busy
1509
- refreshBusySessionAvatar(sessionId, tool, platform).catch(() => {});
1511
+ refreshBusySessionAvatar(sessionId, tool, platform).catch(() => {});
1510
1512
 
1511
1513
  const state: AccumulatorState = {
1512
1514
  accumulatedContent: "",
@@ -1517,24 +1519,24 @@ export async function runAgentSession(
1517
1519
 
1518
1520
  let lastFileWrite = Date.now();
1519
1521
  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
- 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
- );
1522
+ const toolCallMap = new Map<string, { name: string; input: unknown }>();
1523
+ let streamErrored = false;
1524
+ let streamTerminalError: TerminalErrorInfo | undefined;
1525
+ let runOutcome: SessionRunOutcome = "error";
1526
+ const responseStallDetectionEnabled = adapter.responseStallDetectionEnabled !== false;
1527
+
1528
+ const runningPrompt = activePrompts.get(sessionId);
1529
+ if (runningPrompt) {
1530
+ startPromptAvatarRefresh(sessionId, tool, platform, runningPrompt);
1531
+
1532
+ // 在消费第一个事件前建立阶段感知的零字符基线;启动阶段不计时,只有后续
1533
+ // 收到明确的 responding 状态后才会启动三分钟回复停滞保护。
1534
+ runningPrompt.responseProgress = observeResponseProgress(
1535
+ undefined,
1536
+ monitorsOutputProgress(activityTracker.activity.kind),
1537
+ 0,
1538
+ activityTracker.activity.startedAt,
1539
+ );
1538
1540
 
1539
1541
  const checkResponseStall = async () => {
1540
1542
  const current = activePrompts.get(sessionId);
@@ -1604,20 +1606,20 @@ export async function runAgentSession(
1604
1606
  current.controller.abort();
1605
1607
  await killProcessTree(current.processPid);
1606
1608
  console.warn(
1607
- `[${ts()}] [RESPONSE-STALL] Session ${sessionId} auto-ended after 3 minutes without reply progress`,
1609
+ `[${ts()}] [RESPONSE-STALL] Session ${sessionId} auto-ended after 3 minutes without reply progress`,
1608
1610
  );
1609
1611
  };
1610
1612
 
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
- }
1613
+ if (responseStallDetectionEnabled) {
1614
+ const responseStallMonitor = setInterval(() => {
1615
+ void checkResponseStall().catch((err) => {
1616
+ console.warn(`[${ts()}] [RESPONSE-STALL] check failed for ${sessionId}: ${(err as Error).message}`);
1617
+ });
1618
+ }, responseStallCheckIntervalMs);
1619
+ responseStallMonitor.unref?.();
1620
+ runningPrompt.responseStallMonitor = responseStallMonitor;
1621
+ }
1622
+ }
1621
1623
 
1622
1624
  try {
1623
1625
  for await (const unifiedMsg of adapter.prompt(sessionId, userTextWithCapabilities, cwd, controller.signal, {
@@ -1667,7 +1669,7 @@ export async function runAgentSession(
1667
1669
  }
1668
1670
 
1669
1671
  const prompt = activePrompts.get(sessionId);
1670
- if (prompt && !prompt.autoEnded) {
1672
+ if (prompt && !prompt.autoEnded) {
1671
1673
  const totalChars = state.accumulatedContent.length + pickFinalReply(state).length;
1672
1674
  prompt.responseProgress = observeResponseProgress(
1673
1675
  // starting → responding 本身是一次有效进展,即便首个文本块仍为空也应
@@ -1698,10 +1700,10 @@ export async function runAgentSession(
1698
1700
  });
1699
1701
  }
1700
1702
  }
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}`);
1703
+ } catch (streamErr) {
1704
+ streamErrored = true;
1705
+ streamTerminalError = classifyTerminalError(streamErr);
1706
+ console.error(`[${ts()}] [STREAM] Error in stream loop for ${sessionId}: ${(streamErr as Error).message}`);
1705
1707
  } finally {
1706
1708
  // 标记 prompt 结束
1707
1709
  resourceMonitor.off("stuck", onResourceStuck);
@@ -1715,10 +1717,10 @@ export async function runAgentSession(
1715
1717
  const wasAutoEnded = timeoutTriggered && !completedAtTimeoutBoundary;
1716
1718
  const wasAutoRecovery = prompt?.autoRecovery ?? false;
1717
1719
  const autoEndedAt = wasAutoEnded ? prompt?.autoEndedAt : undefined;
1718
- clearPromptResponseStallMonitor(sessionId);
1719
- clearPromptProcessMonitor(sessionId);
1720
- clearPromptAvatarRefreshTimer(sessionId);
1721
- clearPromptFinalResponseCloseTimer(sessionId);
1720
+ clearPromptResponseStallMonitor(sessionId);
1721
+ clearPromptProcessMonitor(sessionId);
1722
+ clearPromptAvatarRefreshTimer(sessionId);
1723
+ clearPromptFinalResponseCloseTimer(sessionId);
1722
1724
  markSessionFinalizing(sessionId);
1723
1725
  activePrompts.delete(sessionId);
1724
1726
 
@@ -1750,24 +1752,24 @@ export async function runAgentSession(
1750
1752
  : wasStopped
1751
1753
  ? "stopped"
1752
1754
  : "done";
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;
1755
+ const finalReply = pickFinalReply(state).trim();
1756
+ const terminalError = streamTerminalError
1757
+ ?? (wasAbnormalExit
1758
+ ? {
1759
+ kind: "process" as const,
1760
+ title: "Agent 进程意外退出",
1761
+ message: "Agent CLI 进程已退出。若回复不完整,请重新发送上一条指令。",
1762
+ occurredAt: Date.now(),
1763
+ }
1764
+ : wasResourceStuck
1765
+ ? {
1766
+ kind: "resource" as const,
1767
+ title: "Agent 进程失去响应",
1768
+ message: "Agent 进程长时间没有 CPU 或内存变化,已被强制停止。若回复不完整,请重新发送上一条指令。",
1769
+ occurredAt: Date.now(),
1770
+ }
1771
+ : undefined);
1772
+ runOutcome = finalStatus;
1771
1773
 
1772
1774
  // stop-stuck-loop 接口可能在 fire-and-forget 中已写入带 final_reply 的
1773
1775
  // stream state,finally 不应覆盖它。同时保留 stuckAt 标记,防止
@@ -1797,9 +1799,9 @@ export async function runAgentSession(
1797
1799
  cwd,
1798
1800
  tool,
1799
1801
  ...(preserveStuckAt ? { stuckAt: preserveStuckAt } : {}),
1800
- ...(autoEndedAt !== undefined ? { autoEndedAt } : {}),
1801
- ...(terminalError ? { terminalError } : {}),
1802
- });
1802
+ ...(autoEndedAt !== undefined ? { autoEndedAt } : {}),
1803
+ ...(terminalError ? { terminalError } : {}),
1804
+ });
1803
1805
 
1804
1806
  // display loop 下一轮会读到最终状态并发送消息
1805
1807
 
@@ -1870,9 +1872,9 @@ export async function runAgentSession(
1870
1872
  autoRecoveryTarget = { chatId: activeAutoEnded, platform: pp };
1871
1873
  }
1872
1874
  }
1873
- console.warn(`[${ts()}] Session ${sessionId} auto-ended after stalled response output (content chunks: ${state.chunkCount})`);
1875
+ console.warn(`[${ts()}] Session ${sessionId} auto-ended after stalled response output (content chunks: ${state.chunkCount})`);
1874
1876
  if (tid) logTrace(tid, "SESSION_END", { sessionId, outcome: "response_stall", chunks: state.chunkCount });
1875
- } else if (wasAbnormalExit) {
1877
+ } else if (wasAbnormalExit) {
1876
1878
  for (const cid of finalizationChatIds) {
1877
1879
  const finfo = sessionInfoMap.get(cid);
1878
1880
  await recordSessionRegistry({
@@ -1887,55 +1889,55 @@ export async function runAgentSession(
1887
1889
  }
1888
1890
  const activeErr = getLastActiveChat(sessionId) ?? finalizationChatIds[0];
1889
1891
  if (activeErr) setSessionChatAvatar(platform, activeErr, tool, "idle", sessionId).catch(() => {});
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 {
1892
+ console.log(`[${ts()}] Session ${sessionId} process exited unexpectedly (content chunks: ${state.chunkCount})`);
1893
+ if (tid) logTrace(tid, "SESSION_END", { sessionId, outcome: "process_missing", chunks: state.chunkCount });
1894
+ } else if (streamErrored || wasResourceStuck) {
1895
+ for (const cid of finalizationChatIds) {
1896
+ const finfo = sessionInfoMap.get(cid);
1897
+ await recordSessionRegistry({
1898
+ chatId: cid,
1899
+ sessionId,
1900
+ tool,
1901
+ turnCount: finfo?.turnCount ?? nextTurnCount,
1902
+ lastContextTokens: finfo?.lastContextTokens ?? nextContextTokens,
1903
+ startTime: finfo?.startTime ?? now,
1904
+ running: false,
1905
+ });
1906
+ }
1907
+ const activeErr = getLastActiveChat(sessionId) ?? finalizationChatIds[0];
1908
+ if (activeErr) {
1909
+ const pp = platformForChat(activeErr) ?? platform;
1910
+ const terminalState = await readStreamState(sessionId);
1911
+ if (
1912
+ terminalError
1913
+ && !displayCards.has(activeErr)
1914
+ && (!terminalState || !isFinalReplySentForTurn(terminalState))
1915
+ ) {
1916
+ await sendFinalReplyTextOnce(
1917
+ pp,
1918
+ activeErr,
1919
+ sessionId,
1920
+ nextTurnCount,
1921
+ formatTerminalErrorNotice(terminalError, finalReplyToWrite),
1922
+ );
1923
+ }
1924
+ setSessionChatAvatar(pp, activeErr, tool, "idle", sessionId).catch(() => {});
1925
+ }
1926
+ const errorOutcome = wasResourceStuck ? "resource_stuck" : "stream_error";
1927
+ console.error(
1928
+ `[${ts()}] Session ${sessionId} ended with ${errorOutcome}` +
1929
+ `${terminalError ? ` (${terminalError.kind}: ${terminalError.title})` : ""} (content chunks: ${state.chunkCount})`,
1930
+ );
1931
+ if (tid) {
1932
+ logTrace(tid, "SESSION_END", {
1933
+ sessionId,
1934
+ outcome: errorOutcome,
1935
+ errorKind: terminalError?.kind,
1936
+ errorTitle: terminalError?.title,
1937
+ chunks: state.chunkCount,
1938
+ });
1939
+ }
1940
+ } else {
1939
1941
  for (const cid of finalizationChatIds) {
1940
1942
  const finfo = sessionInfoMap.get(cid);
1941
1943
  await recordSessionRegistry({
@@ -2047,9 +2049,9 @@ export async function runAgentSession(
2047
2049
  } finally {
2048
2050
  clearSessionFinalizing(sessionId);
2049
2051
  }
2050
- }
2051
- return runOutcome;
2052
- }
2052
+ }
2053
+ return runOutcome;
2054
+ }
2053
2055
 
2054
2056
  // ---------------------------------------------------------------------------
2055
2057
  // startUnifiedDisplayLoop — 全局统一 display 循环,遍历 displayCards 更新卡片
@@ -2126,13 +2128,13 @@ export function startUnifiedDisplayLoop(): void {
2126
2128
  if (activePrompts.has(sessionId)) continue;
2127
2129
 
2128
2130
  const 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;
2131
+ const finalMsg = state.status === "auto_ended"
2132
+ ? formatAutoEndedReply(remaining)
2133
+ : state.status === "error"
2134
+ ? formatTerminalReply(state.status, remaining, state.terminalError) ?? tail
2135
+ : remaining
2136
+ ? remaining + "\n" + tail
2137
+ : tail;
2136
2138
  if (!isFinalReplySentForTurn(state)) {
2137
2139
  await sendFinalReplyTextOnce(p, chatId, sessionId, state.turnCount, finalMsg);
2138
2140
  }
@@ -2154,8 +2156,8 @@ export function startUnifiedDisplayLoop(): void {
2154
2156
  let terminalCardUpdateAccepted = terminalCardAlreadyUpdated;
2155
2157
  if (!terminalCardAlreadyUpdated) {
2156
2158
  const nextSeq = display.sequence + 1;
2157
- const { title: headerTitle, template: headerTemplate } = formatTerminalHeader(state.status, state.terminalError);
2158
- const cardContent = truncateContent(formatTerminalCardContent(state)) || " ";
2159
+ const { title: headerTitle, template: headerTemplate } = formatTerminalHeader(state.status, state.terminalError);
2160
+ const cardContent = truncateContent(formatTerminalCardContent(state)) || " ";
2159
2161
  const doneCard = buildProgressCard(progressView({ text: cardContent, status: "done", showStop: false, headerTitle, headerTemplate }));
2160
2162
  await p.cardUpdate(display.cardId, doneCard, nextSeq).then(() => {
2161
2163
  display.sequence = nextSeq;
@@ -2182,19 +2184,19 @@ export function startUnifiedDisplayLoop(): void {
2182
2184
  }
2183
2185
 
2184
2186
  let terminalTextDelivered = true;
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
- }
2187
+ const terminalReply = formatTerminalReply(state.status, state.finalReply, state.terminalError);
2188
+ const errorWasDeliveredByCard =
2189
+ state.status === "error"
2190
+ && !state.finalReply.trim()
2191
+ && terminalCardUpdateAccepted;
2192
+ if (errorWasDeliveredByCard) {
2193
+ if (!isFinalReplySentForTurn(state)) {
2194
+ await markFinalReplySent(sessionId, state.turnCount);
2195
+ }
2196
+ } else if (terminalReply) {
2197
+ if (!isFinalReplySentForTurn(state)) {
2198
+ terminalTextDelivered = await sendFinalReplyTextOnce(p, chatId, sessionId, state.turnCount, terminalReply);
2199
+ }
2198
2200
  } else if (state.accumulatedContent.trim()) {
2199
2201
  const short = truncateContent(state.accumulatedContent, 30, 4000);
2200
2202
  terminalTextDelivered = await p.sendText(chatId, `[生成过程]\n${short}`).then((ok) => ok !== false).catch(() => false);
@@ -2425,11 +2427,11 @@ export function stopSession(sessionId: string): boolean {
2425
2427
  }
2426
2428
  return false;
2427
2429
  }
2428
- prompt.stopped = true;
2429
- clearPromptResponseStallMonitor(sessionId);
2430
- clearPromptProcessMonitor(sessionId);
2431
- clearPromptAvatarRefreshTimer(sessionId);
2432
- clearPromptFinalResponseCloseTimer(sessionId);
2430
+ prompt.stopped = true;
2431
+ clearPromptResponseStallMonitor(sessionId);
2432
+ clearPromptProcessMonitor(sessionId);
2433
+ clearPromptAvatarRefreshTimer(sessionId);
2434
+ clearPromptFinalResponseCloseTimer(sessionId);
2433
2435
  cancelQueuedMessage(sessionId);
2434
2436
 
2435
2437
  // 先发起整棵进程树清理,再触发 close/abort。Windows 上 CLI 由