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