chatccc 0.2.204 → 0.2.206
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/package.json +1 -1
- package/src/__tests__/agent-activity.test.ts +76 -0
- package/src/__tests__/codex-adapter.test.ts +7 -7
- package/src/__tests__/cursor-adapter.test.ts +5 -5
- package/src/__tests__/response-stall.test.ts +49 -0
- package/src/__tests__/session.test.ts +277 -33
- package/src/adapters/codex-adapter.ts +1 -0
- package/src/adapters/cursor-adapter.ts +1 -0
- package/src/agent-activity.ts +170 -0
- package/src/response-stall.ts +28 -0
- package/src/session-chat-binding.ts +20 -10
- package/src/session.ts +317 -121
- package/src/stream-state.ts +18 -10
package/src/session.ts
CHANGED
|
@@ -21,7 +21,12 @@ import {
|
|
|
21
21
|
toolDisplayName,
|
|
22
22
|
ts,
|
|
23
23
|
} from "./config.ts";
|
|
24
|
-
import { buildProgressCard, getToolEmoji, isCodeBlockOpen, truncateContent } from "./cards.ts";
|
|
24
|
+
import { buildProgressCard, getToolEmoji, isCodeBlockOpen, truncateContent } from "./cards.ts";
|
|
25
|
+
import {
|
|
26
|
+
createAgentActivityTracker,
|
|
27
|
+
formatAgentActivityTitle,
|
|
28
|
+
updateAgentActivity,
|
|
29
|
+
} from "./agent-activity.ts";
|
|
25
30
|
import { simplifyToolUse, simplifyToolResult } from "./simplify.ts";
|
|
26
31
|
import { logTrace } from "./trace.ts";
|
|
27
32
|
import type { UnifiedBlock } from "./adapters/adapter-interface.ts";
|
|
@@ -31,9 +36,11 @@ import { createClaudeAdapter } from "./adapters/claude-adapter.ts";
|
|
|
31
36
|
import { createCursorAdapter } from "./adapters/cursor-adapter.ts";
|
|
32
37
|
import { createCodexAdapter } from "./adapters/codex-adapter.ts";
|
|
33
38
|
import { createCccAdapter } from "./adapters/ccc-adapter.ts";
|
|
34
|
-
import {
|
|
35
|
-
import {
|
|
36
|
-
import
|
|
39
|
+
import { killProcessTree } from "./adapters/proc-tree-kill.ts";
|
|
40
|
+
import { resourceMonitor, registerProcess, unregisterProcess } from "./adapters/resource-monitor.ts";
|
|
41
|
+
import { buildImSkillsPromptCached, exportSkillSubDocs, clearImSkillsPromptCache } from "./im-skills.ts";
|
|
42
|
+
import type { PlatformAdapter } from "./platform-adapter.ts";
|
|
43
|
+
import { hasResponseStalled, observeResponseProgress } from "./response-stall.ts";
|
|
37
44
|
|
|
38
45
|
// 微信显示循环压缩:头5 + ... + 尾5,避免在最后一步 sendText 中压缩指令回复
|
|
39
46
|
function compressWechatDisplayText(text: string): string {
|
|
@@ -82,18 +89,19 @@ async function sendFinalReplyTextOnce(
|
|
|
82
89
|
return sent;
|
|
83
90
|
}
|
|
84
91
|
|
|
85
|
-
async function createVisibleProgressCard(
|
|
92
|
+
async function createVisibleProgressCard(
|
|
86
93
|
platform: PlatformAdapter,
|
|
87
94
|
chatId: string,
|
|
88
95
|
sessionId: string,
|
|
89
|
-
turnCount: number,
|
|
90
|
-
notifyFailureText?: string,
|
|
91
|
-
|
|
96
|
+
turnCount: number,
|
|
97
|
+
notifyFailureText?: string,
|
|
98
|
+
headerTitle = "正在启动 Agent · 0秒",
|
|
99
|
+
): Promise<string | null> {
|
|
92
100
|
for (let attempt = 1; attempt <= 2; attempt++) {
|
|
93
101
|
let cardId: string | null = null;
|
|
94
102
|
try {
|
|
95
|
-
cardId = await platform.cardCreate(
|
|
96
|
-
buildProgressCard("", { showStop: true, headerTitle
|
|
103
|
+
cardId = await platform.cardCreate(
|
|
104
|
+
buildProgressCard("等待 Agent 输出...", { showStop: true, headerTitle }),
|
|
97
105
|
);
|
|
98
106
|
if (!cardId) throw new Error("empty card id");
|
|
99
107
|
await platform.cardSend(chatId, cardId);
|
|
@@ -148,8 +156,12 @@ function platformForChat(chatId: string): PlatformAdapter | null {
|
|
|
148
156
|
return chatPlatformMap.get(chatId) ?? platformRef;
|
|
149
157
|
}
|
|
150
158
|
|
|
151
|
-
const DEFAULT_PROCESS_MONITOR_INTERVAL_MS = 5000;
|
|
152
|
-
|
|
159
|
+
const DEFAULT_PROCESS_MONITOR_INTERVAL_MS = 5000;
|
|
160
|
+
const DEFAULT_RESPONSE_STALL_TIMEOUT_MS = 3 * 60 * 1000;
|
|
161
|
+
const DEFAULT_RESPONSE_STALL_CHECK_INTERVAL_MS = 5000;
|
|
162
|
+
let processMonitorIntervalMs = DEFAULT_PROCESS_MONITOR_INTERVAL_MS;
|
|
163
|
+
let responseStallTimeoutMs = DEFAULT_RESPONSE_STALL_TIMEOUT_MS;
|
|
164
|
+
let responseStallCheckIntervalMs = DEFAULT_RESPONSE_STALL_CHECK_INTERVAL_MS;
|
|
153
165
|
let isProcessAliveImpl = (pid: number): boolean => {
|
|
154
166
|
try {
|
|
155
167
|
process.kill(pid, 0);
|
|
@@ -178,29 +190,68 @@ export function _setProcessMonitorIntervalForTest(ms: number): void {
|
|
|
178
190
|
processMonitorIntervalMs = ms;
|
|
179
191
|
}
|
|
180
192
|
|
|
181
|
-
export function _resetProcessMonitorIntervalForTest(): void {
|
|
182
|
-
processMonitorIntervalMs = DEFAULT_PROCESS_MONITOR_INTERVAL_MS;
|
|
183
|
-
}
|
|
193
|
+
export function _resetProcessMonitorIntervalForTest(): void {
|
|
194
|
+
processMonitorIntervalMs = DEFAULT_PROCESS_MONITOR_INTERVAL_MS;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function _setResponseStallTimeoutForTest(ms: number): void {
|
|
198
|
+
responseStallTimeoutMs = ms;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function _resetResponseStallTimeoutForTest(): void {
|
|
202
|
+
responseStallTimeoutMs = DEFAULT_RESPONSE_STALL_TIMEOUT_MS;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function _setResponseStallCheckIntervalForTest(ms: number): void {
|
|
206
|
+
responseStallCheckIntervalMs = ms;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export function _resetResponseStallCheckIntervalForTest(): void {
|
|
210
|
+
responseStallCheckIntervalMs = DEFAULT_RESPONSE_STALL_CHECK_INTERVAL_MS;
|
|
211
|
+
}
|
|
184
212
|
|
|
185
|
-
function clearPromptProcessMonitor(sessionId: string): void {
|
|
213
|
+
function clearPromptProcessMonitor(sessionId: string): void {
|
|
186
214
|
const prompt = activePrompts.get(sessionId);
|
|
187
215
|
if (!prompt?.processMonitor) return;
|
|
188
216
|
clearInterval(prompt.processMonitor);
|
|
189
217
|
prompt.processMonitor = undefined;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
function
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function clearPromptResponseStallMonitor(sessionId: string): void {
|
|
221
|
+
const prompt = activePrompts.get(sessionId);
|
|
222
|
+
if (!prompt?.responseStallMonitor) return;
|
|
223
|
+
clearInterval(prompt.responseStallMonitor);
|
|
224
|
+
prompt.responseStallMonitor = undefined;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function formatTerminalHeader(status: "running" | "done" | "stopped" | "error" | "auto_ended"): {
|
|
228
|
+
title: string;
|
|
229
|
+
template?: string;
|
|
230
|
+
} {
|
|
231
|
+
if (status === "auto_ended") return { title: "已自动结束 · 3分钟无新内容", template: "orange" };
|
|
232
|
+
if (status === "stopped") return { title: "已停止", template: "red" };
|
|
233
|
+
if (status === "error") return { title: "异常结束", template: "red" };
|
|
234
|
+
return { title: "完成" };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function turnFinalStatus(status: "running" | "done" | "stopped" | "error" | "auto_ended"): "done" | "stopped" {
|
|
238
|
+
return status === "stopped" || status === "error" || status === "auto_ended" ? "stopped" : "done";
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function formatAutoEndedReply(finalReply: string): string {
|
|
242
|
+
const reason = "⚠️ 已自动结束:连续 3 分钟处于“正在生成回复”且回复字符总数没有变化。";
|
|
243
|
+
return finalReply
|
|
244
|
+
? `${reason}以下回复可能不完整。\n\n${finalReply}`
|
|
245
|
+
: `${reason}本轮没有可发送的回复内容。`;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function formatTerminalReply(
|
|
249
|
+
status: "running" | "done" | "stopped" | "error" | "auto_ended",
|
|
250
|
+
finalReply: string,
|
|
251
|
+
): string | null {
|
|
252
|
+
if (status === "auto_ended") return formatAutoEndedReply(finalReply);
|
|
253
|
+
return finalReply || null;
|
|
254
|
+
}
|
|
204
255
|
|
|
205
256
|
function isCardKitSequenceConflict(err: unknown): boolean {
|
|
206
257
|
return err instanceof Error && err.message.includes("300317");
|
|
@@ -218,7 +269,7 @@ function startPromptProcessMonitor(sessionId: string, info: ToolProcessInfo): vo
|
|
|
218
269
|
clearPromptProcessMonitor(sessionId);
|
|
219
270
|
return;
|
|
220
271
|
}
|
|
221
|
-
if (current.stopped || current.abnormalExit || current.resourceStuck) return;
|
|
272
|
+
if (current.stopped || current.abnormalExit || current.resourceStuck || current.autoEnded) return;
|
|
222
273
|
if (isProcessAliveImpl(info.pid)) return;
|
|
223
274
|
|
|
224
275
|
current.abnormalExit = true;
|
|
@@ -331,9 +382,10 @@ export function resetState(): void {
|
|
|
331
382
|
processedMessages.clear();
|
|
332
383
|
lastMsgTimestamps.clear();
|
|
333
384
|
chatPlatformMap.clear();
|
|
334
|
-
for (const prompt of activePrompts.values()) {
|
|
335
|
-
if (prompt.processMonitor) clearInterval(prompt.processMonitor);
|
|
336
|
-
|
|
385
|
+
for (const prompt of activePrompts.values()) {
|
|
386
|
+
if (prompt.processMonitor) clearInterval(prompt.processMonitor);
|
|
387
|
+
if (prompt.responseStallMonitor) clearInterval(prompt.responseStallMonitor);
|
|
388
|
+
}
|
|
337
389
|
activePrompts.clear();
|
|
338
390
|
displayCards.clear();
|
|
339
391
|
sessionModelOverrides.clear();
|
|
@@ -958,7 +1010,7 @@ export async function runAgentSession(
|
|
|
958
1010
|
const onResourceStuck = (data: { pid: number; sessionId: string; idleMinutes: number }) => {
|
|
959
1011
|
if (data.sessionId !== sessionId) return;
|
|
960
1012
|
const prompt = activePrompts.get(sessionId);
|
|
961
|
-
if (!prompt || prompt.stopped || prompt.abnormalExit || prompt.resourceStuck) return;
|
|
1013
|
+
if (!prompt || prompt.stopped || prompt.abnormalExit || prompt.resourceStuck || prompt.autoEnded) return;
|
|
962
1014
|
prompt.resourceStuck = true;
|
|
963
1015
|
|
|
964
1016
|
const chatId = pickDisplayChat(sessionId) ?? getLastActiveChat(sessionId) ?? getChatsForSession(sessionId)[0];
|
|
@@ -1065,9 +1117,10 @@ export async function runAgentSession(
|
|
|
1065
1117
|
// 导致 finalReply 丢失、完成卡片空白。此处主动读取上一轮终端状态完成
|
|
1066
1118
|
// 卡片终结和回复发送,不依赖 display loop 时序,保证"先发完上一个回答
|
|
1067
1119
|
// 再开始缓存问题对应的任务"。
|
|
1068
|
-
const prevState = await readStreamState(sessionId);
|
|
1069
|
-
if (prevState && prevState.status !== "running") {
|
|
1070
|
-
const
|
|
1120
|
+
const prevState = await readStreamState(sessionId);
|
|
1121
|
+
if (prevState && prevState.status !== "running") {
|
|
1122
|
+
const prevTerminalReply = formatTerminalReply(prevState.status, prevState.finalReply);
|
|
1123
|
+
const displayChatId = pickDisplayChat(sessionId);
|
|
1071
1124
|
if (displayChatId) {
|
|
1072
1125
|
const pp = platformForChat(displayChatId);
|
|
1073
1126
|
const display = displayCards.get(displayChatId);
|
|
@@ -1099,45 +1152,49 @@ export async function runAgentSession(
|
|
|
1099
1152
|
const finalStatus = turnFinalStatus(prevState.status);
|
|
1100
1153
|
finalizeTurnCards(sessionId, prevState.turnCount, finalStatus).catch(() => {});
|
|
1101
1154
|
|
|
1102
|
-
if (
|
|
1103
|
-
await sendFinalReplyTextOnce(pp, displayChatId, sessionId, prevState.turnCount,
|
|
1104
|
-
}
|
|
1155
|
+
if (prevTerminalReply && stillOursAfterUpdate && !isFinalReplySentForTurn(prevState)) {
|
|
1156
|
+
await sendFinalReplyTextOnce(pp, displayChatId, sessionId, prevState.turnCount, prevTerminalReply);
|
|
1157
|
+
}
|
|
1105
1158
|
pp.setChatAvatar(displayChatId, prevState.tool, "idle").catch(() => {});
|
|
1106
1159
|
}
|
|
1107
|
-
} else if (pp &&
|
|
1160
|
+
} else if (pp && prevTerminalReply && !isFinalReplySentForTurn(prevState)) {
|
|
1108
1161
|
// 无 display 记录但上一轮有 finalReply(极快轮次),至少发送
|
|
1109
1162
|
const finalStatus = turnFinalStatus(prevState.status);
|
|
1110
1163
|
finalizeTurnCards(sessionId, prevState.turnCount, finalStatus).catch(() => {});
|
|
1111
|
-
await sendFinalReplyTextOnce(pp, displayChatId, sessionId, prevState.turnCount,
|
|
1164
|
+
await sendFinalReplyTextOnce(pp, displayChatId, sessionId, prevState.turnCount, prevTerminalReply);
|
|
1112
1165
|
}
|
|
1113
1166
|
// else: displayCards 无记录且无 finalReply → 无需处理
|
|
1114
1167
|
}
|
|
1115
1168
|
}
|
|
1116
1169
|
|
|
1117
1170
|
// 初始化 stream-state.json
|
|
1118
|
-
const initialState = createEmptyStreamState(sessionId, cwd, tool, nextTurnCount);
|
|
1119
|
-
|
|
1171
|
+
const initialState = createEmptyStreamState(sessionId, cwd, tool, nextTurnCount);
|
|
1172
|
+
const activityTracker = createAgentActivityTracker(initialState.activity?.startedAt ?? Date.now());
|
|
1173
|
+
await writeStreamState(initialState);
|
|
1120
1174
|
|
|
1121
1175
|
// 为新 turn 创建第一张展示卡片,同时注册到 turn-cards 持久化。
|
|
1122
1176
|
// 统一 display loop 始终运行,卡片创建后下一个 tick 即自动开始更新。
|
|
1123
1177
|
const displayChatIdForNew = pickDisplayChat(sessionId);
|
|
1124
|
-
if (displayChatIdForNew) {
|
|
1125
|
-
const ppNew = platformForChat(displayChatIdForNew);
|
|
1126
|
-
if (ppNew && ppNew.kind !== "wechat") {
|
|
1127
|
-
const
|
|
1178
|
+
if (displayChatIdForNew) {
|
|
1179
|
+
const ppNew = platformForChat(displayChatIdForNew);
|
|
1180
|
+
if (ppNew && ppNew.kind !== "wechat") {
|
|
1181
|
+
const initialHeaderTitle = formatAgentActivityTitle(activityTracker.activity);
|
|
1182
|
+
const cardId = await createVisibleProgressCard(
|
|
1128
1183
|
ppNew,
|
|
1129
1184
|
displayChatIdForNew,
|
|
1130
1185
|
sessionId,
|
|
1131
|
-
nextTurnCount,
|
|
1132
|
-
"生成中卡片发送失败,结果将以文本形式发送。",
|
|
1186
|
+
nextTurnCount,
|
|
1187
|
+
"生成中卡片发送失败,结果将以文本形式发送。",
|
|
1188
|
+
initialHeaderTitle,
|
|
1133
1189
|
);
|
|
1134
1190
|
if (cardId) {
|
|
1135
1191
|
displayCards.set(displayChatIdForNew, {
|
|
1136
1192
|
cardId,
|
|
1137
1193
|
sequence: 1,
|
|
1138
1194
|
cardBusy: false,
|
|
1139
|
-
cardCreatedAt: Date.now(),
|
|
1140
|
-
lastSentContent: "",
|
|
1195
|
+
cardCreatedAt: Date.now(),
|
|
1196
|
+
lastSentContent: "",
|
|
1197
|
+
lastSentHeaderTitle: initialHeaderTitle,
|
|
1141
1198
|
streamErrorNotified: false,
|
|
1142
1199
|
sessionId,
|
|
1143
1200
|
turnCount: nextTurnCount,
|
|
@@ -1178,8 +1235,71 @@ export async function runAgentSession(
|
|
|
1178
1235
|
const toolCallMap = new Map<string, { name: string; input: unknown }>();
|
|
1179
1236
|
let streamErrored = false;
|
|
1180
1237
|
|
|
1238
|
+
const runningPrompt = activePrompts.get(sessionId);
|
|
1239
|
+
if (runningPrompt) {
|
|
1240
|
+
const checkResponseStall = async () => {
|
|
1241
|
+
const current = activePrompts.get(sessionId);
|
|
1242
|
+
if (!current || current !== runningPrompt) {
|
|
1243
|
+
clearPromptResponseStallMonitor(sessionId);
|
|
1244
|
+
return;
|
|
1245
|
+
}
|
|
1246
|
+
if (
|
|
1247
|
+
current.stopped
|
|
1248
|
+
|| current.abnormalExit
|
|
1249
|
+
|| current.resourceStuck
|
|
1250
|
+
|| current.autoEnded
|
|
1251
|
+
|| activityTracker.activity.kind !== "responding"
|
|
1252
|
+
|| !hasResponseStalled(current.responseProgress, Date.now(), responseStallTimeoutMs)
|
|
1253
|
+
) {
|
|
1254
|
+
return;
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
const autoEndedAt = Date.now();
|
|
1258
|
+
current.autoEnded = true;
|
|
1259
|
+
current.autoEndedAt = autoEndedAt;
|
|
1260
|
+
clearPromptResponseStallMonitor(sessionId);
|
|
1261
|
+
clearPromptProcessMonitor(sessionId);
|
|
1262
|
+
|
|
1263
|
+
// First publish an atomic terminal state so the card cannot keep claiming the
|
|
1264
|
+
// Agent is running while process cleanup is underway.
|
|
1265
|
+
await writeStreamState({
|
|
1266
|
+
sessionId,
|
|
1267
|
+
status: "auto_ended",
|
|
1268
|
+
accumulatedContent: state.accumulatedContent,
|
|
1269
|
+
finalReply: pickFinalReply(state).trim(),
|
|
1270
|
+
activity: activityTracker.activity,
|
|
1271
|
+
chunkCount: state.chunkCount,
|
|
1272
|
+
turnCount: nextTurnCount,
|
|
1273
|
+
contextTokens: existingInfo?.lastContextTokens ?? 0,
|
|
1274
|
+
updatedAt: autoEndedAt,
|
|
1275
|
+
cwd,
|
|
1276
|
+
tool,
|
|
1277
|
+
autoEndedAt,
|
|
1278
|
+
});
|
|
1279
|
+
|
|
1280
|
+
try {
|
|
1281
|
+
current.closeSession?.();
|
|
1282
|
+
} catch (err) {
|
|
1283
|
+
console.warn(`[${ts()}] [RESPONSE-STALL] closeSession failed for ${sessionId}: ${(err as Error).message}`);
|
|
1284
|
+
}
|
|
1285
|
+
current.controller.abort();
|
|
1286
|
+
await killProcessTree(current.processPid);
|
|
1287
|
+
console.warn(
|
|
1288
|
+
`[${ts()}] [RESPONSE-STALL] Session ${sessionId} auto-ended after 3 minutes without reply character changes`,
|
|
1289
|
+
);
|
|
1290
|
+
};
|
|
1291
|
+
|
|
1292
|
+
const responseStallMonitor = setInterval(() => {
|
|
1293
|
+
void checkResponseStall().catch((err) => {
|
|
1294
|
+
console.warn(`[${ts()}] [RESPONSE-STALL] check failed for ${sessionId}: ${(err as Error).message}`);
|
|
1295
|
+
});
|
|
1296
|
+
}, responseStallCheckIntervalMs);
|
|
1297
|
+
responseStallMonitor.unref?.();
|
|
1298
|
+
runningPrompt.responseStallMonitor = responseStallMonitor;
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1181
1301
|
try {
|
|
1182
|
-
for await (const unifiedMsg of adapter.prompt(sessionId, userTextWithCapabilities, cwd, controller.signal, {
|
|
1302
|
+
for await (const unifiedMsg of adapter.prompt(sessionId, userTextWithCapabilities, cwd, controller.signal, {
|
|
1183
1303
|
onProcessStart: (processInfo) => {
|
|
1184
1304
|
startPromptProcessMonitor(sessionId, processInfo);
|
|
1185
1305
|
if (processInfo.pid !== undefined) registerProcess(processInfo.pid, sessionId);
|
|
@@ -1193,8 +1313,10 @@ export async function runAgentSession(
|
|
|
1193
1313
|
if (prompt) prompt.closeSession = closeSession;
|
|
1194
1314
|
},
|
|
1195
1315
|
})) {
|
|
1196
|
-
|
|
1197
|
-
|
|
1316
|
+
let activityChanged = false;
|
|
1317
|
+
for (const block of unifiedMsg.blocks) {
|
|
1318
|
+
if (updateAgentActivity(activityTracker, block)) activityChanged = true;
|
|
1319
|
+
accumulateBlockContent(block, state, toolCallMap);
|
|
1198
1320
|
|
|
1199
1321
|
if (block.type === "compact_boundary" && block.post_tokens) {
|
|
1200
1322
|
for (const cid of getChatsForSession(sessionId)) {
|
|
@@ -1208,18 +1330,30 @@ export async function runAgentSession(
|
|
|
1208
1330
|
lastContextTokens: block.post_tokens,
|
|
1209
1331
|
running: true,
|
|
1210
1332
|
});
|
|
1211
|
-
}
|
|
1212
|
-
}
|
|
1213
|
-
|
|
1214
|
-
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
const prompt = activePrompts.get(sessionId);
|
|
1337
|
+
if (prompt && !prompt.autoEnded) {
|
|
1338
|
+
const totalChars = state.accumulatedContent.length + pickFinalReply(state).length;
|
|
1339
|
+
prompt.responseProgress = observeResponseProgress(
|
|
1340
|
+
prompt.responseProgress,
|
|
1341
|
+
activityTracker.activity.kind === "responding",
|
|
1342
|
+
totalChars,
|
|
1343
|
+
Date.now(),
|
|
1344
|
+
);
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
// 定时写入文件
|
|
1215
1348
|
const now2 = Date.now();
|
|
1216
|
-
if (now2 - lastFileWrite >= FILE_WRITE_INTERVAL_MS) {
|
|
1217
|
-
lastFileWrite = now2;
|
|
1218
|
-
await writeStreamState({
|
|
1349
|
+
if (activityChanged || now2 - lastFileWrite >= FILE_WRITE_INTERVAL_MS) {
|
|
1350
|
+
lastFileWrite = now2;
|
|
1351
|
+
await writeStreamState({
|
|
1219
1352
|
sessionId,
|
|
1220
1353
|
status: "running",
|
|
1221
|
-
accumulatedContent: state.accumulatedContent,
|
|
1222
|
-
finalReply: pickFinalReply(state),
|
|
1354
|
+
accumulatedContent: state.accumulatedContent,
|
|
1355
|
+
finalReply: pickFinalReply(state),
|
|
1356
|
+
activity: activityTracker.activity,
|
|
1223
1357
|
chunkCount: state.chunkCount,
|
|
1224
1358
|
turnCount: nextTurnCount,
|
|
1225
1359
|
contextTokens: existingInfo?.lastContextTokens ?? 0,
|
|
@@ -1236,17 +1370,26 @@ export async function runAgentSession(
|
|
|
1236
1370
|
// 标记 prompt 结束
|
|
1237
1371
|
resourceMonitor.off("stuck", onResourceStuck);
|
|
1238
1372
|
const prompt = activePrompts.get(sessionId);
|
|
1239
|
-
const wasStopped = prompt?.stopped ?? false;
|
|
1240
|
-
const wasAbnormalExit = prompt?.abnormalExit ?? false;
|
|
1241
|
-
const wasResourceStuck = prompt?.resourceStuck ?? false;
|
|
1242
|
-
|
|
1243
|
-
|
|
1373
|
+
const wasStopped = prompt?.stopped ?? false;
|
|
1374
|
+
const wasAbnormalExit = prompt?.abnormalExit ?? false;
|
|
1375
|
+
const wasResourceStuck = prompt?.resourceStuck ?? false;
|
|
1376
|
+
const wasAutoEnded = prompt?.autoEnded ?? false;
|
|
1377
|
+
const autoEndedAt = prompt?.autoEndedAt;
|
|
1378
|
+
clearPromptResponseStallMonitor(sessionId);
|
|
1379
|
+
clearPromptProcessMonitor(sessionId);
|
|
1380
|
+
activePrompts.delete(sessionId);
|
|
1244
1381
|
|
|
1245
1382
|
// 先写最终状态(done/stopped),确保 display loop 在下一轮消费前
|
|
1246
1383
|
// 读到新状态并终结旧卡片。否则 setImmediate 在 CHECK 阶段先于
|
|
1247
1384
|
// writeFile I/O(POLL 阶段)执行,display loop 会误以为旧轮仍在
|
|
1248
1385
|
// 运行中并更新旧卡片,而不是新建卡片。
|
|
1249
|
-
const finalStatus =
|
|
1386
|
+
const finalStatus = wasAutoEnded
|
|
1387
|
+
? "auto_ended"
|
|
1388
|
+
: (streamErrored || wasAbnormalExit || wasResourceStuck)
|
|
1389
|
+
? "error"
|
|
1390
|
+
: wasStopped
|
|
1391
|
+
? "stopped"
|
|
1392
|
+
: "done";
|
|
1250
1393
|
const finalReply = pickFinalReply(state).trim();
|
|
1251
1394
|
|
|
1252
1395
|
// stop-stuck-loop 接口可能在 fire-and-forget 中已写入带 final_reply 的
|
|
@@ -1267,16 +1410,18 @@ export async function runAgentSession(
|
|
|
1267
1410
|
await writeStreamState({
|
|
1268
1411
|
sessionId,
|
|
1269
1412
|
status: finalStatus,
|
|
1270
|
-
accumulatedContent: state.accumulatedContent,
|
|
1271
|
-
finalReply: finalReplyToWrite,
|
|
1413
|
+
accumulatedContent: state.accumulatedContent,
|
|
1414
|
+
finalReply: finalReplyToWrite,
|
|
1415
|
+
activity: activityTracker.activity,
|
|
1272
1416
|
chunkCount: state.chunkCount,
|
|
1273
1417
|
turnCount: nextTurnCount,
|
|
1274
1418
|
contextTokens: existingInfo?.lastContextTokens ?? 0,
|
|
1275
1419
|
updatedAt: Date.now(),
|
|
1276
1420
|
cwd,
|
|
1277
|
-
tool,
|
|
1278
|
-
...(preserveStuckAt ? { stuckAt: preserveStuckAt } : {}),
|
|
1279
|
-
|
|
1421
|
+
tool,
|
|
1422
|
+
...(preserveStuckAt ? { stuckAt: preserveStuckAt } : {}),
|
|
1423
|
+
...(autoEndedAt !== undefined ? { autoEndedAt } : {}),
|
|
1424
|
+
});
|
|
1280
1425
|
|
|
1281
1426
|
// 消费队列中的缓存消息(异步,不阻塞后续清理)
|
|
1282
1427
|
// 用户 /stop 后应丢弃队列消息,避免用户停止后又自动开始新轮
|
|
@@ -1328,7 +1473,37 @@ export async function runAgentSession(
|
|
|
1328
1473
|
}
|
|
1329
1474
|
console.log(`[${ts()}] Session ${sessionId} stopped (content chunks: ${state.chunkCount})`);
|
|
1330
1475
|
if (tid) logTrace(tid, "SESSION_END", { sessionId, outcome: "stopped", chunks: state.chunkCount });
|
|
1331
|
-
} else if (
|
|
1476
|
+
} else if (wasAutoEnded) {
|
|
1477
|
+
for (const cid of getChatsForSession(sessionId)) {
|
|
1478
|
+
const finfo = sessionInfoMap.get(cid);
|
|
1479
|
+
await recordSessionRegistry({
|
|
1480
|
+
chatId: cid,
|
|
1481
|
+
sessionId,
|
|
1482
|
+
tool,
|
|
1483
|
+
turnCount: finfo?.turnCount ?? nextTurnCount,
|
|
1484
|
+
lastContextTokens: finfo?.lastContextTokens ?? nextContextTokens,
|
|
1485
|
+
startTime: finfo?.startTime ?? now,
|
|
1486
|
+
running: false,
|
|
1487
|
+
});
|
|
1488
|
+
}
|
|
1489
|
+
const activeAutoEnded = getLastActiveChat(sessionId) ?? getChatsForSession(sessionId)[0];
|
|
1490
|
+
if (activeAutoEnded) {
|
|
1491
|
+
const terminalState = await readStreamState(sessionId);
|
|
1492
|
+
if (!displayCards.has(activeAutoEnded) && (!terminalState || !isFinalReplySentForTurn(terminalState))) {
|
|
1493
|
+
const pp = platformForChat(activeAutoEnded) ?? platform;
|
|
1494
|
+
await sendFinalReplyTextOnce(
|
|
1495
|
+
pp,
|
|
1496
|
+
activeAutoEnded,
|
|
1497
|
+
sessionId,
|
|
1498
|
+
nextTurnCount,
|
|
1499
|
+
formatAutoEndedReply(finalReplyToWrite),
|
|
1500
|
+
);
|
|
1501
|
+
}
|
|
1502
|
+
platform.setChatAvatar(activeAutoEnded, tool, "idle").catch(() => {});
|
|
1503
|
+
}
|
|
1504
|
+
console.warn(`[${ts()}] Session ${sessionId} auto-ended after stalled response output (content chunks: ${state.chunkCount})`);
|
|
1505
|
+
if (tid) logTrace(tid, "SESSION_END", { sessionId, outcome: "response_stall", chunks: state.chunkCount });
|
|
1506
|
+
} else if (wasAbnormalExit) {
|
|
1332
1507
|
for (const cid of getChatsForSession(sessionId)) {
|
|
1333
1508
|
const finfo = sessionInfoMap.get(cid);
|
|
1334
1509
|
await recordSessionRegistry({
|
|
@@ -1445,10 +1620,14 @@ export function startUnifiedDisplayLoop(): void {
|
|
|
1445
1620
|
// 还没执行,当前 stream state 可能是 stopSession fire-and-forget
|
|
1446
1621
|
// 写入的,finalReply 滞后于内存态。跳过发送,等 finally 落盘后
|
|
1447
1622
|
// 下一次 tick 再处理,避免发送过期内容或与后续发送重复。
|
|
1448
|
-
if (activePrompts.has(sessionId)) continue;
|
|
1449
|
-
|
|
1450
|
-
const tail = "━━━ 回答结束 ━━━";
|
|
1451
|
-
const finalMsg =
|
|
1623
|
+
if (activePrompts.has(sessionId)) continue;
|
|
1624
|
+
|
|
1625
|
+
const tail = "━━━ 回答结束 ━━━";
|
|
1626
|
+
const finalMsg = state.status === "auto_ended"
|
|
1627
|
+
? formatAutoEndedReply(remaining)
|
|
1628
|
+
: remaining
|
|
1629
|
+
? remaining + "\n" + tail
|
|
1630
|
+
: tail;
|
|
1452
1631
|
if (!isFinalReplySentForTurn(state)) {
|
|
1453
1632
|
await sendFinalReplyTextOnce(p, chatId, sessionId, state.turnCount, finalMsg);
|
|
1454
1633
|
}
|
|
@@ -1497,11 +1676,12 @@ export function startUnifiedDisplayLoop(): void {
|
|
|
1497
1676
|
continue;
|
|
1498
1677
|
}
|
|
1499
1678
|
|
|
1500
|
-
let terminalTextDelivered = true;
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1679
|
+
let terminalTextDelivered = true;
|
|
1680
|
+
const terminalReply = formatTerminalReply(state.status, state.finalReply);
|
|
1681
|
+
if (terminalReply) {
|
|
1682
|
+
if (!isFinalReplySentForTurn(state)) {
|
|
1683
|
+
terminalTextDelivered = await sendFinalReplyTextOnce(p, chatId, sessionId, state.turnCount, terminalReply);
|
|
1684
|
+
}
|
|
1505
1685
|
} else if (state.accumulatedContent.trim()) {
|
|
1506
1686
|
const short = truncateContent(state.accumulatedContent, 30, 4000);
|
|
1507
1687
|
terminalTextDelivered = await p.sendText(chatId, `[生成过程]\n${short}`).then((ok) => ok !== false).catch(() => false);
|
|
@@ -1553,14 +1733,16 @@ export function startUnifiedDisplayLoop(): void {
|
|
|
1553
1733
|
}
|
|
1554
1734
|
} else {
|
|
1555
1735
|
// 非 WeChat: 卡片流程
|
|
1556
|
-
if (display.turnCount !== state.turnCount) {
|
|
1736
|
+
if (display.turnCount !== state.turnCount) {
|
|
1557
1737
|
console.log(`[${ts()}] [DISPLAY] turn mismatch for ${chatId}: display.turnCount=${display.turnCount} state.turnCount=${state.turnCount}, resetting`);
|
|
1558
1738
|
finalizeTurnCards(sessionId, display.turnCount, "done").catch(() => {});
|
|
1559
1739
|
displayCards.delete(chatId);
|
|
1560
|
-
continue;
|
|
1561
|
-
}
|
|
1562
|
-
|
|
1563
|
-
|
|
1740
|
+
continue;
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
const activityHeaderTitle = formatAgentActivityTitle(state.activity, Date.now());
|
|
1744
|
+
|
|
1745
|
+
// 卡片轮转
|
|
1564
1746
|
if (Date.now() - display.cardCreatedAt > CARD_ROTATE_MS) {
|
|
1565
1747
|
display.cardBusy = true;
|
|
1566
1748
|
try {
|
|
@@ -1568,8 +1750,9 @@ export function startUnifiedDisplayLoop(): void {
|
|
|
1568
1750
|
p,
|
|
1569
1751
|
chatId,
|
|
1570
1752
|
sessionId,
|
|
1571
|
-
display.turnCount,
|
|
1572
|
-
display.streamErrorNotified ? undefined : "生成中卡片发送失败,结果将继续更新在上一张卡片中。",
|
|
1753
|
+
display.turnCount,
|
|
1754
|
+
display.streamErrorNotified ? undefined : "生成中卡片发送失败,结果将继续更新在上一张卡片中。",
|
|
1755
|
+
activityHeaderTitle,
|
|
1573
1756
|
);
|
|
1574
1757
|
if (!newCardId) {
|
|
1575
1758
|
display.streamErrorNotified = true;
|
|
@@ -1577,7 +1760,7 @@ export function startUnifiedDisplayLoop(): void {
|
|
|
1577
1760
|
}
|
|
1578
1761
|
const oldSeqBase = display.sequence;
|
|
1579
1762
|
const oldContent = state.accumulatedContent + state.finalReply;
|
|
1580
|
-
const oldCard = buildProgressCard(truncateContent(oldContent) || " ", { showStop: false, headerTitle: "
|
|
1763
|
+
const oldCard = buildProgressCard(truncateContent(oldContent) || " ", { showStop: false, headerTitle: "上一阶段记录" });
|
|
1581
1764
|
await p.cardUpdate(display.cardId, oldCard, oldSeqBase + 1).then(() => {
|
|
1582
1765
|
display.sequence = oldSeqBase + 1;
|
|
1583
1766
|
}).catch(err => {
|
|
@@ -1588,9 +1771,10 @@ export function startUnifiedDisplayLoop(): void {
|
|
|
1588
1771
|
display.sequence = 1;
|
|
1589
1772
|
display.cardCreatedAt = Date.now();
|
|
1590
1773
|
display.rotationAccLen = state.accumulatedContent.length;
|
|
1591
|
-
display.rotationFinalReply = state.finalReply;
|
|
1592
|
-
display.lastSentContent = "";
|
|
1593
|
-
display.
|
|
1774
|
+
display.rotationFinalReply = state.finalReply;
|
|
1775
|
+
display.lastSentContent = "";
|
|
1776
|
+
display.lastSentHeaderTitle = activityHeaderTitle;
|
|
1777
|
+
display.streamErrorNotified = false;
|
|
1594
1778
|
} catch (err) {
|
|
1595
1779
|
console.error(`[${ts()}] [CARDIKT] rotation FAIL for ${chatId}: ${(err as Error).message}`);
|
|
1596
1780
|
} finally {
|
|
@@ -1608,17 +1792,24 @@ export function startUnifiedDisplayLoop(): void {
|
|
|
1608
1792
|
replyDelta = state.finalReply.slice(rotReply.length);
|
|
1609
1793
|
} else {
|
|
1610
1794
|
replyDelta = state.finalReply;
|
|
1611
|
-
}
|
|
1612
|
-
const delta = (accDelta + replyDelta).trim();
|
|
1613
|
-
|
|
1614
|
-
display.dotCount = (display.dotCount % 9) + 1;
|
|
1615
|
-
let deltaBase =
|
|
1616
|
-
if (isCodeBlockOpen(deltaBase)) deltaBase += "\n```";
|
|
1617
|
-
const displayContent = deltaBase + "\n" + "。"
|
|
1618
|
-
if (
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1795
|
+
}
|
|
1796
|
+
const delta = (accDelta + replyDelta).trim();
|
|
1797
|
+
|
|
1798
|
+
display.dotCount = (display.dotCount % 9) + 1;
|
|
1799
|
+
let deltaBase = delta;
|
|
1800
|
+
if (isCodeBlockOpen(deltaBase)) deltaBase += "\n```";
|
|
1801
|
+
const displayContent = deltaBase + "\n" + "。".repeat(display.dotCount);
|
|
1802
|
+
if (
|
|
1803
|
+
displayContent === display.lastSentContent
|
|
1804
|
+
&& activityHeaderTitle === display.lastSentHeaderTitle
|
|
1805
|
+
) continue;
|
|
1806
|
+
|
|
1807
|
+
display.lastSentContent = displayContent;
|
|
1808
|
+
display.lastSentHeaderTitle = activityHeaderTitle;
|
|
1809
|
+
const deltaCard = buildProgressCard(truncateContent(displayContent) || "等待 Agent 输出...", {
|
|
1810
|
+
showStop: true,
|
|
1811
|
+
headerTitle: activityHeaderTitle,
|
|
1812
|
+
});
|
|
1622
1813
|
display.cardBusy = true;
|
|
1623
1814
|
const mySeq = display.sequence + 1;
|
|
1624
1815
|
try {
|
|
@@ -1637,20 +1828,24 @@ export function startUnifiedDisplayLoop(): void {
|
|
|
1637
1828
|
display.cardBusy = false;
|
|
1638
1829
|
}
|
|
1639
1830
|
continue;
|
|
1640
|
-
}
|
|
1641
|
-
|
|
1642
|
-
display.dotCount = (display.dotCount % 9) + 1;
|
|
1643
|
-
let contentBase = state.accumulatedContent + state.finalReply;
|
|
1644
|
-
if (isCodeBlockOpen(contentBase)) contentBase += "\n```";
|
|
1645
|
-
const fullContent = contentBase + "\n" + "。".repeat(display.dotCount);
|
|
1646
|
-
if (
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1831
|
+
}
|
|
1832
|
+
|
|
1833
|
+
display.dotCount = (display.dotCount % 9) + 1;
|
|
1834
|
+
let contentBase = state.accumulatedContent + state.finalReply;
|
|
1835
|
+
if (isCodeBlockOpen(contentBase)) contentBase += "\n```";
|
|
1836
|
+
const fullContent = contentBase + "\n" + "。".repeat(display.dotCount);
|
|
1837
|
+
if (
|
|
1838
|
+
fullContent === display.lastSentContent
|
|
1839
|
+
&& activityHeaderTitle === display.lastSentHeaderTitle
|
|
1840
|
+
) continue;
|
|
1841
|
+
|
|
1842
|
+
display.lastSentContent = fullContent;
|
|
1843
|
+
display.lastSentHeaderTitle = activityHeaderTitle;
|
|
1844
|
+
const cardContent = truncateContent(fullContent) || "等待 Agent 输出...";
|
|
1650
1845
|
display.cardBusy = true;
|
|
1651
1846
|
const mySeq = display.sequence + 1;
|
|
1652
1847
|
try {
|
|
1653
|
-
const card = buildProgressCard(cardContent, { showStop: true, headerTitle:
|
|
1848
|
+
const card = buildProgressCard(cardContent, { showStop: true, headerTitle: activityHeaderTitle });
|
|
1654
1849
|
await p.cardUpdate(display.cardId, card, mySeq);
|
|
1655
1850
|
display.sequence = mySeq;
|
|
1656
1851
|
} catch (err) {
|
|
@@ -1710,8 +1905,9 @@ export function stopUnifiedDisplayLoop(): void {
|
|
|
1710
1905
|
export function stopSession(sessionId: string): boolean {
|
|
1711
1906
|
const prompt = activePrompts.get(sessionId);
|
|
1712
1907
|
if (!prompt) return false;
|
|
1713
|
-
prompt.stopped = true;
|
|
1714
|
-
|
|
1908
|
+
prompt.stopped = true;
|
|
1909
|
+
clearPromptResponseStallMonitor(sessionId);
|
|
1910
|
+
clearPromptProcessMonitor(sessionId);
|
|
1715
1911
|
cancelQueuedMessage(sessionId);
|
|
1716
1912
|
try {
|
|
1717
1913
|
prompt.closeSession?.();
|