chatccc 0.2.94 → 0.2.95

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.
@@ -1,167 +1,167 @@
1
- /**
2
- * builtin/index.ts — ChatCCC 内置 Agent 核心 API
3
- *
4
- * ChatSession 是程序化入口,既可以被 CLI 调用,也可以被其他模块(如 ToolAdapter)调用。
5
- */
6
-
7
- import { streamText } from "ai";
8
-
9
- // ---------------------------------------------------------------------------
10
- // 系统提示词 — 编译期冻结常量
11
- // ---------------------------------------------------------------------------
12
-
13
- const SYSTEM_PROMPT = [
14
- "你是 ChatCCC 内置 AI 编程助手,运行在终端环境中。",
15
- "",
16
- "## 基本规则",
17
- "- 用中文回复,但代码、命令、文件名保持原文",
18
- "- 优先给出直接可用的方案,而非长篇解释",
19
- "- 如果用户的问题涉及代码,直接给出代码并说明用法",
20
- "- 保持简洁,一次聚焦一个问题",
21
- ].join("\n");
22
-
23
- // ---------------------------------------------------------------------------
24
- // 类型定义
25
- // ---------------------------------------------------------------------------
26
-
27
- export interface ChatSessionConfig {
28
- /** DeepSeek API 兼容的服务地址,默认 https://api.deepseek.com/v1 */
29
- baseURL?: string;
30
- /** API Key(优先用 DEEPSEEK_API_KEY 环境变量) */
31
- apiKey?: string;
32
- /** 模型名称,默认 deepseek-chat */
33
- model?: string;
34
- }
35
-
36
- export interface ChatSessionOptions {
37
- /** 会话工作目录 */
38
- cwd?: string;
39
- /** 自定义系统提示词(会拼接到默认提示词之后) */
40
- systemPrompt?: string;
41
- }
42
-
43
- /**
44
- * 流式响应事件
45
- */
46
- export type ChatEvent =
47
- | { type: "text"; text: string; accumulated: string }
48
- | { type: "done"; text: string }
49
- | { type: "error"; message: string };
50
-
51
- // ---------------------------------------------------------------------------
52
- // ChatSession
53
- // ---------------------------------------------------------------------------
54
-
55
- /** 消息角色 */
56
- type MessageRole = "system" | "user" | "assistant" | "tool";
57
-
58
- /** 内部消息类型 */
59
- interface ChatMessage {
60
- role: MessageRole;
61
- content: string;
62
- }
63
-
64
- export class ChatSession {
65
- private model: any;
66
- private messages: ChatMessage[];
67
-
68
- constructor(
69
- config: ChatSessionConfig = {},
70
- options: ChatSessionOptions = {},
71
- ) {
72
- const apiKey = config.apiKey ?? process.env.DEEPSEEK_API_KEY;
73
- if (!apiKey) {
74
- throw new Error(
75
- "DEEPSEEK_API_KEY 未设置。请设置环境变量 DEEPSEEK_API_KEY 或传入 apiKey",
76
- );
77
- }
78
-
79
- const baseURL = config.baseURL ?? "https://api.deepseek.com/v1";
80
- const modelId = config.model ?? "deepseek-chat";
81
-
82
- // 动态 import 以支持 ESM 包在 CJS 环境下的加载(tsx 处理)
83
- const { createOpenAICompatible } = require("@ai-sdk/openai-compatible");
84
- const provider = createOpenAICompatible({
85
- name: "deepseek",
86
- baseURL,
87
- apiKey,
88
- });
89
- this.model = provider(modelId);
90
-
91
- // 构建系统提示词
92
- const systemContent = [SYSTEM_PROMPT];
93
- if (options?.systemPrompt) {
94
- systemContent.push("", options.systemPrompt);
95
- }
96
- if (options?.cwd) {
97
- systemContent.push("", `当前工作目录: ${options.cwd}`);
98
- }
99
-
100
- this.messages = [{ role: "system", content: systemContent.join("\n") }];
101
- }
102
-
103
- /**
104
- * 发送用户消息,返回异步可迭代的文本流。
105
- *
106
- * 使用方式:
107
- * ```typescript
108
- * const session = new ChatSession();
109
- * for await (const event of session.chat("帮我看看 package.json")) {
110
- * if (event.type === "text") process.stdout.write(event.text);
111
- * }
112
- * console.log("完成");
113
- * ```
114
- */
115
- async *chat(
116
- userMessage: string,
117
- signal?: AbortSignal,
118
- ): AsyncIterable<ChatEvent> {
119
- this.messages.push({ role: "user", content: userMessage });
120
-
121
- let fullText = "";
122
-
123
- try {
124
- const result = streamText({
125
- model: this.model,
126
- messages: this.messages as any,
127
- abortSignal: signal,
128
- });
129
-
130
- for await (const chunk of result.textStream) {
131
- fullText += chunk;
132
- yield { type: "text", text: chunk, accumulated: fullText };
133
- }
134
-
135
- this.messages.push({ role: "assistant", content: fullText });
136
- yield { type: "done", text: fullText };
137
- } catch (err) {
138
- const message = err instanceof Error ? err.message : String(err);
139
- if ((err as Error).name === "AbortError" || signal?.aborted) {
140
- // 被中断时,不保存不完整的助手消息
141
- if (fullText) {
142
- this.messages.push({ role: "assistant", content: fullText + "\n[已中断]" });
143
- }
144
- yield { type: "done", text: fullText };
145
- return;
146
- }
147
- yield { type: "error", message };
148
- throw err;
149
- }
150
- }
151
-
152
- /** 返回当前的会话历史(只读) */
153
- get history(): ReadonlyArray<ChatMessage> {
154
- return this.messages;
155
- }
156
-
157
- /** 返回当前轮数(不含 system 消息) */
158
- get turnCount(): number {
159
- return this.messages.length - 1;
160
- }
161
-
162
- /** 清空会话历史,保留 system 消息 */
163
- reset(): void {
164
- const system = this.messages[0];
165
- this.messages = [system];
166
- }
1
+ /**
2
+ * builtin/index.ts — ChatCCC 内置 Agent 核心 API
3
+ *
4
+ * ChatSession 是程序化入口,既可以被 CLI 调用,也可以被其他模块(如 ToolAdapter)调用。
5
+ */
6
+
7
+ import { streamText } from "ai";
8
+
9
+ // ---------------------------------------------------------------------------
10
+ // 系统提示词 — 编译期冻结常量
11
+ // ---------------------------------------------------------------------------
12
+
13
+ const SYSTEM_PROMPT = [
14
+ "你是 ChatCCC 内置 AI 编程助手,运行在终端环境中。",
15
+ "",
16
+ "## 基本规则",
17
+ "- 用中文回复,但代码、命令、文件名保持原文",
18
+ "- 优先给出直接可用的方案,而非长篇解释",
19
+ "- 如果用户的问题涉及代码,直接给出代码并说明用法",
20
+ "- 保持简洁,一次聚焦一个问题",
21
+ ].join("\n");
22
+
23
+ // ---------------------------------------------------------------------------
24
+ // 类型定义
25
+ // ---------------------------------------------------------------------------
26
+
27
+ export interface ChatSessionConfig {
28
+ /** DeepSeek API 兼容的服务地址,默认 https://api.deepseek.com/v1 */
29
+ baseURL?: string;
30
+ /** API Key(优先用 DEEPSEEK_API_KEY 环境变量) */
31
+ apiKey?: string;
32
+ /** 模型名称,默认 deepseek-chat */
33
+ model?: string;
34
+ }
35
+
36
+ export interface ChatSessionOptions {
37
+ /** 会话工作目录 */
38
+ cwd?: string;
39
+ /** 自定义系统提示词(会拼接到默认提示词之后) */
40
+ systemPrompt?: string;
41
+ }
42
+
43
+ /**
44
+ * 流式响应事件
45
+ */
46
+ export type ChatEvent =
47
+ | { type: "text"; text: string; accumulated: string }
48
+ | { type: "done"; text: string }
49
+ | { type: "error"; message: string };
50
+
51
+ // ---------------------------------------------------------------------------
52
+ // ChatSession
53
+ // ---------------------------------------------------------------------------
54
+
55
+ /** 消息角色 */
56
+ type MessageRole = "system" | "user" | "assistant" | "tool";
57
+
58
+ /** 内部消息类型 */
59
+ interface ChatMessage {
60
+ role: MessageRole;
61
+ content: string;
62
+ }
63
+
64
+ export class ChatSession {
65
+ private model: any;
66
+ private messages: ChatMessage[];
67
+
68
+ constructor(
69
+ config: ChatSessionConfig = {},
70
+ options: ChatSessionOptions = {},
71
+ ) {
72
+ const apiKey = config.apiKey ?? process.env.DEEPSEEK_API_KEY;
73
+ if (!apiKey) {
74
+ throw new Error(
75
+ "DEEPSEEK_API_KEY 未设置。请设置环境变量 DEEPSEEK_API_KEY 或传入 apiKey",
76
+ );
77
+ }
78
+
79
+ const baseURL = config.baseURL ?? "https://api.deepseek.com/v1";
80
+ const modelId = config.model ?? "deepseek-chat";
81
+
82
+ // 动态 import 以支持 ESM 包在 CJS 环境下的加载(tsx 处理)
83
+ const { createOpenAICompatible } = require("@ai-sdk/openai-compatible");
84
+ const provider = createOpenAICompatible({
85
+ name: "deepseek",
86
+ baseURL,
87
+ apiKey,
88
+ });
89
+ this.model = provider(modelId);
90
+
91
+ // 构建系统提示词
92
+ const systemContent = [SYSTEM_PROMPT];
93
+ if (options?.systemPrompt) {
94
+ systemContent.push("", options.systemPrompt);
95
+ }
96
+ if (options?.cwd) {
97
+ systemContent.push("", `当前工作目录: ${options.cwd}`);
98
+ }
99
+
100
+ this.messages = [{ role: "system", content: systemContent.join("\n") }];
101
+ }
102
+
103
+ /**
104
+ * 发送用户消息,返回异步可迭代的文本流。
105
+ *
106
+ * 使用方式:
107
+ * ```typescript
108
+ * const session = new ChatSession();
109
+ * for await (const event of session.chat("帮我看看 package.json")) {
110
+ * if (event.type === "text") process.stdout.write(event.text);
111
+ * }
112
+ * console.log("完成");
113
+ * ```
114
+ */
115
+ async *chat(
116
+ userMessage: string,
117
+ signal?: AbortSignal,
118
+ ): AsyncIterable<ChatEvent> {
119
+ this.messages.push({ role: "user", content: userMessage });
120
+
121
+ let fullText = "";
122
+
123
+ try {
124
+ const result = streamText({
125
+ model: this.model,
126
+ messages: this.messages as any,
127
+ abortSignal: signal,
128
+ });
129
+
130
+ for await (const chunk of result.textStream) {
131
+ fullText += chunk;
132
+ yield { type: "text", text: chunk, accumulated: fullText };
133
+ }
134
+
135
+ this.messages.push({ role: "assistant", content: fullText });
136
+ yield { type: "done", text: fullText };
137
+ } catch (err) {
138
+ const message = err instanceof Error ? err.message : String(err);
139
+ if ((err as Error).name === "AbortError" || signal?.aborted) {
140
+ // 被中断时,不保存不完整的助手消息
141
+ if (fullText) {
142
+ this.messages.push({ role: "assistant", content: fullText + "\n[已中断]" });
143
+ }
144
+ yield { type: "done", text: fullText };
145
+ return;
146
+ }
147
+ yield { type: "error", message };
148
+ throw err;
149
+ }
150
+ }
151
+
152
+ /** 返回当前的会话历史(只读) */
153
+ get history(): ReadonlyArray<ChatMessage> {
154
+ return this.messages;
155
+ }
156
+
157
+ /** 返回当前轮数(不含 system 消息) */
158
+ get turnCount(): number {
159
+ return this.messages.length - 1;
160
+ }
161
+
162
+ /** 清空会话历史,保留 system 消息 */
163
+ reset(): void {
164
+ const system = this.messages[0];
165
+ this.messages = [system];
166
+ }
167
167
  }
@@ -65,6 +65,12 @@ export interface ActivePrompt {
65
65
  controller: AbortController;
66
66
  stopped: boolean;
67
67
  startTime: number;
68
+ /** Root PID for the CLI process currently serving this prompt, if the adapter exposes one. */
69
+ processPid?: number;
70
+ processMonitor?: ReturnType<typeof setInterval>;
71
+ /** Set when the watchdog detects that the CLI process disappeared before stream finalization. */
72
+ abnormalExit?: boolean;
73
+ abnormalExitNotified?: boolean;
68
74
  }
69
75
 
70
76
  export const activePrompts = new Map<string, ActivePrompt>();
@@ -206,6 +212,9 @@ export function consumeQueuedMessage(platform: PlatformAdapter, msg: QueuedMessa
206
212
  export function resetBindingState(): void {
207
213
  sessionChatsMap.clear();
208
214
  lastActiveChatMap.clear();
215
+ for (const prompt of activePrompts.values()) {
216
+ if (prompt.processMonitor) clearInterval(prompt.processMonitor);
217
+ }
209
218
  activePrompts.clear();
210
219
  queuedMessages.clear();
211
220
  displayCards.clear();
package/src/session.ts CHANGED
@@ -25,6 +25,7 @@ import { simplifyToolUse, simplifyToolResult } from "./simplify.ts";
25
25
  import { logTrace } from "./trace.ts";
26
26
  import type { UnifiedBlock } from "./adapters/adapter-interface.ts";
27
27
  import type { ToolAdapter } from "./adapters/adapter-interface.ts";
28
+ import type { ToolProcessInfo } from "./adapters/adapter-interface.ts";
28
29
  import { createClaudeAdapter } from "./adapters/claude-adapter.ts";
29
30
  import { createCursorAdapter } from "./adapters/cursor-adapter.ts";
30
31
  import { createCodexAdapter } from "./adapters/codex-adapter.ts";
@@ -114,6 +115,110 @@ function platformForChat(chatId: string): PlatformAdapter | null {
114
115
  return chatPlatformMap.get(chatId) ?? platformRef;
115
116
  }
116
117
 
118
+ const DEFAULT_PROCESS_MONITOR_INTERVAL_MS = 5000;
119
+ let processMonitorIntervalMs = DEFAULT_PROCESS_MONITOR_INTERVAL_MS;
120
+ let isProcessAliveImpl = (pid: number): boolean => {
121
+ try {
122
+ process.kill(pid, 0);
123
+ return true;
124
+ } catch {
125
+ return false;
126
+ }
127
+ };
128
+
129
+ export function _setProcessAliveForTest(impl: (pid: number) => boolean): void {
130
+ isProcessAliveImpl = impl;
131
+ }
132
+
133
+ export function _resetProcessAliveForTest(): void {
134
+ isProcessAliveImpl = (pid: number): boolean => {
135
+ try {
136
+ process.kill(pid, 0);
137
+ return true;
138
+ } catch {
139
+ return false;
140
+ }
141
+ };
142
+ }
143
+
144
+ export function _setProcessMonitorIntervalForTest(ms: number): void {
145
+ processMonitorIntervalMs = ms;
146
+ }
147
+
148
+ export function _resetProcessMonitorIntervalForTest(): void {
149
+ processMonitorIntervalMs = DEFAULT_PROCESS_MONITOR_INTERVAL_MS;
150
+ }
151
+
152
+ function clearPromptProcessMonitor(sessionId: string): void {
153
+ const prompt = activePrompts.get(sessionId);
154
+ if (!prompt?.processMonitor) return;
155
+ clearInterval(prompt.processMonitor);
156
+ prompt.processMonitor = undefined;
157
+ }
158
+
159
+ function formatTerminalHeader(status: "running" | "done" | "stopped" | "error"): {
160
+ title: string;
161
+ template?: string;
162
+ } {
163
+ if (status === "stopped") return { title: "已停止", template: "red" };
164
+ if (status === "error") return { title: "异常结束", template: "red" };
165
+ return { title: "完成" };
166
+ }
167
+
168
+ function turnFinalStatus(status: "running" | "done" | "stopped" | "error"): "done" | "stopped" {
169
+ return status === "stopped" || status === "error" ? "stopped" : "done";
170
+ }
171
+
172
+ function startPromptProcessMonitor(sessionId: string, info: ToolProcessInfo): void {
173
+ const prompt = activePrompts.get(sessionId);
174
+ if (!prompt) return;
175
+ prompt.processPid = info.pid;
176
+ clearPromptProcessMonitor(sessionId);
177
+
178
+ const check = async () => {
179
+ const current = activePrompts.get(sessionId);
180
+ if (!current || current !== prompt) {
181
+ clearPromptProcessMonitor(sessionId);
182
+ return;
183
+ }
184
+ if (current.stopped || current.abnormalExit) return;
185
+ if (isProcessAliveImpl(info.pid)) return;
186
+
187
+ current.abnormalExit = true;
188
+ clearPromptProcessMonitor(sessionId);
189
+
190
+ const state = await readStreamState(sessionId);
191
+ if (state?.status === "running") {
192
+ await writeStreamState({
193
+ ...state,
194
+ status: "error",
195
+ updatedAt: Date.now(),
196
+ });
197
+ }
198
+
199
+ const chatId = pickDisplayChat(sessionId) ?? getLastActiveChat(sessionId) ?? getChatsForSession(sessionId)[0];
200
+ const p = chatId ? platformForChat(chatId) : null;
201
+ if (chatId && p && !current.abnormalExitNotified) {
202
+ current.abnormalExitNotified = true;
203
+ await p.sendText(
204
+ chatId,
205
+ `⚠️ 进程异常结束:session ${sessionId} 对应的 CLI 进程 PID ${info.pid} 已不存在,已按完成处理。若回复不完整,请重新发送上一条指令。`,
206
+ ).catch(() => {});
207
+ }
208
+
209
+ // 主动关闭 readline,让 runAgentSession 的 finally 落盘 error 终态并清理 activePrompts。
210
+ current.controller.abort();
211
+ };
212
+
213
+ const handle = setInterval(() => {
214
+ void check().catch((err) => {
215
+ console.warn(`[${ts()}] [PROCESS-MONITOR] check failed for ${sessionId}: ${(err as Error).message}`);
216
+ });
217
+ }, processMonitorIntervalMs);
218
+ handle.unref?.();
219
+ prompt.processMonitor = handle;
220
+ }
221
+
117
222
  export function _getPlatformForChatForTest(chatId: string): PlatformAdapter | null {
118
223
  return platformForChat(chatId);
119
224
  }
@@ -189,6 +294,9 @@ export function resetState(): void {
189
294
  processedMessages.clear();
190
295
  lastMsgTimestamps.clear();
191
296
  chatPlatformMap.clear();
297
+ for (const prompt of activePrompts.values()) {
298
+ if (prompt.processMonitor) clearInterval(prompt.processMonitor);
299
+ }
192
300
  activePrompts.clear();
193
301
  displayCards.clear();
194
302
  stopUnifiedDisplayLoop();
@@ -833,13 +941,12 @@ export async function runAgentSession(
833
941
  // terminal state,发送了 finalReply 并删除/替换了 displayCards 条目。
834
942
  // 通过引用比较检测——若统一 loop 已处理则只补持久化和头像,不重复发。
835
943
  if (displayCards.get(displayChatId) !== display) {
836
- const finalStatus = prevState.status === "stopped" ? "stopped" : "done";
944
+ const finalStatus = turnFinalStatus(prevState.status);
837
945
  finalizeTurnCards(sessionId, prevState.turnCount, finalStatus).catch(() => {});
838
946
  pp.setChatAvatar(displayChatId, prevState.tool, "idle").catch(() => {});
839
947
  } else {
840
948
  const nextSeq = display.sequence + 1;
841
- const headerTitle = prevState.status === "stopped" ? "已停止" : "完成";
842
- const headerTemplate = prevState.status === "stopped" ? "red" : undefined;
949
+ const { title: headerTitle, template: headerTemplate } = formatTerminalHeader(prevState.status);
843
950
  const cardContent = truncateContent(prevState.accumulatedContent + prevState.finalReply) || " ";
844
951
  const doneCard = buildProgressCard(cardContent, { showStop: false, headerTitle, headerTemplate });
845
952
  await pp.cardUpdate(display.cardId, doneCard, nextSeq).catch(() => {});
@@ -848,7 +955,7 @@ export async function runAgentSession(
848
955
  displayCards.delete(displayChatId);
849
956
 
850
957
  // 持久化:标记上一轮所有卡片为终态
851
- const finalStatus = prevState.status === "stopped" ? "stopped" : "done";
958
+ const finalStatus = turnFinalStatus(prevState.status);
852
959
  finalizeTurnCards(sessionId, prevState.turnCount, finalStatus).catch(() => {});
853
960
 
854
961
  if (prevState.finalReply && stillOursAfterUpdate && !isFinalReplySentForTurn(prevState)) {
@@ -858,7 +965,7 @@ export async function runAgentSession(
858
965
  }
859
966
  } else if (pp && prevState.finalReply && !isFinalReplySentForTurn(prevState)) {
860
967
  // 无 display 记录但上一轮有 finalReply(极快轮次),至少发送
861
- const finalStatus = prevState.status === "stopped" ? "stopped" : "done";
968
+ const finalStatus = turnFinalStatus(prevState.status);
862
969
  finalizeTurnCards(sessionId, prevState.turnCount, finalStatus).catch(() => {});
863
970
  await sendFinalReplyTextOnce(pp, displayChatId, sessionId, prevState.turnCount, prevState.finalReply);
864
971
  }
@@ -928,7 +1035,14 @@ export async function runAgentSession(
928
1035
  const toolCallMap = new Map<string, { name: string; input: unknown }>();
929
1036
 
930
1037
  try {
931
- for await (const unifiedMsg of adapter.prompt(sessionId, userTextWithCapabilities, cwd, controller.signal)) {
1038
+ for await (const unifiedMsg of adapter.prompt(sessionId, userTextWithCapabilities, cwd, controller.signal, {
1039
+ onProcessStart: (processInfo) => {
1040
+ startPromptProcessMonitor(sessionId, processInfo);
1041
+ },
1042
+ onProcessExit: () => {
1043
+ clearPromptProcessMonitor(sessionId);
1044
+ },
1045
+ })) {
932
1046
  for (const block of unifiedMsg.blocks) {
933
1047
  accumulateBlockContent(block, state, toolCallMap);
934
1048
 
@@ -971,13 +1085,15 @@ export async function runAgentSession(
971
1085
  // 标记 prompt 结束
972
1086
  const prompt = activePrompts.get(sessionId);
973
1087
  const wasStopped = prompt?.stopped ?? false;
1088
+ const wasAbnormalExit = prompt?.abnormalExit ?? false;
1089
+ clearPromptProcessMonitor(sessionId);
974
1090
  activePrompts.delete(sessionId);
975
1091
 
976
1092
  // 先写最终状态(done/stopped),确保 display loop 在下一轮消费前
977
1093
  // 读到新状态并终结旧卡片。否则 setImmediate 在 CHECK 阶段先于
978
1094
  // writeFile I/O(POLL 阶段)执行,display loop 会误以为旧轮仍在
979
1095
  // 运行中并更新旧卡片,而不是新建卡片。
980
- const finalStatus = wasStopped ? "stopped" : "done";
1096
+ const finalStatus = wasAbnormalExit ? "error" : wasStopped ? "stopped" : "done";
981
1097
  const finalReply = pickFinalReply(state).trim();
982
1098
  await writeStreamState({
983
1099
  sessionId,
@@ -1039,6 +1155,23 @@ export async function runAgentSession(
1039
1155
  if (active1) platform.setChatAvatar(active1, tool, "idle").catch(() => {});
1040
1156
  console.log(`[${ts()}] Session ${sessionId} stopped (content chunks: ${state.chunkCount})`);
1041
1157
  if (tid) logTrace(tid, "SESSION_END", { sessionId, outcome: "stopped", chunks: state.chunkCount });
1158
+ } else if (wasAbnormalExit) {
1159
+ for (const cid of getChatsForSession(sessionId)) {
1160
+ const finfo = sessionInfoMap.get(cid);
1161
+ await recordSessionRegistry({
1162
+ chatId: cid,
1163
+ sessionId,
1164
+ tool,
1165
+ turnCount: finfo?.turnCount ?? nextTurnCount,
1166
+ lastContextTokens: finfo?.lastContextTokens ?? nextContextTokens,
1167
+ startTime: finfo?.startTime ?? now,
1168
+ running: false,
1169
+ });
1170
+ }
1171
+ const activeErr = getLastActiveChat(sessionId) ?? getChatsForSession(sessionId)[0];
1172
+ if (activeErr) platform.setChatAvatar(activeErr, tool, "idle").catch(() => {});
1173
+ console.log(`[${ts()}] Session ${sessionId} process exited unexpectedly (content chunks: ${state.chunkCount})`);
1174
+ if (tid) logTrace(tid, "SESSION_END", { sessionId, outcome: "process_missing", chunks: state.chunkCount });
1042
1175
  } else {
1043
1176
  for (const cid of getChatsForSession(sessionId)) {
1044
1177
  const finfo = sessionInfoMap.get(cid);
@@ -1140,8 +1273,7 @@ export function startUnifiedDisplayLoop(): void {
1140
1273
  // 发送最终结果(卡片平台)
1141
1274
  while (display.cardBusy) await new Promise(r => setTimeout(r, 20));
1142
1275
  const nextSeq = display.sequence + 1;
1143
- const headerTitle = state.status === "stopped" ? "已停止" : "完成";
1144
- const headerTemplate = state.status === "stopped" ? "red" : undefined;
1276
+ const { title: headerTitle, template: headerTemplate } = formatTerminalHeader(state.status);
1145
1277
  const cardContent = truncateContent(state.accumulatedContent + state.finalReply) || " ";
1146
1278
  const doneCard = buildProgressCard(cardContent, { showStop: false, headerTitle, headerTemplate });
1147
1279
  await p.cardUpdate(display.cardId, doneCard, nextSeq).catch(() => {});
@@ -1156,7 +1288,7 @@ export function startUnifiedDisplayLoop(): void {
1156
1288
  continue;
1157
1289
  }
1158
1290
 
1159
- const finalSt = state.status === "stopped" ? "stopped" : "done";
1291
+ const finalSt = turnFinalStatus(state.status);
1160
1292
  finalizeTurnCards(sessionId, state.turnCount, finalSt).catch(() => {});
1161
1293
  displayCards.delete(chatId);
1162
1294
  if (state.finalReply) {
@@ -1343,6 +1475,7 @@ export function stopSession(sessionId: string): boolean {
1343
1475
  const prompt = activePrompts.get(sessionId);
1344
1476
  if (!prompt) return false;
1345
1477
  prompt.stopped = true;
1478
+ clearPromptProcessMonitor(sessionId);
1346
1479
  cancelQueuedMessage(sessionId);
1347
1480
  prompt.controller.abort();
1348
1481
  console.log(`[${ts()}] [STOP] Session ${sessionId} aborted`);
@@ -1434,7 +1567,8 @@ export async function getSessionStatus(chatId: string): Promise<SessionStatus |
1434
1567
  const info = sessionInfoMap.get(chatId);
1435
1568
  if (!info) return null;
1436
1569
 
1437
- const isActive = activePrompts.has(info.sessionId) && !(activePrompts.get(info.sessionId)?.stopped);
1570
+ const activePrompt = activePrompts.get(info.sessionId);
1571
+ const isActive = !!activePrompt && !activePrompt.stopped && !activePrompt.abnormalExit;
1438
1572
  const { model, effort } = await resolveModelEffort(info.tool, info.sessionId);
1439
1573
 
1440
1574
  const registry = await loadSessionRegistry();
@@ -1509,7 +1643,9 @@ export async function getAllSessionsStatus(): Promise<SessionsListEntry[]> {
1509
1643
  chatId: info.chatId,
1510
1644
  sessionId: info.sessionId,
1511
1645
  chatName: info.chatName || "",
1512
- active: activePrompts.has(info.sessionId) && !(activePrompts.get(info.sessionId)?.stopped),
1646
+ active: !!activePrompts.get(info.sessionId) &&
1647
+ !activePrompts.get(info.sessionId)?.stopped &&
1648
+ !activePrompts.get(info.sessionId)?.abnormalExit,
1513
1649
  turnCount: info.turnCount,
1514
1650
  startTime: info.startTime,
1515
1651
  model,