chatccc 0.2.222 → 0.2.224
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/config.sample.json +40 -39
- package/package.json +1 -1
- package/src/__tests__/builtin-chat-session.test.ts +33 -0
- package/src/__tests__/builtin-config.test.ts +1 -0
- package/src/__tests__/card-plain-text.test.ts +7 -6
- package/src/__tests__/cards.test.ts +179 -178
- package/src/__tests__/ccc-adapter.test.ts +20 -0
- package/src/__tests__/config-reload.test.ts +52 -52
- package/src/__tests__/config-sample.test.ts +41 -40
- package/src/__tests__/orchestrator.test.ts +836 -803
- package/src/__tests__/progress-reducer.test.ts +110 -0
- package/src/__tests__/session.test.ts +1183 -1173
- package/src/__tests__/terminal-renderer.test.ts +143 -0
- package/src/adapters/ccc-adapter.ts +1 -0
- package/src/builtin/cli.ts +42 -1
- package/src/builtin/index.ts +10 -0
- package/src/cards.ts +280 -275
- package/src/config.ts +203 -192
- package/src/progress/reducer.ts +108 -0
- package/src/progress/terminal-renderer.ts +190 -0
- package/src/progress/view.ts +77 -0
- package/src/session.ts +938 -937
- package/src/web-ui.ts +610 -607
package/src/session.ts
CHANGED
|
@@ -2,9 +2,9 @@ import { readFile, writeFile, mkdir } from "node:fs/promises";
|
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
|
|
4
4
|
import {
|
|
5
|
-
CLAUDE_API_KEY,
|
|
6
|
-
CLAUDE_BASE_URL,
|
|
7
|
-
CLAUDE_MAX_TURN,
|
|
5
|
+
CLAUDE_API_KEY,
|
|
6
|
+
CLAUDE_BASE_URL,
|
|
7
|
+
CLAUDE_MAX_TURN,
|
|
8
8
|
CLAUDE_MODEL,
|
|
9
9
|
CLAUDE_SUBAGENT_MODEL,
|
|
10
10
|
CHATCCC_PORT,
|
|
@@ -14,41 +14,42 @@ import {
|
|
|
14
14
|
addRecentDir,
|
|
15
15
|
anthropicConfigDisplay,
|
|
16
16
|
config,
|
|
17
|
-
fileLog,
|
|
18
|
-
getDefaultCwd,
|
|
19
|
-
getDefaultEffortForTool,
|
|
20
|
-
isAnthropicConfigEmpty,
|
|
21
|
-
toolDisplayName,
|
|
22
|
-
ts,
|
|
23
|
-
} from "./config.ts";
|
|
24
|
-
import { buildProgressCard, getToolEmoji, isCodeBlockOpen, truncateContent } from "./cards.ts";
|
|
25
|
-
import {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
17
|
+
fileLog,
|
|
18
|
+
getDefaultCwd,
|
|
19
|
+
getDefaultEffortForTool,
|
|
20
|
+
isAnthropicConfigEmpty,
|
|
21
|
+
toolDisplayName,
|
|
22
|
+
ts,
|
|
23
|
+
} from "./config.ts";
|
|
24
|
+
import { buildProgressCard, getToolEmoji, isCodeBlockOpen, truncateContent } from "./cards.ts";
|
|
25
|
+
import { progressView } from "./progress/view.ts";
|
|
26
|
+
import {
|
|
27
|
+
createAgentActivityTracker,
|
|
28
|
+
formatAgentActivityTitle,
|
|
29
|
+
updateAgentActivity,
|
|
30
|
+
} from "./agent-activity.ts";
|
|
31
|
+
import type { AgentActivityKind } from "./agent-activity.ts";
|
|
31
32
|
import { simplifyToolUse, simplifyToolResult } from "./simplify.ts";
|
|
32
33
|
import { logTrace } from "./trace.ts";
|
|
33
34
|
import type { UnifiedBlock } from "./adapters/adapter-interface.ts";
|
|
34
35
|
import type { ToolAdapter } from "./adapters/adapter-interface.ts";
|
|
35
36
|
import type { ToolProcessInfo } from "./adapters/adapter-interface.ts";
|
|
36
|
-
import { createClaudeAdapter } from "./adapters/claude-adapter.ts";
|
|
37
|
-
import { createCursorAdapter } from "./adapters/cursor-adapter.ts";
|
|
38
|
-
import { createCodexAdapter } from "./adapters/codex-adapter.ts";
|
|
39
|
-
import { createCccAdapter } from "./adapters/ccc-adapter.ts";
|
|
40
|
-
import { killProcessTree } from "./adapters/proc-tree-kill.ts";
|
|
41
|
-
import { resourceMonitor, registerProcess, unregisterProcess } from "./adapters/resource-monitor.ts";
|
|
42
|
-
import { buildImSkillsPromptCached, exportSkillSubDocs, clearImSkillsPromptCache } from "./im-skills.ts";
|
|
43
|
-
import type { PlatformAdapter } from "./platform-adapter.ts";
|
|
44
|
-
import { hasResponseStalled, observeResponseProgress } from "./response-stall.ts";
|
|
45
|
-
import {
|
|
46
|
-
MAX_PROCESSED,
|
|
47
|
-
clearFeishuMessageLedgerMemory,
|
|
48
|
-
processedMessages,
|
|
49
|
-
} from "./feishu-message-ingress.ts";
|
|
50
|
-
|
|
51
|
-
export { MAX_PROCESSED, processedMessages };
|
|
37
|
+
import { createClaudeAdapter } from "./adapters/claude-adapter.ts";
|
|
38
|
+
import { createCursorAdapter } from "./adapters/cursor-adapter.ts";
|
|
39
|
+
import { createCodexAdapter } from "./adapters/codex-adapter.ts";
|
|
40
|
+
import { createCccAdapter } from "./adapters/ccc-adapter.ts";
|
|
41
|
+
import { killProcessTree } from "./adapters/proc-tree-kill.ts";
|
|
42
|
+
import { resourceMonitor, registerProcess, unregisterProcess } from "./adapters/resource-monitor.ts";
|
|
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
|
+
MAX_PROCESSED,
|
|
48
|
+
clearFeishuMessageLedgerMemory,
|
|
49
|
+
processedMessages,
|
|
50
|
+
} from "./feishu-message-ingress.ts";
|
|
51
|
+
|
|
52
|
+
export { MAX_PROCESSED, processedMessages };
|
|
52
53
|
|
|
53
54
|
// 微信显示循环压缩:头5 + ... + 尾5,避免在最后一步 sendText 中压缩指令回复
|
|
54
55
|
function compressWechatDisplayText(text: string): string {
|
|
@@ -80,16 +81,16 @@ import {
|
|
|
80
81
|
pickDisplayChat,
|
|
81
82
|
dequeueMessage,
|
|
82
83
|
consumeQueuedMessage,
|
|
83
|
-
cancelQueuedMessage,
|
|
84
|
-
setQueuePreservedChat,
|
|
85
|
-
consumeQueuePreservedChat,
|
|
86
|
-
markSessionFinalizing,
|
|
87
|
-
clearSessionFinalizing,
|
|
88
|
-
reserveAutoRecovery,
|
|
89
|
-
consumeAutoRecoveryReservation,
|
|
90
|
-
cancelAutoRecoveryReservation,
|
|
91
|
-
hasAutoRecoveryReservation,
|
|
92
|
-
} from "./session-chat-binding.ts";
|
|
84
|
+
cancelQueuedMessage,
|
|
85
|
+
setQueuePreservedChat,
|
|
86
|
+
consumeQueuePreservedChat,
|
|
87
|
+
markSessionFinalizing,
|
|
88
|
+
clearSessionFinalizing,
|
|
89
|
+
reserveAutoRecovery,
|
|
90
|
+
consumeAutoRecoveryReservation,
|
|
91
|
+
cancelAutoRecoveryReservation,
|
|
92
|
+
hasAutoRecoveryReservation,
|
|
93
|
+
} from "./session-chat-binding.ts";
|
|
93
94
|
|
|
94
95
|
async function sendFinalReplyTextOnce(
|
|
95
96
|
platform: PlatformAdapter,
|
|
@@ -103,19 +104,19 @@ async function sendFinalReplyTextOnce(
|
|
|
103
104
|
return sent;
|
|
104
105
|
}
|
|
105
106
|
|
|
106
|
-
async function createVisibleProgressCard(
|
|
107
|
+
async function createVisibleProgressCard(
|
|
107
108
|
platform: PlatformAdapter,
|
|
108
109
|
chatId: string,
|
|
109
110
|
sessionId: string,
|
|
110
|
-
turnCount: number,
|
|
111
|
-
notifyFailureText?: string,
|
|
112
|
-
headerTitle = "正在启动 Agent · 0秒",
|
|
113
|
-
): Promise<string | null> {
|
|
111
|
+
turnCount: number,
|
|
112
|
+
notifyFailureText?: string,
|
|
113
|
+
headerTitle = "正在启动 Agent · 0秒",
|
|
114
|
+
): Promise<string | null> {
|
|
114
115
|
for (let attempt = 1; attempt <= 2; attempt++) {
|
|
115
116
|
let cardId: string | null = null;
|
|
116
117
|
try {
|
|
117
|
-
cardId = await platform.cardCreate(
|
|
118
|
-
buildProgressCard("等待 Agent 输出...",
|
|
118
|
+
cardId = await platform.cardCreate(
|
|
119
|
+
buildProgressCard(progressView({ text: "等待 Agent 输出...", showStop: true, headerTitle })),
|
|
119
120
|
);
|
|
120
121
|
if (!cardId) throw new Error("empty card id");
|
|
121
122
|
await platform.cardSend(chatId, cardId);
|
|
@@ -167,20 +168,20 @@ function platformForChat(chatId: string): PlatformAdapter | null {
|
|
|
167
168
|
return chatPlatformMap.get(chatId) ?? platformRef;
|
|
168
169
|
}
|
|
169
170
|
|
|
170
|
-
const DEFAULT_PROCESS_MONITOR_INTERVAL_MS = 5000;
|
|
171
|
-
const DEFAULT_RESPONSE_STALL_TIMEOUT_MS = 3 * 60 * 1000;
|
|
172
|
-
const DEFAULT_RESPONSE_STALL_CHECK_INTERVAL_MS = 5000;
|
|
173
|
-
const DEFAULT_FINAL_RESPONSE_CLOSE_TIMEOUT_MS = 10_000;
|
|
174
|
-
export const RESPONSE_STALL_RECOVERY_PROMPT = "完成了吗?如果没完成继续";
|
|
175
|
-
export const RESPONSE_STALL_RECOVERY_NOTICE =
|
|
176
|
-
`检测到会话停滞,正在自动确认并继续。\n\n${RESPONSE_STALL_RECOVERY_PROMPT}`;
|
|
177
|
-
export const RESPONSE_STALL_RECOVERY_EXHAUSTED_NOTICE =
|
|
178
|
-
"⚠️ 自动续跑仍连续 3 分钟没有启动进展或新回复,本次不再自动继续。";
|
|
179
|
-
const RESPONSE_STALL_RECOVERY_DELAY_MS = 200;
|
|
180
|
-
let processMonitorIntervalMs = DEFAULT_PROCESS_MONITOR_INTERVAL_MS;
|
|
181
|
-
let responseStallTimeoutMs = DEFAULT_RESPONSE_STALL_TIMEOUT_MS;
|
|
182
|
-
let responseStallCheckIntervalMs = DEFAULT_RESPONSE_STALL_CHECK_INTERVAL_MS;
|
|
183
|
-
let finalResponseCloseTimeoutMs = DEFAULT_FINAL_RESPONSE_CLOSE_TIMEOUT_MS;
|
|
171
|
+
const DEFAULT_PROCESS_MONITOR_INTERVAL_MS = 5000;
|
|
172
|
+
const DEFAULT_RESPONSE_STALL_TIMEOUT_MS = 3 * 60 * 1000;
|
|
173
|
+
const DEFAULT_RESPONSE_STALL_CHECK_INTERVAL_MS = 5000;
|
|
174
|
+
const DEFAULT_FINAL_RESPONSE_CLOSE_TIMEOUT_MS = 10_000;
|
|
175
|
+
export const RESPONSE_STALL_RECOVERY_PROMPT = "完成了吗?如果没完成继续";
|
|
176
|
+
export const RESPONSE_STALL_RECOVERY_NOTICE =
|
|
177
|
+
`检测到会话停滞,正在自动确认并继续。\n\n${RESPONSE_STALL_RECOVERY_PROMPT}`;
|
|
178
|
+
export const RESPONSE_STALL_RECOVERY_EXHAUSTED_NOTICE =
|
|
179
|
+
"⚠️ 自动续跑仍连续 3 分钟没有启动进展或新回复,本次不再自动继续。";
|
|
180
|
+
const RESPONSE_STALL_RECOVERY_DELAY_MS = 200;
|
|
181
|
+
let processMonitorIntervalMs = DEFAULT_PROCESS_MONITOR_INTERVAL_MS;
|
|
182
|
+
let responseStallTimeoutMs = DEFAULT_RESPONSE_STALL_TIMEOUT_MS;
|
|
183
|
+
let responseStallCheckIntervalMs = DEFAULT_RESPONSE_STALL_CHECK_INTERVAL_MS;
|
|
184
|
+
let finalResponseCloseTimeoutMs = DEFAULT_FINAL_RESPONSE_CLOSE_TIMEOUT_MS;
|
|
184
185
|
let isProcessAliveImpl = (pid: number): boolean => {
|
|
185
186
|
try {
|
|
186
187
|
process.kill(pid, 0);
|
|
@@ -209,137 +210,137 @@ export function _setProcessMonitorIntervalForTest(ms: number): void {
|
|
|
209
210
|
processMonitorIntervalMs = ms;
|
|
210
211
|
}
|
|
211
212
|
|
|
212
|
-
export function _resetProcessMonitorIntervalForTest(): void {
|
|
213
|
-
processMonitorIntervalMs = DEFAULT_PROCESS_MONITOR_INTERVAL_MS;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
export function _setResponseStallTimeoutForTest(ms: number): void {
|
|
217
|
-
responseStallTimeoutMs = ms;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
export function _resetResponseStallTimeoutForTest(): void {
|
|
221
|
-
responseStallTimeoutMs = DEFAULT_RESPONSE_STALL_TIMEOUT_MS;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
export function _setResponseStallCheckIntervalForTest(ms: number): void {
|
|
225
|
-
responseStallCheckIntervalMs = ms;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
export function _resetResponseStallCheckIntervalForTest(): void {
|
|
229
|
-
responseStallCheckIntervalMs = DEFAULT_RESPONSE_STALL_CHECK_INTERVAL_MS;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
export function _setFinalResponseCloseTimeoutForTest(ms: number): void {
|
|
233
|
-
finalResponseCloseTimeoutMs = ms;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
export function _resetFinalResponseCloseTimeoutForTest(): void {
|
|
237
|
-
finalResponseCloseTimeoutMs = DEFAULT_FINAL_RESPONSE_CLOSE_TIMEOUT_MS;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
function clearPromptProcessMonitor(sessionId: string): void {
|
|
213
|
+
export function _resetProcessMonitorIntervalForTest(): void {
|
|
214
|
+
processMonitorIntervalMs = DEFAULT_PROCESS_MONITOR_INTERVAL_MS;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export function _setResponseStallTimeoutForTest(ms: number): void {
|
|
218
|
+
responseStallTimeoutMs = ms;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export function _resetResponseStallTimeoutForTest(): void {
|
|
222
|
+
responseStallTimeoutMs = DEFAULT_RESPONSE_STALL_TIMEOUT_MS;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export function _setResponseStallCheckIntervalForTest(ms: number): void {
|
|
226
|
+
responseStallCheckIntervalMs = ms;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export function _resetResponseStallCheckIntervalForTest(): void {
|
|
230
|
+
responseStallCheckIntervalMs = DEFAULT_RESPONSE_STALL_CHECK_INTERVAL_MS;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export function _setFinalResponseCloseTimeoutForTest(ms: number): void {
|
|
234
|
+
finalResponseCloseTimeoutMs = ms;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export function _resetFinalResponseCloseTimeoutForTest(): void {
|
|
238
|
+
finalResponseCloseTimeoutMs = DEFAULT_FINAL_RESPONSE_CLOSE_TIMEOUT_MS;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function clearPromptProcessMonitor(sessionId: string): void {
|
|
241
242
|
const prompt = activePrompts.get(sessionId);
|
|
242
243
|
if (!prompt?.processMonitor) return;
|
|
243
244
|
clearInterval(prompt.processMonitor);
|
|
244
245
|
prompt.processMonitor = undefined;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
function clearPromptResponseStallMonitor(sessionId: string): void {
|
|
248
|
-
const prompt = activePrompts.get(sessionId);
|
|
249
|
-
if (!prompt?.responseStallMonitor) return;
|
|
250
|
-
clearInterval(prompt.responseStallMonitor);
|
|
251
|
-
prompt.responseStallMonitor = undefined;
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
function clearPromptFinalResponseCloseTimer(sessionId: string): void {
|
|
255
|
-
const prompt = activePrompts.get(sessionId);
|
|
256
|
-
if (!prompt?.finalResponseCloseTimer) return;
|
|
257
|
-
clearTimeout(prompt.finalResponseCloseTimer);
|
|
258
|
-
prompt.finalResponseCloseTimer = undefined;
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
/**
|
|
262
|
-
* 权威终态只说明 Agent 已完成本轮,不保证 CLI/SDK 的输出流会及时关闭。
|
|
263
|
-
* 给正常清理保留 10 秒;若流仍悬挂,则关闭底层 session 并杀掉当前 CLI 树,
|
|
264
|
-
* 让 runAgentSession 以 done 收尾。这里绝不触发自动续跑,因为答案已完整到达。
|
|
265
|
-
*/
|
|
266
|
-
function scheduleFinalResponseCloseGuard(
|
|
267
|
-
sessionId: string,
|
|
268
|
-
runningPrompt: NonNullable<ReturnType<typeof activePrompts.get>>,
|
|
269
|
-
): void {
|
|
270
|
-
if (runningPrompt.finalResponseCloseTimer) return;
|
|
271
|
-
|
|
272
|
-
const timeoutMs = finalResponseCloseTimeoutMs;
|
|
273
|
-
const handle = setTimeout(() => {
|
|
274
|
-
const current = activePrompts.get(sessionId);
|
|
275
|
-
if (
|
|
276
|
-
!current
|
|
277
|
-
|| current !== runningPrompt
|
|
278
|
-
|| !current.finalResponseObserved
|
|
279
|
-
|| current.stopped
|
|
280
|
-
|| current.abnormalExit
|
|
281
|
-
|| current.resourceStuck
|
|
282
|
-
|| current.autoEnded
|
|
283
|
-
) {
|
|
284
|
-
return;
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
current.finalResponseCloseTimer = undefined;
|
|
288
|
-
clearPromptProcessMonitor(sessionId);
|
|
289
|
-
clearPromptResponseStallMonitor(sessionId);
|
|
290
|
-
try {
|
|
291
|
-
current.closeSession?.();
|
|
292
|
-
} catch (err) {
|
|
293
|
-
console.warn(
|
|
294
|
-
`[${ts()}] [FINAL-RESPONSE] closeSession failed for ${sessionId}: ${(err as Error).message}`,
|
|
295
|
-
);
|
|
296
|
-
}
|
|
297
|
-
current.controller.abort();
|
|
298
|
-
void killProcessTree(current.processPid);
|
|
299
|
-
console.warn(
|
|
300
|
-
`[${ts()}] [FINAL-RESPONSE] Session ${sessionId} stream stayed open for ${timeoutMs}ms after its authoritative final event; forced clean shutdown`,
|
|
301
|
-
);
|
|
302
|
-
}, timeoutMs);
|
|
303
|
-
handle.unref?.();
|
|
304
|
-
runningPrompt.finalResponseCloseTimer = handle;
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
function formatTerminalHeader(status: "running" | "done" | "stopped" | "error" | "auto_ended"): {
|
|
308
|
-
title: string;
|
|
309
|
-
template?: string;
|
|
310
|
-
} {
|
|
311
|
-
if (status === "auto_ended") return { title: "已自动结束 · 3分钟无新内容", template: "orange" };
|
|
312
|
-
if (status === "stopped") return { title: "已停止", template: "red" };
|
|
313
|
-
if (status === "error") return { title: "异常结束", template: "red" };
|
|
314
|
-
return { title: "完成" };
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
function turnFinalStatus(status: "running" | "done" | "stopped" | "error" | "auto_ended"): "done" | "stopped" {
|
|
318
|
-
return status === "stopped" || status === "error" || status === "auto_ended" ? "stopped" : "done";
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
function formatAutoEndedReply(finalReply: string): string {
|
|
322
|
-
const reason = "⚠️ 已自动结束:连续 3 分钟没有启动进展或回复字符变化。";
|
|
323
|
-
return finalReply
|
|
324
|
-
? `${reason}以下回复可能不完整。\n\n${finalReply}`
|
|
325
|
-
: `${reason}本轮没有可发送的回复内容。`;
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
/**
|
|
329
|
-
* 只监控用户无法判断是否仍有进展的两个阶段。思考、工具调用和搜索可能合法地
|
|
330
|
-
* 长时间不产生回复字符,由资源监控负责识别真正僵死,不能在这里误杀。
|
|
331
|
-
*/
|
|
332
|
-
function monitorsOutputProgress(kind: AgentActivityKind): boolean {
|
|
333
|
-
return kind === "starting" || kind === "responding";
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
function formatTerminalReply(
|
|
337
|
-
status: "running" | "done" | "stopped" | "error" | "auto_ended",
|
|
338
|
-
finalReply: string,
|
|
339
|
-
): string | null {
|
|
340
|
-
if (status === "auto_ended") return formatAutoEndedReply(finalReply);
|
|
341
|
-
return finalReply || null;
|
|
342
|
-
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function clearPromptResponseStallMonitor(sessionId: string): void {
|
|
249
|
+
const prompt = activePrompts.get(sessionId);
|
|
250
|
+
if (!prompt?.responseStallMonitor) return;
|
|
251
|
+
clearInterval(prompt.responseStallMonitor);
|
|
252
|
+
prompt.responseStallMonitor = undefined;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function clearPromptFinalResponseCloseTimer(sessionId: string): void {
|
|
256
|
+
const prompt = activePrompts.get(sessionId);
|
|
257
|
+
if (!prompt?.finalResponseCloseTimer) return;
|
|
258
|
+
clearTimeout(prompt.finalResponseCloseTimer);
|
|
259
|
+
prompt.finalResponseCloseTimer = undefined;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* 权威终态只说明 Agent 已完成本轮,不保证 CLI/SDK 的输出流会及时关闭。
|
|
264
|
+
* 给正常清理保留 10 秒;若流仍悬挂,则关闭底层 session 并杀掉当前 CLI 树,
|
|
265
|
+
* 让 runAgentSession 以 done 收尾。这里绝不触发自动续跑,因为答案已完整到达。
|
|
266
|
+
*/
|
|
267
|
+
function scheduleFinalResponseCloseGuard(
|
|
268
|
+
sessionId: string,
|
|
269
|
+
runningPrompt: NonNullable<ReturnType<typeof activePrompts.get>>,
|
|
270
|
+
): void {
|
|
271
|
+
if (runningPrompt.finalResponseCloseTimer) return;
|
|
272
|
+
|
|
273
|
+
const timeoutMs = finalResponseCloseTimeoutMs;
|
|
274
|
+
const handle = setTimeout(() => {
|
|
275
|
+
const current = activePrompts.get(sessionId);
|
|
276
|
+
if (
|
|
277
|
+
!current
|
|
278
|
+
|| current !== runningPrompt
|
|
279
|
+
|| !current.finalResponseObserved
|
|
280
|
+
|| current.stopped
|
|
281
|
+
|| current.abnormalExit
|
|
282
|
+
|| current.resourceStuck
|
|
283
|
+
|| current.autoEnded
|
|
284
|
+
) {
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
current.finalResponseCloseTimer = undefined;
|
|
289
|
+
clearPromptProcessMonitor(sessionId);
|
|
290
|
+
clearPromptResponseStallMonitor(sessionId);
|
|
291
|
+
try {
|
|
292
|
+
current.closeSession?.();
|
|
293
|
+
} catch (err) {
|
|
294
|
+
console.warn(
|
|
295
|
+
`[${ts()}] [FINAL-RESPONSE] closeSession failed for ${sessionId}: ${(err as Error).message}`,
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
current.controller.abort();
|
|
299
|
+
void killProcessTree(current.processPid);
|
|
300
|
+
console.warn(
|
|
301
|
+
`[${ts()}] [FINAL-RESPONSE] Session ${sessionId} stream stayed open for ${timeoutMs}ms after its authoritative final event; forced clean shutdown`,
|
|
302
|
+
);
|
|
303
|
+
}, timeoutMs);
|
|
304
|
+
handle.unref?.();
|
|
305
|
+
runningPrompt.finalResponseCloseTimer = handle;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function formatTerminalHeader(status: "running" | "done" | "stopped" | "error" | "auto_ended"): {
|
|
309
|
+
title: string;
|
|
310
|
+
template?: string;
|
|
311
|
+
} {
|
|
312
|
+
if (status === "auto_ended") return { title: "已自动结束 · 3分钟无新内容", template: "orange" };
|
|
313
|
+
if (status === "stopped") return { title: "已停止", template: "red" };
|
|
314
|
+
if (status === "error") return { title: "异常结束", template: "red" };
|
|
315
|
+
return { title: "完成" };
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function turnFinalStatus(status: "running" | "done" | "stopped" | "error" | "auto_ended"): "done" | "stopped" {
|
|
319
|
+
return status === "stopped" || status === "error" || status === "auto_ended" ? "stopped" : "done";
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function formatAutoEndedReply(finalReply: string): string {
|
|
323
|
+
const reason = "⚠️ 已自动结束:连续 3 分钟没有启动进展或回复字符变化。";
|
|
324
|
+
return finalReply
|
|
325
|
+
? `${reason}以下回复可能不完整。\n\n${finalReply}`
|
|
326
|
+
: `${reason}本轮没有可发送的回复内容。`;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* 只监控用户无法判断是否仍有进展的两个阶段。思考、工具调用和搜索可能合法地
|
|
331
|
+
* 长时间不产生回复字符,由资源监控负责识别真正僵死,不能在这里误杀。
|
|
332
|
+
*/
|
|
333
|
+
function monitorsOutputProgress(kind: AgentActivityKind): boolean {
|
|
334
|
+
return kind === "starting" || kind === "responding";
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function formatTerminalReply(
|
|
338
|
+
status: "running" | "done" | "stopped" | "error" | "auto_ended",
|
|
339
|
+
finalReply: string,
|
|
340
|
+
): string | null {
|
|
341
|
+
if (status === "auto_ended") return formatAutoEndedReply(finalReply);
|
|
342
|
+
return finalReply || null;
|
|
343
|
+
}
|
|
343
344
|
|
|
344
345
|
function isCardKitSequenceConflict(err: unknown): boolean {
|
|
345
346
|
return err instanceof Error && err.message.includes("300317");
|
|
@@ -357,7 +358,7 @@ function startPromptProcessMonitor(sessionId: string, info: ToolProcessInfo): vo
|
|
|
357
358
|
clearPromptProcessMonitor(sessionId);
|
|
358
359
|
return;
|
|
359
360
|
}
|
|
360
|
-
if (current.stopped || current.abnormalExit || current.resourceStuck || current.autoEnded) return;
|
|
361
|
+
if (current.stopped || current.abnormalExit || current.resourceStuck || current.autoEnded) return;
|
|
361
362
|
if (isProcessAliveImpl(info.pid)) return;
|
|
362
363
|
|
|
363
364
|
current.abnormalExit = true;
|
|
@@ -467,23 +468,23 @@ export function resetState(): void {
|
|
|
467
468
|
}
|
|
468
469
|
chatSessionMap.clear();
|
|
469
470
|
sessionInfoMap.clear();
|
|
470
|
-
clearFeishuMessageLedgerMemory();
|
|
471
|
+
clearFeishuMessageLedgerMemory();
|
|
471
472
|
lastMsgTimestamps.clear();
|
|
472
473
|
chatPlatformMap.clear();
|
|
473
|
-
for (const prompt of activePrompts.values()) {
|
|
474
|
-
if (prompt.processMonitor) clearInterval(prompt.processMonitor);
|
|
475
|
-
if (prompt.responseStallMonitor) clearInterval(prompt.responseStallMonitor);
|
|
476
|
-
if (prompt.finalResponseCloseTimer) clearTimeout(prompt.finalResponseCloseTimer);
|
|
477
|
-
}
|
|
478
|
-
activePrompts.clear();
|
|
479
|
-
displayCards.clear();
|
|
480
|
-
sessionModelOverrides.clear();
|
|
481
|
-
sessionEffortOverrides.clear();
|
|
482
|
-
sessionFastModeOverrides.clear();
|
|
483
|
-
adapterCache.clear();
|
|
484
|
-
stopUnifiedDisplayLoop();
|
|
485
|
-
console.log(`[${ts()}] [RESET] State cleared (dedup + active sessions + bindings)`);
|
|
486
|
-
}
|
|
474
|
+
for (const prompt of activePrompts.values()) {
|
|
475
|
+
if (prompt.processMonitor) clearInterval(prompt.processMonitor);
|
|
476
|
+
if (prompt.responseStallMonitor) clearInterval(prompt.responseStallMonitor);
|
|
477
|
+
if (prompt.finalResponseCloseTimer) clearTimeout(prompt.finalResponseCloseTimer);
|
|
478
|
+
}
|
|
479
|
+
activePrompts.clear();
|
|
480
|
+
displayCards.clear();
|
|
481
|
+
sessionModelOverrides.clear();
|
|
482
|
+
sessionEffortOverrides.clear();
|
|
483
|
+
sessionFastModeOverrides.clear();
|
|
484
|
+
adapterCache.clear();
|
|
485
|
+
stopUnifiedDisplayLoop();
|
|
486
|
+
console.log(`[${ts()}] [RESET] State cleared (dedup + active sessions + bindings)`);
|
|
487
|
+
}
|
|
487
488
|
|
|
488
489
|
// 注:`rebuildBindingsFromRegistry` 定义在下方与 loadSessionRegistry 同区域,
|
|
489
490
|
// 是 onReady/onReconnected 取代 resetState 的正确入口。
|
|
@@ -495,9 +496,9 @@ export function resetState(): void {
|
|
|
495
496
|
const adapterCache = new Map<string, ToolAdapter>();
|
|
496
497
|
|
|
497
498
|
// Per-session 模型覆盖(/model 命令设置,不持久化)
|
|
498
|
-
const sessionModelOverrides = new Map<string, string>();
|
|
499
|
-
const sessionEffortOverrides = new Map<string, string>();
|
|
500
|
-
const sessionFastModeOverrides = new Map<string, boolean>();
|
|
499
|
+
const sessionModelOverrides = new Map<string, string>();
|
|
500
|
+
const sessionEffortOverrides = new Map<string, string>();
|
|
501
|
+
const sessionFastModeOverrides = new Map<string, boolean>();
|
|
501
502
|
|
|
502
503
|
/** 返回 session 的生效模型:优先 per-session 覆盖,其次全局配置(Claude) */
|
|
503
504
|
function getModelForSession(sessionId?: string): string {
|
|
@@ -509,47 +510,47 @@ function getModelForSession(sessionId?: string): string {
|
|
|
509
510
|
}
|
|
510
511
|
|
|
511
512
|
/** 返回指定 tool 的生效模型:优先 per-session 覆盖,其次 tool 默认配置 */
|
|
512
|
-
export function getEffectiveModelForTool(tool: string, sessionId?: string): string {
|
|
513
|
-
if (sessionId) {
|
|
514
|
-
const override = sessionModelOverrides.get(sessionId);
|
|
515
|
-
if (override) return override;
|
|
516
|
-
}
|
|
517
|
-
if (tool === "cursor") return config.cursor.model;
|
|
518
|
-
if (tool === "codex") return config.codex.model;
|
|
519
|
-
if (tool === "ccc") return config.ccc.model;
|
|
520
|
-
return CLAUDE_MODEL;
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
export function getEffectiveEffortForTool(tool: string, sessionId?: string): string {
|
|
524
|
-
if (sessionId) {
|
|
525
|
-
const override = sessionEffortOverrides.get(sessionId);
|
|
526
|
-
if (override) return override;
|
|
527
|
-
}
|
|
528
|
-
if (tool === "claude" || tool === "codex") {
|
|
529
|
-
return getDefaultEffortForTool(tool);
|
|
530
|
-
}
|
|
531
|
-
return "";
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
export function getEffectiveFastModeForTool(tool: string, sessionId?: string): boolean {
|
|
535
|
-
if (tool !== "codex") return false;
|
|
536
|
-
if (sessionId && sessionFastModeOverrides.has(sessionId)) {
|
|
537
|
-
return sessionFastModeOverrides.get(sessionId) === true;
|
|
538
|
-
}
|
|
539
|
-
return config.codex.fastMode;
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
function setSessionChatAvatar(
|
|
543
|
-
platform: PlatformAdapter,
|
|
544
|
-
chatId: string,
|
|
545
|
-
tool: string,
|
|
546
|
-
status: string,
|
|
547
|
-
sessionId: string,
|
|
548
|
-
): Promise<void> {
|
|
549
|
-
return getEffectiveFastModeForTool(tool, sessionId)
|
|
550
|
-
? platform.setChatAvatar(chatId, tool, status, { fastMode: true })
|
|
551
|
-
: platform.setChatAvatar(chatId, tool, status);
|
|
552
|
-
}
|
|
513
|
+
export function getEffectiveModelForTool(tool: string, sessionId?: string): string {
|
|
514
|
+
if (sessionId) {
|
|
515
|
+
const override = sessionModelOverrides.get(sessionId);
|
|
516
|
+
if (override) return override;
|
|
517
|
+
}
|
|
518
|
+
if (tool === "cursor") return config.cursor.model;
|
|
519
|
+
if (tool === "codex") return config.codex.model;
|
|
520
|
+
if (tool === "ccc") return config.ccc.model;
|
|
521
|
+
return CLAUDE_MODEL;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
export function getEffectiveEffortForTool(tool: string, sessionId?: string): string {
|
|
525
|
+
if (sessionId) {
|
|
526
|
+
const override = sessionEffortOverrides.get(sessionId);
|
|
527
|
+
if (override) return override;
|
|
528
|
+
}
|
|
529
|
+
if (tool === "claude" || tool === "codex" || tool === "ccc") {
|
|
530
|
+
return getDefaultEffortForTool(tool);
|
|
531
|
+
}
|
|
532
|
+
return "";
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
export function getEffectiveFastModeForTool(tool: string, sessionId?: string): boolean {
|
|
536
|
+
if (tool !== "codex") return false;
|
|
537
|
+
if (sessionId && sessionFastModeOverrides.has(sessionId)) {
|
|
538
|
+
return sessionFastModeOverrides.get(sessionId) === true;
|
|
539
|
+
}
|
|
540
|
+
return config.codex.fastMode;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function setSessionChatAvatar(
|
|
544
|
+
platform: PlatformAdapter,
|
|
545
|
+
chatId: string,
|
|
546
|
+
tool: string,
|
|
547
|
+
status: string,
|
|
548
|
+
sessionId: string,
|
|
549
|
+
): Promise<void> {
|
|
550
|
+
return getEffectiveFastModeForTool(tool, sessionId)
|
|
551
|
+
? platform.setChatAvatar(chatId, tool, status, { fastMode: true })
|
|
552
|
+
: platform.setChatAvatar(chatId, tool, status);
|
|
553
|
+
}
|
|
553
554
|
|
|
554
555
|
/** 为指定 session 设置模型覆盖(/model <name>) */
|
|
555
556
|
export function setSessionModelOverride(sessionId: string, model: string): void {
|
|
@@ -558,53 +559,56 @@ export function setSessionModelOverride(sessionId: string, model: string): void
|
|
|
558
559
|
}
|
|
559
560
|
|
|
560
561
|
/** 清除指定 session 的模型覆盖(/model clear) */
|
|
561
|
-
export function clearSessionModelOverride(sessionId: string): void {
|
|
562
|
-
sessionModelOverrides.delete(sessionId);
|
|
563
|
-
adapterCache.clear();
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
export function setSessionEffortOverride(sessionId: string, effort: string): void {
|
|
567
|
-
sessionEffortOverrides.set(sessionId, effort);
|
|
568
|
-
adapterCache.clear();
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
export function clearSessionEffortOverride(sessionId: string): void {
|
|
572
|
-
sessionEffortOverrides.delete(sessionId);
|
|
573
|
-
adapterCache.clear();
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
export function setSessionFastModeOverride(sessionId: string, fastMode: boolean): void {
|
|
577
|
-
sessionFastModeOverrides.set(sessionId, fastMode);
|
|
578
|
-
adapterCache.clear();
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
export function getAdapterForTool(tool: string, sessionId?: string): ToolAdapter {
|
|
582
|
-
const effectiveModel = getEffectiveModelForTool(tool, sessionId);
|
|
583
|
-
const effectiveEffort = getEffectiveEffortForTool(tool, sessionId);
|
|
584
|
-
const effectiveFastMode = getEffectiveFastModeForTool(tool, sessionId);
|
|
585
|
-
const cacheKey = `${tool}:${effectiveModel || ""}:${effectiveEffort || ""}:${effectiveFastMode ? "fast" : "default"}`;
|
|
586
|
-
const cached = adapterCache.get(cacheKey);
|
|
587
|
-
if (cached) return cached;
|
|
588
|
-
|
|
589
|
-
let adapter: ToolAdapter;
|
|
590
|
-
if (tool === "cursor") {
|
|
591
|
-
adapter = createCursorAdapter({ model: effectiveModel || undefined });
|
|
592
|
-
} else if (tool === "codex") {
|
|
593
|
-
adapter = createCodexAdapter({
|
|
594
|
-
model: effectiveModel || undefined,
|
|
595
|
-
effort: effectiveEffort || undefined,
|
|
596
|
-
fastMode: effectiveFastMode,
|
|
597
|
-
});
|
|
598
|
-
} else if (tool === "ccc") {
|
|
599
|
-
adapter = createCccAdapter({
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
562
|
+
export function clearSessionModelOverride(sessionId: string): void {
|
|
563
|
+
sessionModelOverrides.delete(sessionId);
|
|
564
|
+
adapterCache.clear();
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
export function setSessionEffortOverride(sessionId: string, effort: string): void {
|
|
568
|
+
sessionEffortOverrides.set(sessionId, effort);
|
|
569
|
+
adapterCache.clear();
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
export function clearSessionEffortOverride(sessionId: string): void {
|
|
573
|
+
sessionEffortOverrides.delete(sessionId);
|
|
574
|
+
adapterCache.clear();
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
export function setSessionFastModeOverride(sessionId: string, fastMode: boolean): void {
|
|
578
|
+
sessionFastModeOverrides.set(sessionId, fastMode);
|
|
579
|
+
adapterCache.clear();
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
export function getAdapterForTool(tool: string, sessionId?: string): ToolAdapter {
|
|
583
|
+
const effectiveModel = getEffectiveModelForTool(tool, sessionId);
|
|
584
|
+
const effectiveEffort = getEffectiveEffortForTool(tool, sessionId);
|
|
585
|
+
const effectiveFastMode = getEffectiveFastModeForTool(tool, sessionId);
|
|
586
|
+
const cacheKey = `${tool}:${effectiveModel || ""}:${effectiveEffort || ""}:${effectiveFastMode ? "fast" : "default"}`;
|
|
587
|
+
const cached = adapterCache.get(cacheKey);
|
|
588
|
+
if (cached) return cached;
|
|
589
|
+
|
|
590
|
+
let adapter: ToolAdapter;
|
|
591
|
+
if (tool === "cursor") {
|
|
592
|
+
adapter = createCursorAdapter({ model: effectiveModel || undefined });
|
|
593
|
+
} else if (tool === "codex") {
|
|
594
|
+
adapter = createCodexAdapter({
|
|
595
|
+
model: effectiveModel || undefined,
|
|
596
|
+
effort: effectiveEffort || undefined,
|
|
597
|
+
fastMode: effectiveFastMode,
|
|
598
|
+
});
|
|
599
|
+
} else if (tool === "ccc") {
|
|
600
|
+
adapter = createCccAdapter({
|
|
601
|
+
model: effectiveModel || undefined,
|
|
602
|
+
effort: effectiveEffort || undefined,
|
|
603
|
+
});
|
|
604
|
+
} else {
|
|
605
|
+
adapter = createClaudeAdapter({
|
|
606
|
+
model: effectiveModel,
|
|
607
|
+
subagentModel: CLAUDE_SUBAGENT_MODEL,
|
|
608
|
+
effort: effectiveEffort,
|
|
609
|
+
apiKey: CLAUDE_API_KEY,
|
|
610
|
+
baseUrl: CLAUDE_BASE_URL,
|
|
611
|
+
isEmpty: isAnthropicConfigEmpty,
|
|
608
612
|
maxTurn: CLAUDE_MAX_TURN,
|
|
609
613
|
});
|
|
610
614
|
}
|
|
@@ -676,13 +680,13 @@ export function _resetSessionToolsFileForTest(): void {
|
|
|
676
680
|
export const SESSION_REGISTRY_FILE = join(USER_DATA_DIR, "state", "session-registry.json");
|
|
677
681
|
let sessionRegistryFile = SESSION_REGISTRY_FILE;
|
|
678
682
|
|
|
679
|
-
export interface SessionRegistryUpdate {
|
|
680
|
-
chatId: string;
|
|
681
|
-
sessionId: string;
|
|
682
|
-
tool: string;
|
|
683
|
-
/** 会话容器类型;旧 registry 没有该字段,读取时必须兼容。 */
|
|
684
|
-
chatType?: string;
|
|
685
|
-
chatName?: string;
|
|
683
|
+
export interface SessionRegistryUpdate {
|
|
684
|
+
chatId: string;
|
|
685
|
+
sessionId: string;
|
|
686
|
+
tool: string;
|
|
687
|
+
/** 会话容器类型;旧 registry 没有该字段,读取时必须兼容。 */
|
|
688
|
+
chatType?: string;
|
|
689
|
+
chatName?: string;
|
|
686
690
|
turnCount?: number;
|
|
687
691
|
lastContextTokens?: number;
|
|
688
692
|
startTime?: number;
|
|
@@ -690,12 +694,12 @@ export interface SessionRegistryUpdate {
|
|
|
690
694
|
running?: boolean;
|
|
691
695
|
}
|
|
692
696
|
|
|
693
|
-
interface SessionRegistryRecord {
|
|
694
|
-
chatId: string;
|
|
695
|
-
sessionId: string;
|
|
696
|
-
tool: string;
|
|
697
|
-
chatType?: string;
|
|
698
|
-
chatName: string;
|
|
697
|
+
interface SessionRegistryRecord {
|
|
698
|
+
chatId: string;
|
|
699
|
+
sessionId: string;
|
|
700
|
+
tool: string;
|
|
701
|
+
chatType?: string;
|
|
702
|
+
chatName: string;
|
|
699
703
|
turnCount: number;
|
|
700
704
|
lastContextTokens: number;
|
|
701
705
|
startTime: number;
|
|
@@ -756,11 +760,11 @@ export async function recordSessionRegistry(update: SessionRegistryUpdate): Prom
|
|
|
756
760
|
const now = update.updatedAt ?? Date.now();
|
|
757
761
|
|
|
758
762
|
data[update.chatId] = {
|
|
759
|
-
chatId: update.chatId,
|
|
760
|
-
sessionId: update.sessionId,
|
|
761
|
-
tool: update.tool,
|
|
762
|
-
chatType: update.chatType ?? existing?.chatType,
|
|
763
|
-
chatName: update.chatName ?? existing?.chatName ?? "",
|
|
763
|
+
chatId: update.chatId,
|
|
764
|
+
sessionId: update.sessionId,
|
|
765
|
+
tool: update.tool,
|
|
766
|
+
chatType: update.chatType ?? existing?.chatType,
|
|
767
|
+
chatName: update.chatName ?? existing?.chatName ?? "",
|
|
764
768
|
turnCount: update.turnCount ?? existing?.turnCount ?? 0,
|
|
765
769
|
lastContextTokens: update.lastContextTokens ?? existing?.lastContextTokens ?? 0,
|
|
766
770
|
startTime: update.startTime ?? existing?.startTime ?? now,
|
|
@@ -1009,12 +1013,12 @@ export async function switchChatBinding(args: SwitchChatBindingArgs): Promise<Sw
|
|
|
1009
1013
|
|
|
1010
1014
|
// Step 3: 持久化(registry + sessions.json)。
|
|
1011
1015
|
// 这两步即使失败也不影响内存正确性,下次 prompt 会再写一次。
|
|
1012
|
-
await recordSessionRegistry({
|
|
1013
|
-
chatId,
|
|
1014
|
-
sessionId: newSessionId,
|
|
1015
|
-
tool,
|
|
1016
|
-
chatType,
|
|
1017
|
-
chatName,
|
|
1016
|
+
await recordSessionRegistry({
|
|
1017
|
+
chatId,
|
|
1018
|
+
sessionId: newSessionId,
|
|
1019
|
+
tool,
|
|
1020
|
+
chatType,
|
|
1021
|
+
chatName,
|
|
1018
1022
|
turnCount: initialTurnCount,
|
|
1019
1023
|
lastContextTokens: initialContextTokens,
|
|
1020
1024
|
startTime: now,
|
|
@@ -1037,58 +1041,58 @@ export async function switchChatBinding(args: SwitchChatBindingArgs): Promise<Sw
|
|
|
1037
1041
|
function formatToolConfigForLog(tool: string, sessionModel?: string, sessionId?: string): string {
|
|
1038
1042
|
if (tool === "cursor") {
|
|
1039
1043
|
return `model=${sessionModel ?? "(由 cursor-agent 决定,init 事件后学习)"}`;
|
|
1040
|
-
}
|
|
1041
|
-
if (tool === "codex") {
|
|
1042
|
-
const m = getEffectiveModelForTool(tool, sessionId);
|
|
1043
|
-
const e = getEffectiveEffortForTool(tool, sessionId);
|
|
1044
|
-
const modelStr = m.trim() !== "" ? m : "(由 codex config.toml 决定)";
|
|
1045
|
-
const effortStr = e.trim() !== ""
|
|
1046
|
-
? `effort=${e}`
|
|
1047
|
-
: "effort=(由 codex config.toml 决定)";
|
|
1048
|
-
return `model=${modelStr}, ${effortStr}, fast=${getEffectiveFastModeForTool(tool, sessionId) ? "on" : "off"}`;
|
|
1049
|
-
}
|
|
1050
|
-
if (tool === "ccc") {
|
|
1051
|
-
const m = getEffectiveModelForTool(tool, sessionId);
|
|
1052
|
-
const modelStr = m.trim() !== "" ? m : "(not configured)";
|
|
1053
|
-
return `model=${modelStr}, baseURL=${config.ccc.DEEPSEEK_BASE_URL}`;
|
|
1054
|
-
}
|
|
1055
|
-
return `model=${anthropicConfigDisplay(getModelForSession(sessionId))}, subagentModel=${anthropicConfigDisplay(CLAUDE_SUBAGENT_MODEL)}, effort=${anthropicConfigDisplay(getEffectiveEffortForTool(tool, sessionId))}`;
|
|
1056
|
-
}
|
|
1057
|
-
|
|
1058
|
-
export async function initClaudeSession(tool: string, overrideCwd?: string, chatId?: string): Promise<{ sessionId: string; cwd: string }> {
|
|
1059
|
-
const cwd = overrideCwd ?? (await getDefaultCwd(chatId));
|
|
1060
|
-
const adapter = getAdapterForTool(tool);
|
|
1044
|
+
}
|
|
1045
|
+
if (tool === "codex") {
|
|
1046
|
+
const m = getEffectiveModelForTool(tool, sessionId);
|
|
1047
|
+
const e = getEffectiveEffortForTool(tool, sessionId);
|
|
1048
|
+
const modelStr = m.trim() !== "" ? m : "(由 codex config.toml 决定)";
|
|
1049
|
+
const effortStr = e.trim() !== ""
|
|
1050
|
+
? `effort=${e}`
|
|
1051
|
+
: "effort=(由 codex config.toml 决定)";
|
|
1052
|
+
return `model=${modelStr}, ${effortStr}, fast=${getEffectiveFastModeForTool(tool, sessionId) ? "on" : "off"}`;
|
|
1053
|
+
}
|
|
1054
|
+
if (tool === "ccc") {
|
|
1055
|
+
const m = getEffectiveModelForTool(tool, sessionId);
|
|
1056
|
+
const modelStr = m.trim() !== "" ? m : "(not configured)";
|
|
1057
|
+
return `model=${modelStr}, baseURL=${config.ccc.DEEPSEEK_BASE_URL}`;
|
|
1058
|
+
}
|
|
1059
|
+
return `model=${anthropicConfigDisplay(getModelForSession(sessionId))}, subagentModel=${anthropicConfigDisplay(CLAUDE_SUBAGENT_MODEL)}, effort=${anthropicConfigDisplay(getEffectiveEffortForTool(tool, sessionId))}`;
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
export async function initClaudeSession(tool: string, overrideCwd?: string, chatId?: string): Promise<{ sessionId: string; cwd: string }> {
|
|
1063
|
+
const cwd = overrideCwd ?? (await getDefaultCwd(chatId));
|
|
1064
|
+
const adapter = getAdapterForTool(tool);
|
|
1061
1065
|
console.log(
|
|
1062
|
-
`[${ts()}] [STEP 1/5] Creating ${adapter.displayName} session (${formatToolConfigForLog(tool)}, cwd=${cwd})`
|
|
1063
|
-
);
|
|
1064
|
-
|
|
1065
|
-
// Claude/Cursor 创建会话时需要先等待 SDK/CLI 的 init 事件。它们若在首个
|
|
1066
|
-
// 事件前卡死,正式 turn 尚未建立,runAgentSession 的看门狗无法介入。
|
|
1067
|
-
// 因此创建入口也使用相同的三分钟阈值,并通过 AbortSignal 释放底层资源。
|
|
1068
|
-
const createController = new AbortController();
|
|
1069
|
-
let createTimeout: ReturnType<typeof setTimeout> | undefined;
|
|
1070
|
-
const timeoutError = new Error(
|
|
1071
|
-
`${adapter.displayName} session creation timed out after 3 minutes without an init event`,
|
|
1072
|
-
);
|
|
1073
|
-
const timeoutPromise = new Promise<never>((_resolve, reject) => {
|
|
1074
|
-
createTimeout = setTimeout(() => {
|
|
1075
|
-
// 先固定对外错误,再 abort 适配器,避免适配器自己的 abort 错误赢得竞态。
|
|
1076
|
-
reject(timeoutError);
|
|
1077
|
-
createController.abort();
|
|
1078
|
-
}, responseStallTimeoutMs);
|
|
1079
|
-
createTimeout.unref?.();
|
|
1080
|
-
});
|
|
1081
|
-
|
|
1082
|
-
let result: Awaited<ReturnType<ToolAdapter["createSession"]>>;
|
|
1083
|
-
try {
|
|
1084
|
-
result = await Promise.race([
|
|
1085
|
-
adapter.createSession(cwd, createController.signal),
|
|
1086
|
-
timeoutPromise,
|
|
1087
|
-
]);
|
|
1088
|
-
} finally {
|
|
1089
|
-
if (createTimeout) clearTimeout(createTimeout);
|
|
1090
|
-
}
|
|
1091
|
-
const sessionId = result.sessionId;
|
|
1066
|
+
`[${ts()}] [STEP 1/5] Creating ${adapter.displayName} session (${formatToolConfigForLog(tool)}, cwd=${cwd})`
|
|
1067
|
+
);
|
|
1068
|
+
|
|
1069
|
+
// Claude/Cursor 创建会话时需要先等待 SDK/CLI 的 init 事件。它们若在首个
|
|
1070
|
+
// 事件前卡死,正式 turn 尚未建立,runAgentSession 的看门狗无法介入。
|
|
1071
|
+
// 因此创建入口也使用相同的三分钟阈值,并通过 AbortSignal 释放底层资源。
|
|
1072
|
+
const createController = new AbortController();
|
|
1073
|
+
let createTimeout: ReturnType<typeof setTimeout> | undefined;
|
|
1074
|
+
const timeoutError = new Error(
|
|
1075
|
+
`${adapter.displayName} session creation timed out after 3 minutes without an init event`,
|
|
1076
|
+
);
|
|
1077
|
+
const timeoutPromise = new Promise<never>((_resolve, reject) => {
|
|
1078
|
+
createTimeout = setTimeout(() => {
|
|
1079
|
+
// 先固定对外错误,再 abort 适配器,避免适配器自己的 abort 错误赢得竞态。
|
|
1080
|
+
reject(timeoutError);
|
|
1081
|
+
createController.abort();
|
|
1082
|
+
}, responseStallTimeoutMs);
|
|
1083
|
+
createTimeout.unref?.();
|
|
1084
|
+
});
|
|
1085
|
+
|
|
1086
|
+
let result: Awaited<ReturnType<ToolAdapter["createSession"]>>;
|
|
1087
|
+
try {
|
|
1088
|
+
result = await Promise.race([
|
|
1089
|
+
adapter.createSession(cwd, createController.signal),
|
|
1090
|
+
timeoutPromise,
|
|
1091
|
+
]);
|
|
1092
|
+
} finally {
|
|
1093
|
+
if (createTimeout) clearTimeout(createTimeout);
|
|
1094
|
+
}
|
|
1095
|
+
const sessionId = result.sessionId;
|
|
1092
1096
|
console.log(`[${ts()}] → sessionId: ${sessionId}`);
|
|
1093
1097
|
|
|
1094
1098
|
await saveSessionTool(sessionId, tool);
|
|
@@ -1110,41 +1114,41 @@ export async function resumeAndPrompt(
|
|
|
1110
1114
|
return runAgentSession(sessionId, userText, platform, chatId, msgTimestamp, tool, traceId);
|
|
1111
1115
|
}
|
|
1112
1116
|
|
|
1113
|
-
// ---------------------------------------------------------------------------
|
|
1114
|
-
// runAgentSession — session 中心的 agent prompt(文件持久化 + display 解耦)
|
|
1115
|
-
// ---------------------------------------------------------------------------
|
|
1116
|
-
|
|
1117
|
-
interface RunAgentSessionOptions {
|
|
1118
|
-
/**
|
|
1119
|
-
* 标记本轮是 response-stall 后的唯一一次内部续跑。若本轮再次因相同原因
|
|
1120
|
-
* 停滞,不再递归创建第三轮,避免服务异常期间无限消耗 token。
|
|
1121
|
-
*/
|
|
1122
|
-
autoRecovery?: boolean;
|
|
1123
|
-
}
|
|
1124
|
-
|
|
1125
|
-
export async function runAgentSession(
|
|
1126
|
-
sessionId: string,
|
|
1127
|
-
userText: string,
|
|
1128
|
-
platform: PlatformAdapter,
|
|
1129
|
-
_chatId: string,
|
|
1130
|
-
msgTimestamp: number,
|
|
1131
|
-
tool: string,
|
|
1132
|
-
traceId?: string,
|
|
1133
|
-
options: RunAgentSessionOptions = {},
|
|
1134
|
-
): Promise<void> {
|
|
1135
|
-
const tid = traceId ?? "";
|
|
1136
|
-
|
|
1137
|
-
// runAgentSession 是飞书用户消息、队列消息与内部自动恢复共同使用的唯一
|
|
1138
|
-
// prompt 执行入口。即使冷启动后的历史群只靠群描述解析出 sessionId、registry
|
|
1139
|
-
// 尚未重建出内存映射,也必须在任何异步操作前补齐绑定,确保三种来源都有完全
|
|
1140
|
-
// 相同的卡片、状态和收尾行为。
|
|
1141
|
-
const previousSessionId = sessionInfoMap.get(_chatId)?.sessionId;
|
|
1142
|
-
if (previousSessionId && previousSessionId !== sessionId) {
|
|
1143
|
-
unbindChatFromSession(previousSessionId, _chatId);
|
|
1144
|
-
}
|
|
1145
|
-
bindChatToSession(sessionId, _chatId);
|
|
1146
|
-
|
|
1147
|
-
// 记录用户最后发送消息的群(display loop 只推送到该群)
|
|
1117
|
+
// ---------------------------------------------------------------------------
|
|
1118
|
+
// runAgentSession — session 中心的 agent prompt(文件持久化 + display 解耦)
|
|
1119
|
+
// ---------------------------------------------------------------------------
|
|
1120
|
+
|
|
1121
|
+
interface RunAgentSessionOptions {
|
|
1122
|
+
/**
|
|
1123
|
+
* 标记本轮是 response-stall 后的唯一一次内部续跑。若本轮再次因相同原因
|
|
1124
|
+
* 停滞,不再递归创建第三轮,避免服务异常期间无限消耗 token。
|
|
1125
|
+
*/
|
|
1126
|
+
autoRecovery?: boolean;
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
export async function runAgentSession(
|
|
1130
|
+
sessionId: string,
|
|
1131
|
+
userText: string,
|
|
1132
|
+
platform: PlatformAdapter,
|
|
1133
|
+
_chatId: string,
|
|
1134
|
+
msgTimestamp: number,
|
|
1135
|
+
tool: string,
|
|
1136
|
+
traceId?: string,
|
|
1137
|
+
options: RunAgentSessionOptions = {},
|
|
1138
|
+
): Promise<void> {
|
|
1139
|
+
const tid = traceId ?? "";
|
|
1140
|
+
|
|
1141
|
+
// runAgentSession 是飞书用户消息、队列消息与内部自动恢复共同使用的唯一
|
|
1142
|
+
// prompt 执行入口。即使冷启动后的历史群只靠群描述解析出 sessionId、registry
|
|
1143
|
+
// 尚未重建出内存映射,也必须在任何异步操作前补齐绑定,确保三种来源都有完全
|
|
1144
|
+
// 相同的卡片、状态和收尾行为。
|
|
1145
|
+
const previousSessionId = sessionInfoMap.get(_chatId)?.sessionId;
|
|
1146
|
+
if (previousSessionId && previousSessionId !== sessionId) {
|
|
1147
|
+
unbindChatFromSession(previousSessionId, _chatId);
|
|
1148
|
+
}
|
|
1149
|
+
bindChatToSession(sessionId, _chatId);
|
|
1150
|
+
|
|
1151
|
+
// 记录用户最后发送消息的群(display loop 只推送到该群)
|
|
1148
1152
|
// 如果是从队列消费且队列消息来自其他群,保留原来的 display chat
|
|
1149
1153
|
recordChatPlatform(_chatId, platform);
|
|
1150
1154
|
recordLastActiveChat(sessionId, consumeQueuePreservedChat(sessionId) ?? _chatId);
|
|
@@ -1165,19 +1169,19 @@ export async function runAgentSession(
|
|
|
1165
1169
|
// 注意:下面的 try/catch 在准备失败时会清理 activePrompts。
|
|
1166
1170
|
const controller = new AbortController();
|
|
1167
1171
|
const now = Date.now();
|
|
1168
|
-
activePrompts.set(sessionId, {
|
|
1169
|
-
controller,
|
|
1170
|
-
stopped: false,
|
|
1171
|
-
startTime: now,
|
|
1172
|
-
autoRecovery: options.autoRecovery === true,
|
|
1173
|
-
finalResponseObserved: false,
|
|
1174
|
-
});
|
|
1172
|
+
activePrompts.set(sessionId, {
|
|
1173
|
+
controller,
|
|
1174
|
+
stopped: false,
|
|
1175
|
+
startTime: now,
|
|
1176
|
+
autoRecovery: options.autoRecovery === true,
|
|
1177
|
+
finalResponseObserved: false,
|
|
1178
|
+
});
|
|
1175
1179
|
|
|
1176
1180
|
// 资源监控僵死检测:CPU + 内存连续 3 分钟无变化 → 强制停止
|
|
1177
1181
|
const onResourceStuck = (data: { pid: number; sessionId: string; idleMinutes: number }) => {
|
|
1178
1182
|
if (data.sessionId !== sessionId) return;
|
|
1179
1183
|
const prompt = activePrompts.get(sessionId);
|
|
1180
|
-
if (!prompt || prompt.stopped || prompt.abnormalExit || prompt.resourceStuck || prompt.autoEnded) return;
|
|
1184
|
+
if (!prompt || prompt.stopped || prompt.abnormalExit || prompt.resourceStuck || prompt.autoEnded) return;
|
|
1181
1185
|
prompt.resourceStuck = true;
|
|
1182
1186
|
|
|
1183
1187
|
const chatId = pickDisplayChat(sessionId) ?? getLastActiveChat(sessionId) ?? getChatsForSession(sessionId)[0];
|
|
@@ -1214,12 +1218,12 @@ export async function runAgentSession(
|
|
|
1214
1218
|
const imSkillsCacheDir = join(USER_DATA_DIR, "im-skills");
|
|
1215
1219
|
const skillVariables = {
|
|
1216
1220
|
cwd,
|
|
1217
|
-
session_id: sessionId,
|
|
1218
|
-
im_skills_cache_dir: imSkillsCacheDir,
|
|
1219
|
-
delegate_task_url: `http://127.0.0.1:${CHATCCC_PORT}/api/agent/delegate-task`,
|
|
1220
|
-
send_image_url: `http://127.0.0.1:${CHATCCC_PORT}/api/agent/send-image`,
|
|
1221
|
-
send_file_url: `http://127.0.0.1:${CHATCCC_PORT}/api/agent/send-file`,
|
|
1222
|
-
send_image_script: join(feishuSkillDir, "send-image.mjs"),
|
|
1221
|
+
session_id: sessionId,
|
|
1222
|
+
im_skills_cache_dir: imSkillsCacheDir,
|
|
1223
|
+
delegate_task_url: `http://127.0.0.1:${CHATCCC_PORT}/api/agent/delegate-task`,
|
|
1224
|
+
send_image_url: `http://127.0.0.1:${CHATCCC_PORT}/api/agent/send-image`,
|
|
1225
|
+
send_file_url: `http://127.0.0.1:${CHATCCC_PORT}/api/agent/send-file`,
|
|
1226
|
+
send_image_script: join(feishuSkillDir, "send-image.mjs"),
|
|
1223
1227
|
send_file_script: join(feishuSkillDir, "send-file.mjs"),
|
|
1224
1228
|
download_video_script: join(feishuSkillDir, "download-video.mjs"),
|
|
1225
1229
|
wechat_send_image_script: join(wechatImageSkillDir, "send-image.mjs"),
|
|
@@ -1284,10 +1288,10 @@ export async function runAgentSession(
|
|
|
1284
1288
|
// 导致 finalReply 丢失、完成卡片空白。此处主动读取上一轮终端状态完成
|
|
1285
1289
|
// 卡片终结和回复发送,不依赖 display loop 时序,保证"先发完上一个回答
|
|
1286
1290
|
// 再开始缓存问题对应的任务"。
|
|
1287
|
-
const prevState = await readStreamState(sessionId);
|
|
1288
|
-
if (prevState && prevState.status !== "running") {
|
|
1289
|
-
const prevTerminalReply = formatTerminalReply(prevState.status, prevState.finalReply);
|
|
1290
|
-
const displayChatId = pickDisplayChat(sessionId);
|
|
1291
|
+
const prevState = await readStreamState(sessionId);
|
|
1292
|
+
if (prevState && prevState.status !== "running") {
|
|
1293
|
+
const prevTerminalReply = formatTerminalReply(prevState.status, prevState.finalReply);
|
|
1294
|
+
const displayChatId = pickDisplayChat(sessionId);
|
|
1291
1295
|
if (displayChatId) {
|
|
1292
1296
|
const pp = platformForChat(displayChatId);
|
|
1293
1297
|
const display = displayCards.get(displayChatId);
|
|
@@ -1302,12 +1306,12 @@ export async function runAgentSession(
|
|
|
1302
1306
|
if (displayCards.get(displayChatId) !== display) {
|
|
1303
1307
|
const finalStatus = turnFinalStatus(prevState.status);
|
|
1304
1308
|
finalizeTurnCards(sessionId, prevState.turnCount, finalStatus).catch(() => {});
|
|
1305
|
-
setSessionChatAvatar(pp, displayChatId, prevState.tool, "idle", sessionId).catch(() => {});
|
|
1309
|
+
setSessionChatAvatar(pp, displayChatId, prevState.tool, "idle", sessionId).catch(() => {});
|
|
1306
1310
|
} else {
|
|
1307
1311
|
const nextSeq = display.sequence + 1;
|
|
1308
1312
|
const { title: headerTitle, template: headerTemplate } = formatTerminalHeader(prevState.status);
|
|
1309
1313
|
const cardContent = truncateContent(prevState.accumulatedContent + prevState.finalReply) || " ";
|
|
1310
|
-
const doneCard = buildProgressCard(cardContent,
|
|
1314
|
+
const doneCard = buildProgressCard(progressView({ text: cardContent, status: "done", showStop: false, headerTitle, headerTemplate }));
|
|
1311
1315
|
await pp.cardUpdate(display.cardId, doneCard, nextSeq).catch(err => {
|
|
1312
1316
|
console.error(`[${ts()}] [DISPLAY] prevState final cardUpdate failed: ${(err as Error).message}`);
|
|
1313
1317
|
});
|
|
@@ -1319,49 +1323,49 @@ export async function runAgentSession(
|
|
|
1319
1323
|
const finalStatus = turnFinalStatus(prevState.status);
|
|
1320
1324
|
finalizeTurnCards(sessionId, prevState.turnCount, finalStatus).catch(() => {});
|
|
1321
1325
|
|
|
1322
|
-
if (prevTerminalReply && stillOursAfterUpdate && !isFinalReplySentForTurn(prevState)) {
|
|
1323
|
-
await sendFinalReplyTextOnce(pp, displayChatId, sessionId, prevState.turnCount, prevTerminalReply);
|
|
1324
|
-
}
|
|
1325
|
-
setSessionChatAvatar(pp, displayChatId, prevState.tool, "idle", sessionId).catch(() => {});
|
|
1326
|
+
if (prevTerminalReply && stillOursAfterUpdate && !isFinalReplySentForTurn(prevState)) {
|
|
1327
|
+
await sendFinalReplyTextOnce(pp, displayChatId, sessionId, prevState.turnCount, prevTerminalReply);
|
|
1328
|
+
}
|
|
1329
|
+
setSessionChatAvatar(pp, displayChatId, prevState.tool, "idle", sessionId).catch(() => {});
|
|
1326
1330
|
}
|
|
1327
|
-
} else if (pp && prevTerminalReply && !isFinalReplySentForTurn(prevState)) {
|
|
1331
|
+
} else if (pp && prevTerminalReply && !isFinalReplySentForTurn(prevState)) {
|
|
1328
1332
|
// 无 display 记录但上一轮有 finalReply(极快轮次),至少发送
|
|
1329
1333
|
const finalStatus = turnFinalStatus(prevState.status);
|
|
1330
1334
|
finalizeTurnCards(sessionId, prevState.turnCount, finalStatus).catch(() => {});
|
|
1331
|
-
await sendFinalReplyTextOnce(pp, displayChatId, sessionId, prevState.turnCount, prevTerminalReply);
|
|
1335
|
+
await sendFinalReplyTextOnce(pp, displayChatId, sessionId, prevState.turnCount, prevTerminalReply);
|
|
1332
1336
|
}
|
|
1333
1337
|
// else: displayCards 无记录且无 finalReply → 无需处理
|
|
1334
1338
|
}
|
|
1335
1339
|
}
|
|
1336
1340
|
|
|
1337
1341
|
// 初始化 stream-state.json
|
|
1338
|
-
const initialState = createEmptyStreamState(sessionId, cwd, tool, nextTurnCount);
|
|
1339
|
-
const activityTracker = createAgentActivityTracker(initialState.activity?.startedAt ?? Date.now());
|
|
1340
|
-
await writeStreamState(initialState);
|
|
1342
|
+
const initialState = createEmptyStreamState(sessionId, cwd, tool, nextTurnCount);
|
|
1343
|
+
const activityTracker = createAgentActivityTracker(initialState.activity?.startedAt ?? Date.now());
|
|
1344
|
+
await writeStreamState(initialState);
|
|
1341
1345
|
|
|
1342
1346
|
// 为新 turn 创建第一张展示卡片,同时注册到 turn-cards 持久化。
|
|
1343
1347
|
// 统一 display loop 始终运行,卡片创建后下一个 tick 即自动开始更新。
|
|
1344
1348
|
const displayChatIdForNew = pickDisplayChat(sessionId);
|
|
1345
|
-
if (displayChatIdForNew) {
|
|
1346
|
-
const ppNew = platformForChat(displayChatIdForNew);
|
|
1347
|
-
if (ppNew && ppNew.kind !== "wechat") {
|
|
1348
|
-
const initialHeaderTitle = formatAgentActivityTitle(activityTracker.activity);
|
|
1349
|
-
const cardId = await createVisibleProgressCard(
|
|
1349
|
+
if (displayChatIdForNew) {
|
|
1350
|
+
const ppNew = platformForChat(displayChatIdForNew);
|
|
1351
|
+
if (ppNew && ppNew.kind !== "wechat") {
|
|
1352
|
+
const initialHeaderTitle = formatAgentActivityTitle(activityTracker.activity);
|
|
1353
|
+
const cardId = await createVisibleProgressCard(
|
|
1350
1354
|
ppNew,
|
|
1351
1355
|
displayChatIdForNew,
|
|
1352
1356
|
sessionId,
|
|
1353
|
-
nextTurnCount,
|
|
1354
|
-
"生成中卡片发送失败,结果将以文本形式发送。",
|
|
1355
|
-
initialHeaderTitle,
|
|
1357
|
+
nextTurnCount,
|
|
1358
|
+
"生成中卡片发送失败,结果将以文本形式发送。",
|
|
1359
|
+
initialHeaderTitle,
|
|
1356
1360
|
);
|
|
1357
1361
|
if (cardId) {
|
|
1358
1362
|
displayCards.set(displayChatIdForNew, {
|
|
1359
1363
|
cardId,
|
|
1360
1364
|
sequence: 1,
|
|
1361
1365
|
cardBusy: false,
|
|
1362
|
-
cardCreatedAt: Date.now(),
|
|
1363
|
-
lastSentContent: "",
|
|
1364
|
-
lastSentHeaderTitle: initialHeaderTitle,
|
|
1366
|
+
cardCreatedAt: Date.now(),
|
|
1367
|
+
lastSentContent: "",
|
|
1368
|
+
lastSentHeaderTitle: initialHeaderTitle,
|
|
1365
1369
|
streamErrorNotified: false,
|
|
1366
1370
|
sessionId,
|
|
1367
1371
|
turnCount: nextTurnCount,
|
|
@@ -1387,7 +1391,7 @@ export async function runAgentSession(
|
|
|
1387
1391
|
// 设置最后活跃群头像为 busy
|
|
1388
1392
|
const activeCid = getLastActiveChat(sessionId) ?? getChatsForSession(sessionId)[0];
|
|
1389
1393
|
if (activeCid) {
|
|
1390
|
-
setSessionChatAvatar(platform, activeCid, tool, "busy", sessionId).catch(() => {});
|
|
1394
|
+
setSessionChatAvatar(platform, activeCid, tool, "busy", sessionId).catch(() => {});
|
|
1391
1395
|
}
|
|
1392
1396
|
|
|
1393
1397
|
const state: AccumulatorState = {
|
|
@@ -1397,106 +1401,106 @@ export async function runAgentSession(
|
|
|
1397
1401
|
chunkCount: 0,
|
|
1398
1402
|
};
|
|
1399
1403
|
|
|
1400
|
-
let lastFileWrite = Date.now();
|
|
1401
|
-
const FILE_WRITE_INTERVAL_MS = 2000;
|
|
1402
|
-
const toolCallMap = new Map<string, { name: string; input: unknown }>();
|
|
1403
|
-
let streamErrored = false;
|
|
1404
|
-
|
|
1405
|
-
const runningPrompt = activePrompts.get(sessionId);
|
|
1406
|
-
if (runningPrompt) {
|
|
1407
|
-
// 必须在消费第一个事件前建立零字符基线。部分 CLI 卡死时只启动了进程,
|
|
1408
|
-
// 甚至一个事件都不会 yield;若等循环体更新进度,这种会话会永久停在
|
|
1409
|
-
// “正在启动 Agent”,也永远触发不了三分钟保护。
|
|
1410
|
-
runningPrompt.responseProgress = observeResponseProgress(
|
|
1411
|
-
undefined,
|
|
1412
|
-
true,
|
|
1413
|
-
0,
|
|
1414
|
-
activityTracker.activity.startedAt,
|
|
1415
|
-
);
|
|
1416
|
-
|
|
1417
|
-
const checkResponseStall = async () => {
|
|
1418
|
-
const current = activePrompts.get(sessionId);
|
|
1419
|
-
if (!current || current !== runningPrompt) {
|
|
1420
|
-
clearPromptResponseStallMonitor(sessionId);
|
|
1421
|
-
return;
|
|
1422
|
-
}
|
|
1423
|
-
if (
|
|
1424
|
-
current.stopped
|
|
1425
|
-
|| current.abnormalExit
|
|
1426
|
-
|| current.resourceStuck
|
|
1427
|
-
|| current.autoEnded
|
|
1428
|
-
|| current.finalResponseObserved
|
|
1429
|
-
|| !monitorsOutputProgress(activityTracker.activity.kind)
|
|
1430
|
-
|| !hasResponseStalled(current.responseProgress, Date.now(), responseStallTimeoutMs)
|
|
1431
|
-
) {
|
|
1432
|
-
return;
|
|
1433
|
-
}
|
|
1434
|
-
|
|
1435
|
-
const autoEndedAt = Date.now();
|
|
1436
|
-
// 普通轮第一次因回复停滞结束时,立即预约同 session 的内部续跑。
|
|
1437
|
-
// 预约先于 abort/收尾建立,isSessionRunning 会在整个交接窗口保持 true,
|
|
1438
|
-
// 因此恰好到达的用户消息只能排队,绝不可能抢在恢复 prompt 前。
|
|
1439
|
-
// 若当前已经是恢复轮,则不再预约第三轮。
|
|
1440
|
-
if (!current.autoRecovery) {
|
|
1441
|
-
reserveAutoRecovery(sessionId);
|
|
1442
|
-
}
|
|
1443
|
-
current.autoEnded = true;
|
|
1444
|
-
current.autoEndedAt = autoEndedAt;
|
|
1445
|
-
clearPromptResponseStallMonitor(sessionId);
|
|
1446
|
-
clearPromptProcessMonitor(sessionId);
|
|
1447
|
-
|
|
1448
|
-
// First publish an atomic terminal state so the card cannot keep claiming the
|
|
1449
|
-
// Agent is running while process cleanup is underway.
|
|
1450
|
-
await writeStreamState({
|
|
1451
|
-
sessionId,
|
|
1452
|
-
status: "auto_ended",
|
|
1453
|
-
accumulatedContent: state.accumulatedContent,
|
|
1454
|
-
finalReply: pickFinalReply(state).trim(),
|
|
1455
|
-
activity: activityTracker.activity,
|
|
1456
|
-
chunkCount: state.chunkCount,
|
|
1457
|
-
turnCount: nextTurnCount,
|
|
1458
|
-
contextTokens: existingInfo?.lastContextTokens ?? 0,
|
|
1459
|
-
updatedAt: autoEndedAt,
|
|
1460
|
-
cwd,
|
|
1461
|
-
tool,
|
|
1462
|
-
autoEndedAt,
|
|
1463
|
-
});
|
|
1464
|
-
|
|
1465
|
-
// 最终事件可能在上面的落盘 I/O 期间到达。只有适配器明确标记的完整
|
|
1466
|
-
// final response 才能赢得这场竞态;普通文本片段绝不能取消超时。
|
|
1467
|
-
if (current.finalResponseObserved) {
|
|
1468
|
-
current.autoEnded = false;
|
|
1469
|
-
current.autoEndedAt = undefined;
|
|
1470
|
-
cancelAutoRecoveryReservation(sessionId);
|
|
1471
|
-
console.log(
|
|
1472
|
-
`[${ts()}] [RESPONSE-STALL] Authoritative final response won timeout race for ${sessionId}`,
|
|
1473
|
-
);
|
|
1474
|
-
return;
|
|
1475
|
-
}
|
|
1476
|
-
|
|
1477
|
-
try {
|
|
1478
|
-
current.closeSession?.();
|
|
1479
|
-
} catch (err) {
|
|
1480
|
-
console.warn(`[${ts()}] [RESPONSE-STALL] closeSession failed for ${sessionId}: ${(err as Error).message}`);
|
|
1481
|
-
}
|
|
1482
|
-
current.controller.abort();
|
|
1483
|
-
await killProcessTree(current.processPid);
|
|
1484
|
-
console.warn(
|
|
1485
|
-
`[${ts()}] [RESPONSE-STALL] Session ${sessionId} auto-ended after 3 minutes without startup or reply progress`,
|
|
1486
|
-
);
|
|
1487
|
-
};
|
|
1488
|
-
|
|
1489
|
-
const responseStallMonitor = setInterval(() => {
|
|
1490
|
-
void checkResponseStall().catch((err) => {
|
|
1491
|
-
console.warn(`[${ts()}] [RESPONSE-STALL] check failed for ${sessionId}: ${(err as Error).message}`);
|
|
1492
|
-
});
|
|
1493
|
-
}, responseStallCheckIntervalMs);
|
|
1494
|
-
responseStallMonitor.unref?.();
|
|
1495
|
-
runningPrompt.responseStallMonitor = responseStallMonitor;
|
|
1496
|
-
}
|
|
1497
|
-
|
|
1498
|
-
try {
|
|
1499
|
-
for await (const unifiedMsg of adapter.prompt(sessionId, userTextWithCapabilities, cwd, controller.signal, {
|
|
1404
|
+
let lastFileWrite = Date.now();
|
|
1405
|
+
const FILE_WRITE_INTERVAL_MS = 2000;
|
|
1406
|
+
const toolCallMap = new Map<string, { name: string; input: unknown }>();
|
|
1407
|
+
let streamErrored = false;
|
|
1408
|
+
|
|
1409
|
+
const runningPrompt = activePrompts.get(sessionId);
|
|
1410
|
+
if (runningPrompt) {
|
|
1411
|
+
// 必须在消费第一个事件前建立零字符基线。部分 CLI 卡死时只启动了进程,
|
|
1412
|
+
// 甚至一个事件都不会 yield;若等循环体更新进度,这种会话会永久停在
|
|
1413
|
+
// “正在启动 Agent”,也永远触发不了三分钟保护。
|
|
1414
|
+
runningPrompt.responseProgress = observeResponseProgress(
|
|
1415
|
+
undefined,
|
|
1416
|
+
true,
|
|
1417
|
+
0,
|
|
1418
|
+
activityTracker.activity.startedAt,
|
|
1419
|
+
);
|
|
1420
|
+
|
|
1421
|
+
const checkResponseStall = async () => {
|
|
1422
|
+
const current = activePrompts.get(sessionId);
|
|
1423
|
+
if (!current || current !== runningPrompt) {
|
|
1424
|
+
clearPromptResponseStallMonitor(sessionId);
|
|
1425
|
+
return;
|
|
1426
|
+
}
|
|
1427
|
+
if (
|
|
1428
|
+
current.stopped
|
|
1429
|
+
|| current.abnormalExit
|
|
1430
|
+
|| current.resourceStuck
|
|
1431
|
+
|| current.autoEnded
|
|
1432
|
+
|| current.finalResponseObserved
|
|
1433
|
+
|| !monitorsOutputProgress(activityTracker.activity.kind)
|
|
1434
|
+
|| !hasResponseStalled(current.responseProgress, Date.now(), responseStallTimeoutMs)
|
|
1435
|
+
) {
|
|
1436
|
+
return;
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
const autoEndedAt = Date.now();
|
|
1440
|
+
// 普通轮第一次因回复停滞结束时,立即预约同 session 的内部续跑。
|
|
1441
|
+
// 预约先于 abort/收尾建立,isSessionRunning 会在整个交接窗口保持 true,
|
|
1442
|
+
// 因此恰好到达的用户消息只能排队,绝不可能抢在恢复 prompt 前。
|
|
1443
|
+
// 若当前已经是恢复轮,则不再预约第三轮。
|
|
1444
|
+
if (!current.autoRecovery) {
|
|
1445
|
+
reserveAutoRecovery(sessionId);
|
|
1446
|
+
}
|
|
1447
|
+
current.autoEnded = true;
|
|
1448
|
+
current.autoEndedAt = autoEndedAt;
|
|
1449
|
+
clearPromptResponseStallMonitor(sessionId);
|
|
1450
|
+
clearPromptProcessMonitor(sessionId);
|
|
1451
|
+
|
|
1452
|
+
// First publish an atomic terminal state so the card cannot keep claiming the
|
|
1453
|
+
// Agent is running while process cleanup is underway.
|
|
1454
|
+
await writeStreamState({
|
|
1455
|
+
sessionId,
|
|
1456
|
+
status: "auto_ended",
|
|
1457
|
+
accumulatedContent: state.accumulatedContent,
|
|
1458
|
+
finalReply: pickFinalReply(state).trim(),
|
|
1459
|
+
activity: activityTracker.activity,
|
|
1460
|
+
chunkCount: state.chunkCount,
|
|
1461
|
+
turnCount: nextTurnCount,
|
|
1462
|
+
contextTokens: existingInfo?.lastContextTokens ?? 0,
|
|
1463
|
+
updatedAt: autoEndedAt,
|
|
1464
|
+
cwd,
|
|
1465
|
+
tool,
|
|
1466
|
+
autoEndedAt,
|
|
1467
|
+
});
|
|
1468
|
+
|
|
1469
|
+
// 最终事件可能在上面的落盘 I/O 期间到达。只有适配器明确标记的完整
|
|
1470
|
+
// final response 才能赢得这场竞态;普通文本片段绝不能取消超时。
|
|
1471
|
+
if (current.finalResponseObserved) {
|
|
1472
|
+
current.autoEnded = false;
|
|
1473
|
+
current.autoEndedAt = undefined;
|
|
1474
|
+
cancelAutoRecoveryReservation(sessionId);
|
|
1475
|
+
console.log(
|
|
1476
|
+
`[${ts()}] [RESPONSE-STALL] Authoritative final response won timeout race for ${sessionId}`,
|
|
1477
|
+
);
|
|
1478
|
+
return;
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
try {
|
|
1482
|
+
current.closeSession?.();
|
|
1483
|
+
} catch (err) {
|
|
1484
|
+
console.warn(`[${ts()}] [RESPONSE-STALL] closeSession failed for ${sessionId}: ${(err as Error).message}`);
|
|
1485
|
+
}
|
|
1486
|
+
current.controller.abort();
|
|
1487
|
+
await killProcessTree(current.processPid);
|
|
1488
|
+
console.warn(
|
|
1489
|
+
`[${ts()}] [RESPONSE-STALL] Session ${sessionId} auto-ended after 3 minutes without startup or reply progress`,
|
|
1490
|
+
);
|
|
1491
|
+
};
|
|
1492
|
+
|
|
1493
|
+
const responseStallMonitor = setInterval(() => {
|
|
1494
|
+
void checkResponseStall().catch((err) => {
|
|
1495
|
+
console.warn(`[${ts()}] [RESPONSE-STALL] check failed for ${sessionId}: ${(err as Error).message}`);
|
|
1496
|
+
});
|
|
1497
|
+
}, responseStallCheckIntervalMs);
|
|
1498
|
+
responseStallMonitor.unref?.();
|
|
1499
|
+
runningPrompt.responseStallMonitor = responseStallMonitor;
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
try {
|
|
1503
|
+
for await (const unifiedMsg of adapter.prompt(sessionId, userTextWithCapabilities, cwd, controller.signal, {
|
|
1500
1504
|
onProcessStart: (processInfo) => {
|
|
1501
1505
|
startPromptProcessMonitor(sessionId, processInfo);
|
|
1502
1506
|
if (processInfo.pid !== undefined) registerProcess(processInfo.pid, sessionId);
|
|
@@ -1505,27 +1509,27 @@ export async function runAgentSession(
|
|
|
1505
1509
|
clearPromptProcessMonitor(sessionId);
|
|
1506
1510
|
if (exitInfo.pid !== undefined) unregisterProcess(exitInfo.pid);
|
|
1507
1511
|
},
|
|
1508
|
-
onSessionCreated: (closeSession) => {
|
|
1509
|
-
const prompt = activePrompts.get(sessionId);
|
|
1510
|
-
if (prompt) prompt.closeSession = closeSession;
|
|
1511
|
-
},
|
|
1512
|
-
})) {
|
|
1513
|
-
if (unifiedMsg.isFinalResponse) {
|
|
1514
|
-
const prompt = activePrompts.get(sessionId);
|
|
1515
|
-
if (prompt && prompt === runningPrompt) {
|
|
1516
|
-
// 同步标记必须发生在任何 await 之前,让 watchdog 无法在已收到完整
|
|
1517
|
-
// 最终事件后仍把本轮判为停滞。
|
|
1518
|
-
if (!prompt.finalResponseObserved) {
|
|
1519
|
-
prompt.finalResponseObserved = true;
|
|
1520
|
-
scheduleFinalResponseCloseGuard(sessionId, prompt);
|
|
1521
|
-
}
|
|
1522
|
-
}
|
|
1523
|
-
}
|
|
1524
|
-
|
|
1525
|
-
let activityChanged = false;
|
|
1526
|
-
for (const block of unifiedMsg.blocks) {
|
|
1527
|
-
if (updateAgentActivity(activityTracker, block)) activityChanged = true;
|
|
1528
|
-
accumulateBlockContent(block, state, toolCallMap);
|
|
1512
|
+
onSessionCreated: (closeSession) => {
|
|
1513
|
+
const prompt = activePrompts.get(sessionId);
|
|
1514
|
+
if (prompt) prompt.closeSession = closeSession;
|
|
1515
|
+
},
|
|
1516
|
+
})) {
|
|
1517
|
+
if (unifiedMsg.isFinalResponse) {
|
|
1518
|
+
const prompt = activePrompts.get(sessionId);
|
|
1519
|
+
if (prompt && prompt === runningPrompt) {
|
|
1520
|
+
// 同步标记必须发生在任何 await 之前,让 watchdog 无法在已收到完整
|
|
1521
|
+
// 最终事件后仍把本轮判为停滞。
|
|
1522
|
+
if (!prompt.finalResponseObserved) {
|
|
1523
|
+
prompt.finalResponseObserved = true;
|
|
1524
|
+
scheduleFinalResponseCloseGuard(sessionId, prompt);
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
let activityChanged = false;
|
|
1530
|
+
for (const block of unifiedMsg.blocks) {
|
|
1531
|
+
if (updateAgentActivity(activityTracker, block)) activityChanged = true;
|
|
1532
|
+
accumulateBlockContent(block, state, toolCallMap);
|
|
1529
1533
|
|
|
1530
1534
|
if (block.type === "compact_boundary" && block.post_tokens) {
|
|
1531
1535
|
for (const cid of getChatsForSession(sessionId)) {
|
|
@@ -1539,32 +1543,32 @@ export async function runAgentSession(
|
|
|
1539
1543
|
lastContextTokens: block.post_tokens,
|
|
1540
1544
|
running: true,
|
|
1541
1545
|
});
|
|
1542
|
-
}
|
|
1543
|
-
}
|
|
1544
|
-
|
|
1545
|
-
const prompt = activePrompts.get(sessionId);
|
|
1546
|
-
if (prompt && !prompt.autoEnded) {
|
|
1547
|
-
const totalChars = state.accumulatedContent.length + pickFinalReply(state).length;
|
|
1548
|
-
prompt.responseProgress = observeResponseProgress(
|
|
1549
|
-
// starting → responding 本身是一次有效进展,即便首个文本块仍为空也应
|
|
1550
|
-
// 重新计时;其它活动阶段会清空观察窗口。
|
|
1551
|
-
activityChanged ? undefined : prompt.responseProgress,
|
|
1552
|
-
monitorsOutputProgress(activityTracker.activity.kind),
|
|
1553
|
-
totalChars,
|
|
1554
|
-
Date.now(),
|
|
1555
|
-
);
|
|
1556
|
-
}
|
|
1557
|
-
|
|
1558
|
-
// 定时写入文件
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
const prompt = activePrompts.get(sessionId);
|
|
1550
|
+
if (prompt && !prompt.autoEnded) {
|
|
1551
|
+
const totalChars = state.accumulatedContent.length + pickFinalReply(state).length;
|
|
1552
|
+
prompt.responseProgress = observeResponseProgress(
|
|
1553
|
+
// starting → responding 本身是一次有效进展,即便首个文本块仍为空也应
|
|
1554
|
+
// 重新计时;其它活动阶段会清空观察窗口。
|
|
1555
|
+
activityChanged ? undefined : prompt.responseProgress,
|
|
1556
|
+
monitorsOutputProgress(activityTracker.activity.kind),
|
|
1557
|
+
totalChars,
|
|
1558
|
+
Date.now(),
|
|
1559
|
+
);
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
// 定时写入文件
|
|
1559
1563
|
const now2 = Date.now();
|
|
1560
|
-
if (activityChanged || now2 - lastFileWrite >= FILE_WRITE_INTERVAL_MS) {
|
|
1561
|
-
lastFileWrite = now2;
|
|
1562
|
-
await writeStreamState({
|
|
1564
|
+
if (activityChanged || now2 - lastFileWrite >= FILE_WRITE_INTERVAL_MS) {
|
|
1565
|
+
lastFileWrite = now2;
|
|
1566
|
+
await writeStreamState({
|
|
1563
1567
|
sessionId,
|
|
1564
1568
|
status: "running",
|
|
1565
|
-
accumulatedContent: state.accumulatedContent,
|
|
1566
|
-
finalReply: pickFinalReply(state),
|
|
1567
|
-
activity: activityTracker.activity,
|
|
1569
|
+
accumulatedContent: state.accumulatedContent,
|
|
1570
|
+
finalReply: pickFinalReply(state),
|
|
1571
|
+
activity: activityTracker.activity,
|
|
1568
1572
|
chunkCount: state.chunkCount,
|
|
1569
1573
|
turnCount: nextTurnCount,
|
|
1570
1574
|
contextTokens: existingInfo?.lastContextTokens ?? 0,
|
|
@@ -1573,58 +1577,58 @@ export async function runAgentSession(
|
|
|
1573
1577
|
tool,
|
|
1574
1578
|
});
|
|
1575
1579
|
}
|
|
1576
|
-
}
|
|
1577
|
-
} catch (streamErr) {
|
|
1578
|
-
streamErrored = true;
|
|
1579
|
-
console.error(`[${ts()}] [STREAM] Error in stream loop for ${sessionId}: ${(streamErr as Error).message}`);
|
|
1580
|
-
} finally {
|
|
1580
|
+
}
|
|
1581
|
+
} catch (streamErr) {
|
|
1582
|
+
streamErrored = true;
|
|
1583
|
+
console.error(`[${ts()}] [STREAM] Error in stream loop for ${sessionId}: ${(streamErr as Error).message}`);
|
|
1584
|
+
} finally {
|
|
1581
1585
|
// 标记 prompt 结束
|
|
1582
1586
|
resourceMonitor.off("stuck", onResourceStuck);
|
|
1583
1587
|
const prompt = activePrompts.get(sessionId);
|
|
1584
|
-
const wasStopped = prompt?.stopped ?? false;
|
|
1585
|
-
const wasAbnormalExit = prompt?.abnormalExit ?? false;
|
|
1586
|
-
const wasResourceStuck = prompt?.resourceStuck ?? false;
|
|
1587
|
-
const timeoutTriggered = prompt?.autoEnded ?? false;
|
|
1588
|
-
const completedAtTimeoutBoundary =
|
|
1589
|
-
timeoutTriggered && (prompt?.finalResponseObserved ?? false);
|
|
1590
|
-
const wasAutoEnded = timeoutTriggered && !completedAtTimeoutBoundary;
|
|
1591
|
-
const wasAutoRecovery = prompt?.autoRecovery ?? false;
|
|
1592
|
-
const autoEndedAt = wasAutoEnded ? prompt?.autoEndedAt : undefined;
|
|
1593
|
-
clearPromptResponseStallMonitor(sessionId);
|
|
1594
|
-
clearPromptProcessMonitor(sessionId);
|
|
1595
|
-
clearPromptFinalResponseCloseTimer(sessionId);
|
|
1596
|
-
markSessionFinalizing(sessionId);
|
|
1597
|
-
activePrompts.delete(sessionId);
|
|
1598
|
-
|
|
1599
|
-
try {
|
|
1600
|
-
if (completedAtTimeoutBoundary) {
|
|
1601
|
-
// reservation 可能在 watchdog 开始终止普通轮时已经建立。最终回复若在
|
|
1602
|
-
// abort/kill 清理边界到达,本轮按完成处理,并原子取消尚未启动的恢复轮。
|
|
1603
|
-
cancelAutoRecoveryReservation(sessionId);
|
|
1604
|
-
console.log(
|
|
1605
|
-
`[${ts()}] [RESPONSE-STALL] Session ${sessionId} completed with an authoritative final response during timeout cleanup`,
|
|
1606
|
-
);
|
|
1607
|
-
}
|
|
1608
|
-
// 即使运行期间映射被异常清空,也必须更新本次实际触发 chat,避免 registry
|
|
1609
|
-
// 永久残留 running=true。
|
|
1610
|
-
const finalizationChatIds = [...new Set([
|
|
1611
|
-
...getChatsForSession(sessionId),
|
|
1612
|
-
_chatId,
|
|
1613
|
-
])];
|
|
1614
|
-
// 先写最终状态(done/stopped),确保 display loop 在下一轮消费前
|
|
1615
|
-
// 读到新状态并终结旧卡片。否则 setImmediate 在 CHECK 阶段先于
|
|
1616
|
-
// writeFile I/O(POLL 阶段)执行,display loop 会误以为旧轮仍在
|
|
1617
|
-
// 运行中并更新旧卡片,而不是新建卡片。
|
|
1618
|
-
const finalStatus = completedAtTimeoutBoundary
|
|
1619
|
-
? "done"
|
|
1620
|
-
: wasAutoEnded
|
|
1621
|
-
? "auto_ended"
|
|
1622
|
-
: (streamErrored || wasAbnormalExit || wasResourceStuck)
|
|
1623
|
-
? "error"
|
|
1624
|
-
: wasStopped
|
|
1625
|
-
? "stopped"
|
|
1626
|
-
: "done";
|
|
1627
|
-
const finalReply = pickFinalReply(state).trim();
|
|
1588
|
+
const wasStopped = prompt?.stopped ?? false;
|
|
1589
|
+
const wasAbnormalExit = prompt?.abnormalExit ?? false;
|
|
1590
|
+
const wasResourceStuck = prompt?.resourceStuck ?? false;
|
|
1591
|
+
const timeoutTriggered = prompt?.autoEnded ?? false;
|
|
1592
|
+
const completedAtTimeoutBoundary =
|
|
1593
|
+
timeoutTriggered && (prompt?.finalResponseObserved ?? false);
|
|
1594
|
+
const wasAutoEnded = timeoutTriggered && !completedAtTimeoutBoundary;
|
|
1595
|
+
const wasAutoRecovery = prompt?.autoRecovery ?? false;
|
|
1596
|
+
const autoEndedAt = wasAutoEnded ? prompt?.autoEndedAt : undefined;
|
|
1597
|
+
clearPromptResponseStallMonitor(sessionId);
|
|
1598
|
+
clearPromptProcessMonitor(sessionId);
|
|
1599
|
+
clearPromptFinalResponseCloseTimer(sessionId);
|
|
1600
|
+
markSessionFinalizing(sessionId);
|
|
1601
|
+
activePrompts.delete(sessionId);
|
|
1602
|
+
|
|
1603
|
+
try {
|
|
1604
|
+
if (completedAtTimeoutBoundary) {
|
|
1605
|
+
// reservation 可能在 watchdog 开始终止普通轮时已经建立。最终回复若在
|
|
1606
|
+
// abort/kill 清理边界到达,本轮按完成处理,并原子取消尚未启动的恢复轮。
|
|
1607
|
+
cancelAutoRecoveryReservation(sessionId);
|
|
1608
|
+
console.log(
|
|
1609
|
+
`[${ts()}] [RESPONSE-STALL] Session ${sessionId} completed with an authoritative final response during timeout cleanup`,
|
|
1610
|
+
);
|
|
1611
|
+
}
|
|
1612
|
+
// 即使运行期间映射被异常清空,也必须更新本次实际触发 chat,避免 registry
|
|
1613
|
+
// 永久残留 running=true。
|
|
1614
|
+
const finalizationChatIds = [...new Set([
|
|
1615
|
+
...getChatsForSession(sessionId),
|
|
1616
|
+
_chatId,
|
|
1617
|
+
])];
|
|
1618
|
+
// 先写最终状态(done/stopped),确保 display loop 在下一轮消费前
|
|
1619
|
+
// 读到新状态并终结旧卡片。否则 setImmediate 在 CHECK 阶段先于
|
|
1620
|
+
// writeFile I/O(POLL 阶段)执行,display loop 会误以为旧轮仍在
|
|
1621
|
+
// 运行中并更新旧卡片,而不是新建卡片。
|
|
1622
|
+
const finalStatus = completedAtTimeoutBoundary
|
|
1623
|
+
? "done"
|
|
1624
|
+
: wasAutoEnded
|
|
1625
|
+
? "auto_ended"
|
|
1626
|
+
: (streamErrored || wasAbnormalExit || wasResourceStuck)
|
|
1627
|
+
? "error"
|
|
1628
|
+
: wasStopped
|
|
1629
|
+
? "stopped"
|
|
1630
|
+
: "done";
|
|
1631
|
+
const finalReply = pickFinalReply(state).trim();
|
|
1628
1632
|
|
|
1629
1633
|
// stop-stuck-loop 接口可能在 fire-and-forget 中已写入带 final_reply 的
|
|
1630
1634
|
// stream state,finally 不应覆盖它。同时保留 stuckAt 标记,防止
|
|
@@ -1644,25 +1648,25 @@ export async function runAgentSession(
|
|
|
1644
1648
|
await writeStreamState({
|
|
1645
1649
|
sessionId,
|
|
1646
1650
|
status: finalStatus,
|
|
1647
|
-
accumulatedContent: state.accumulatedContent,
|
|
1648
|
-
finalReply: finalReplyToWrite,
|
|
1649
|
-
activity: activityTracker.activity,
|
|
1651
|
+
accumulatedContent: state.accumulatedContent,
|
|
1652
|
+
finalReply: finalReplyToWrite,
|
|
1653
|
+
activity: activityTracker.activity,
|
|
1650
1654
|
chunkCount: state.chunkCount,
|
|
1651
1655
|
turnCount: nextTurnCount,
|
|
1652
1656
|
contextTokens: existingInfo?.lastContextTokens ?? 0,
|
|
1653
1657
|
updatedAt: Date.now(),
|
|
1654
1658
|
cwd,
|
|
1655
|
-
tool,
|
|
1656
|
-
...(preserveStuckAt ? { stuckAt: preserveStuckAt } : {}),
|
|
1657
|
-
...(autoEndedAt !== undefined ? { autoEndedAt } : {}),
|
|
1658
|
-
});
|
|
1659
|
-
|
|
1660
|
-
// display loop 下一轮会读到最终状态并发送消息
|
|
1661
|
-
|
|
1662
|
-
let autoRecoveryTarget: { chatId: string; platform: PlatformAdapter } | undefined;
|
|
1663
|
-
|
|
1664
|
-
if (wasStopped) {
|
|
1665
|
-
for (const cid of finalizationChatIds) {
|
|
1659
|
+
tool,
|
|
1660
|
+
...(preserveStuckAt ? { stuckAt: preserveStuckAt } : {}),
|
|
1661
|
+
...(autoEndedAt !== undefined ? { autoEndedAt } : {}),
|
|
1662
|
+
});
|
|
1663
|
+
|
|
1664
|
+
// display loop 下一轮会读到最终状态并发送消息
|
|
1665
|
+
|
|
1666
|
+
let autoRecoveryTarget: { chatId: string; platform: PlatformAdapter } | undefined;
|
|
1667
|
+
|
|
1668
|
+
if (wasStopped) {
|
|
1669
|
+
for (const cid of finalizationChatIds) {
|
|
1666
1670
|
const finfo = sessionInfoMap.get(cid);
|
|
1667
1671
|
await recordSessionRegistry({
|
|
1668
1672
|
chatId: cid,
|
|
@@ -1674,62 +1678,15 @@ export async function runAgentSession(
|
|
|
1674
1678
|
running: false,
|
|
1675
1679
|
});
|
|
1676
1680
|
}
|
|
1677
|
-
const active1 = getLastActiveChat(sessionId) ?? finalizationChatIds[0];
|
|
1681
|
+
const active1 = getLastActiveChat(sessionId) ?? finalizationChatIds[0];
|
|
1678
1682
|
if (active1) {
|
|
1679
1683
|
await platform.sendText(active1, "会话已停止。").catch(() => {});
|
|
1680
|
-
setSessionChatAvatar(platform, active1, tool, "idle", sessionId).catch(() => {});
|
|
1684
|
+
setSessionChatAvatar(platform, active1, tool, "idle", sessionId).catch(() => {});
|
|
1681
1685
|
}
|
|
1682
1686
|
console.log(`[${ts()}] Session ${sessionId} stopped (content chunks: ${state.chunkCount})`);
|
|
1683
1687
|
if (tid) logTrace(tid, "SESSION_END", { sessionId, outcome: "stopped", chunks: state.chunkCount });
|
|
1684
|
-
} else if (wasAutoEnded) {
|
|
1685
|
-
for (const cid of finalizationChatIds) {
|
|
1686
|
-
const finfo = sessionInfoMap.get(cid);
|
|
1687
|
-
await recordSessionRegistry({
|
|
1688
|
-
chatId: cid,
|
|
1689
|
-
sessionId,
|
|
1690
|
-
tool,
|
|
1691
|
-
turnCount: finfo?.turnCount ?? nextTurnCount,
|
|
1692
|
-
lastContextTokens: finfo?.lastContextTokens ?? nextContextTokens,
|
|
1693
|
-
startTime: finfo?.startTime ?? now,
|
|
1694
|
-
running: false,
|
|
1695
|
-
});
|
|
1696
|
-
}
|
|
1697
|
-
const activeAutoEnded = getLastActiveChat(sessionId) ?? finalizationChatIds[0];
|
|
1698
|
-
if (activeAutoEnded) {
|
|
1699
|
-
const pp = platformForChat(activeAutoEnded) ?? platform;
|
|
1700
|
-
const terminalState = await readStreamState(sessionId);
|
|
1701
|
-
if (!displayCards.has(activeAutoEnded) && (!terminalState || !isFinalReplySentForTurn(terminalState))) {
|
|
1702
|
-
await sendFinalReplyTextOnce(
|
|
1703
|
-
pp,
|
|
1704
|
-
activeAutoEnded,
|
|
1705
|
-
sessionId,
|
|
1706
|
-
nextTurnCount,
|
|
1707
|
-
formatAutoEndedReply(finalReplyToWrite),
|
|
1708
|
-
);
|
|
1709
|
-
}
|
|
1710
|
-
setSessionChatAvatar(pp, activeAutoEnded, tool, "idle", sessionId).catch(() => {});
|
|
1711
|
-
|
|
1712
|
-
if (wasAutoRecovery) {
|
|
1713
|
-
// 这是紧接第一次停滞而启动的恢复轮;再次发生相同停滞即终止
|
|
1714
|
-
// 自动链,避免第三轮及之后的无限续跑。
|
|
1715
|
-
await pp.sendText(
|
|
1716
|
-
activeAutoEnded,
|
|
1717
|
-
RESPONSE_STALL_RECOVERY_EXHAUSTED_NOTICE,
|
|
1718
|
-
).catch(() => {});
|
|
1719
|
-
} else if (hasAutoRecoveryReservation(sessionId)) {
|
|
1720
|
-
// 用户可见提示与内部恢复 prompt 分离:提示发送失败不影响恢复,
|
|
1721
|
-
// 内部恢复仍由 reservation 保证先于普通缓存消息。
|
|
1722
|
-
await pp.sendText(
|
|
1723
|
-
activeAutoEnded,
|
|
1724
|
-
RESPONSE_STALL_RECOVERY_NOTICE,
|
|
1725
|
-
).catch(() => {});
|
|
1726
|
-
autoRecoveryTarget = { chatId: activeAutoEnded, platform: pp };
|
|
1727
|
-
}
|
|
1728
|
-
}
|
|
1729
|
-
console.warn(`[${ts()}] Session ${sessionId} auto-ended after stalled startup or response output (content chunks: ${state.chunkCount})`);
|
|
1730
|
-
if (tid) logTrace(tid, "SESSION_END", { sessionId, outcome: "response_stall", chunks: state.chunkCount });
|
|
1731
|
-
} else if (wasAbnormalExit) {
|
|
1732
|
-
for (const cid of finalizationChatIds) {
|
|
1688
|
+
} else if (wasAutoEnded) {
|
|
1689
|
+
for (const cid of finalizationChatIds) {
|
|
1733
1690
|
const finfo = sessionInfoMap.get(cid);
|
|
1734
1691
|
await recordSessionRegistry({
|
|
1735
1692
|
chatId: cid,
|
|
@@ -1741,12 +1698,59 @@ export async function runAgentSession(
|
|
|
1741
1698
|
running: false,
|
|
1742
1699
|
});
|
|
1743
1700
|
}
|
|
1744
|
-
const
|
|
1745
|
-
if (
|
|
1701
|
+
const activeAutoEnded = getLastActiveChat(sessionId) ?? finalizationChatIds[0];
|
|
1702
|
+
if (activeAutoEnded) {
|
|
1703
|
+
const pp = platformForChat(activeAutoEnded) ?? platform;
|
|
1704
|
+
const terminalState = await readStreamState(sessionId);
|
|
1705
|
+
if (!displayCards.has(activeAutoEnded) && (!terminalState || !isFinalReplySentForTurn(terminalState))) {
|
|
1706
|
+
await sendFinalReplyTextOnce(
|
|
1707
|
+
pp,
|
|
1708
|
+
activeAutoEnded,
|
|
1709
|
+
sessionId,
|
|
1710
|
+
nextTurnCount,
|
|
1711
|
+
formatAutoEndedReply(finalReplyToWrite),
|
|
1712
|
+
);
|
|
1713
|
+
}
|
|
1714
|
+
setSessionChatAvatar(pp, activeAutoEnded, tool, "idle", sessionId).catch(() => {});
|
|
1715
|
+
|
|
1716
|
+
if (wasAutoRecovery) {
|
|
1717
|
+
// 这是紧接第一次停滞而启动的恢复轮;再次发生相同停滞即终止
|
|
1718
|
+
// 自动链,避免第三轮及之后的无限续跑。
|
|
1719
|
+
await pp.sendText(
|
|
1720
|
+
activeAutoEnded,
|
|
1721
|
+
RESPONSE_STALL_RECOVERY_EXHAUSTED_NOTICE,
|
|
1722
|
+
).catch(() => {});
|
|
1723
|
+
} else if (hasAutoRecoveryReservation(sessionId)) {
|
|
1724
|
+
// 用户可见提示与内部恢复 prompt 分离:提示发送失败不影响恢复,
|
|
1725
|
+
// 内部恢复仍由 reservation 保证先于普通缓存消息。
|
|
1726
|
+
await pp.sendText(
|
|
1727
|
+
activeAutoEnded,
|
|
1728
|
+
RESPONSE_STALL_RECOVERY_NOTICE,
|
|
1729
|
+
).catch(() => {});
|
|
1730
|
+
autoRecoveryTarget = { chatId: activeAutoEnded, platform: pp };
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
console.warn(`[${ts()}] Session ${sessionId} auto-ended after stalled startup or response output (content chunks: ${state.chunkCount})`);
|
|
1734
|
+
if (tid) logTrace(tid, "SESSION_END", { sessionId, outcome: "response_stall", chunks: state.chunkCount });
|
|
1735
|
+
} else if (wasAbnormalExit) {
|
|
1736
|
+
for (const cid of finalizationChatIds) {
|
|
1737
|
+
const finfo = sessionInfoMap.get(cid);
|
|
1738
|
+
await recordSessionRegistry({
|
|
1739
|
+
chatId: cid,
|
|
1740
|
+
sessionId,
|
|
1741
|
+
tool,
|
|
1742
|
+
turnCount: finfo?.turnCount ?? nextTurnCount,
|
|
1743
|
+
lastContextTokens: finfo?.lastContextTokens ?? nextContextTokens,
|
|
1744
|
+
startTime: finfo?.startTime ?? now,
|
|
1745
|
+
running: false,
|
|
1746
|
+
});
|
|
1747
|
+
}
|
|
1748
|
+
const activeErr = getLastActiveChat(sessionId) ?? finalizationChatIds[0];
|
|
1749
|
+
if (activeErr) setSessionChatAvatar(platform, activeErr, tool, "idle", sessionId).catch(() => {});
|
|
1746
1750
|
console.log(`[${ts()}] Session ${sessionId} process exited unexpectedly (content chunks: ${state.chunkCount})`);
|
|
1747
1751
|
if (tid) logTrace(tid, "SESSION_END", { sessionId, outcome: "process_missing", chunks: state.chunkCount });
|
|
1748
|
-
} else {
|
|
1749
|
-
for (const cid of finalizationChatIds) {
|
|
1752
|
+
} else {
|
|
1753
|
+
for (const cid of finalizationChatIds) {
|
|
1750
1754
|
const finfo = sessionInfoMap.get(cid);
|
|
1751
1755
|
await recordSessionRegistry({
|
|
1752
1756
|
chatId: cid,
|
|
@@ -1758,107 +1762,107 @@ export async function runAgentSession(
|
|
|
1758
1762
|
running: false,
|
|
1759
1763
|
});
|
|
1760
1764
|
}
|
|
1761
|
-
const active2 = getLastActiveChat(sessionId) ?? finalizationChatIds[0];
|
|
1765
|
+
const active2 = getLastActiveChat(sessionId) ?? finalizationChatIds[0];
|
|
1762
1766
|
if (active2) {
|
|
1763
1767
|
const terminalState = await readStreamState(sessionId);
|
|
1764
1768
|
if (finalReply && !displayCards.has(active2) && (!terminalState || !isFinalReplySentForTurn(terminalState))) {
|
|
1765
1769
|
const pp = platformForChat(active2) ?? platform;
|
|
1766
1770
|
await sendFinalReplyTextOnce(pp, active2, sessionId, nextTurnCount, finalReply);
|
|
1767
1771
|
}
|
|
1768
|
-
setSessionChatAvatar(platform, active2, tool, "idle", sessionId).catch(() => {});
|
|
1772
|
+
setSessionChatAvatar(platform, active2, tool, "idle", sessionId).catch(() => {});
|
|
1773
|
+
}
|
|
1774
|
+
console.log(`[${ts()}] Session ${sessionId} stream complete (content chunks: ${state.chunkCount})`);
|
|
1775
|
+
if (tid) logTrace(tid, "SESSION_END", { sessionId, chunks: state.chunkCount, finalTextLen: finalReply.length });
|
|
1776
|
+
}
|
|
1777
|
+
|
|
1778
|
+
// 失去聊天绑定时无法安全选择恢复轮的展示目标,取消本次进程内预约。
|
|
1779
|
+
if (
|
|
1780
|
+
wasAutoEnded
|
|
1781
|
+
&& hasAutoRecoveryReservation(sessionId)
|
|
1782
|
+
&& !autoRecoveryTarget
|
|
1783
|
+
) {
|
|
1784
|
+
cancelAutoRecoveryReservation(sessionId);
|
|
1785
|
+
}
|
|
1786
|
+
const shouldScheduleAutoRecovery =
|
|
1787
|
+
autoRecoveryTarget !== undefined
|
|
1788
|
+
&& hasAutoRecoveryReservation(sessionId);
|
|
1789
|
+
|
|
1790
|
+
// 必须等本轮最终卡片、registry 和头像全部收尾后再取出并调度队列消息。
|
|
1791
|
+
// 在收尾期间新到达的消息也会因 finalizingSessions 被正确排入这里。
|
|
1792
|
+
let queuedForConsumption: ReturnType<typeof dequeueMessage> = undefined;
|
|
1793
|
+
if (wasStopped) {
|
|
1794
|
+
const discarded = dequeueMessage(sessionId);
|
|
1795
|
+
if (discarded) {
|
|
1796
|
+
console.log(`[${ts()}] [QUEUE] Discarding queued message for stopped session ${sessionId}`);
|
|
1797
|
+
}
|
|
1798
|
+
} else if (!shouldScheduleAutoRecovery) {
|
|
1799
|
+
// 第一次 response-stall 后保留普通缓存;恢复轮结束后由恢复轮的
|
|
1800
|
+
// finally 再消费,顺序固定为“自动恢复 → 用户缓存”。
|
|
1801
|
+
queuedForConsumption = dequeueMessage(sessionId);
|
|
1802
|
+
}
|
|
1803
|
+
|
|
1804
|
+
if (queuedForConsumption) {
|
|
1805
|
+
const queued = queuedForConsumption;
|
|
1806
|
+
// 队列消息可能来自其他群,保存当前 display chat 避免 display loop 被
|
|
1807
|
+
// 错误重定向(runAgentSession 会 consumeQueuePreservedChat 并在存在时
|
|
1808
|
+
// 用保存的 chat 替代 queued.chatId 作为 display 目标)。
|
|
1809
|
+
const preservedChat = getLastActiveChat(sessionId);
|
|
1810
|
+
if (preservedChat && preservedChat !== queued.chatId) {
|
|
1811
|
+
setQueuePreservedChat(sessionId, preservedChat);
|
|
1769
1812
|
}
|
|
1770
|
-
console.log(`[${ts()}]
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
// 在定时器等待期间仍令 isSessionRunning=true;调用 runAgentSession
|
|
1824
|
-
// 时会在首次 await 前同步写入 activePrompts,然后才消费 reservation,
|
|
1825
|
-
// 因而不存在普通用户消息可插入的事件循环空窗。
|
|
1826
|
-
setTimeout(() => {
|
|
1827
|
-
if (!hasAutoRecoveryReservation(sessionId)) return;
|
|
1828
|
-
const recoveryRun = runAgentSession(
|
|
1829
|
-
sessionId,
|
|
1830
|
-
RESPONSE_STALL_RECOVERY_PROMPT,
|
|
1831
|
-
target.platform,
|
|
1832
|
-
target.chatId,
|
|
1833
|
-
Date.now(),
|
|
1834
|
-
tool,
|
|
1835
|
-
undefined,
|
|
1836
|
-
{ autoRecovery: true },
|
|
1837
|
-
);
|
|
1838
|
-
consumeAutoRecoveryReservation(sessionId);
|
|
1839
|
-
void recoveryRun.catch((err) => {
|
|
1840
|
-
console.error(
|
|
1841
|
-
`[${ts()}] [RESPONSE-STALL] Automatic recovery failed for ${sessionId}: ${(err as Error).message}`,
|
|
1842
|
-
);
|
|
1843
|
-
target.platform.sendText(
|
|
1844
|
-
target.chatId,
|
|
1845
|
-
`⚠️ 自动续跑启动失败:${(err as Error).message}`,
|
|
1846
|
-
).catch(() => {});
|
|
1847
|
-
|
|
1848
|
-
// 若恢复轮在进入主 stream try/finally 前即准备失败,它不会自然
|
|
1849
|
-
// 消费此前保留的用户缓存;在错误回调中补做一次,避免队列悬挂。
|
|
1850
|
-
const queued = dequeueMessage(sessionId);
|
|
1851
|
-
if (queued) {
|
|
1852
|
-
consumeQueuedMessage(target.platform, queued);
|
|
1853
|
-
}
|
|
1854
|
-
});
|
|
1855
|
-
}, RESPONSE_STALL_RECOVERY_DELAY_MS);
|
|
1856
|
-
}
|
|
1857
|
-
} finally {
|
|
1858
|
-
clearSessionFinalizing(sessionId);
|
|
1859
|
-
}
|
|
1860
|
-
}
|
|
1861
|
-
}
|
|
1813
|
+
console.log(`[${ts()}] [QUEUE] Consuming queued message for session ${sessionId}: "${queued.text.slice(0, 50)}"`);
|
|
1814
|
+
// setTimeout 而非 setImmediate:给 display loop 的 setInterval
|
|
1815
|
+
// 足够时间读到最终状态并终结旧卡片,避免新轮更新旧卡片。
|
|
1816
|
+
setTimeout(() => {
|
|
1817
|
+
consumeQueuedMessage(platform, queued);
|
|
1818
|
+
}, RESPONSE_STALL_RECOVERY_DELAY_MS);
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1821
|
+
if (shouldScheduleAutoRecovery && autoRecoveryTarget) {
|
|
1822
|
+
const target = autoRecoveryTarget;
|
|
1823
|
+
console.log(
|
|
1824
|
+
`[${ts()}] [RESPONSE-STALL] Reserved automatic recovery for session ${sessionId}`,
|
|
1825
|
+
);
|
|
1826
|
+
// 延迟与普通队列原有策略一致,让上一轮终态先完成展示。reservation
|
|
1827
|
+
// 在定时器等待期间仍令 isSessionRunning=true;调用 runAgentSession
|
|
1828
|
+
// 时会在首次 await 前同步写入 activePrompts,然后才消费 reservation,
|
|
1829
|
+
// 因而不存在普通用户消息可插入的事件循环空窗。
|
|
1830
|
+
setTimeout(() => {
|
|
1831
|
+
if (!hasAutoRecoveryReservation(sessionId)) return;
|
|
1832
|
+
const recoveryRun = runAgentSession(
|
|
1833
|
+
sessionId,
|
|
1834
|
+
RESPONSE_STALL_RECOVERY_PROMPT,
|
|
1835
|
+
target.platform,
|
|
1836
|
+
target.chatId,
|
|
1837
|
+
Date.now(),
|
|
1838
|
+
tool,
|
|
1839
|
+
undefined,
|
|
1840
|
+
{ autoRecovery: true },
|
|
1841
|
+
);
|
|
1842
|
+
consumeAutoRecoveryReservation(sessionId);
|
|
1843
|
+
void recoveryRun.catch((err) => {
|
|
1844
|
+
console.error(
|
|
1845
|
+
`[${ts()}] [RESPONSE-STALL] Automatic recovery failed for ${sessionId}: ${(err as Error).message}`,
|
|
1846
|
+
);
|
|
1847
|
+
target.platform.sendText(
|
|
1848
|
+
target.chatId,
|
|
1849
|
+
`⚠️ 自动续跑启动失败:${(err as Error).message}`,
|
|
1850
|
+
).catch(() => {});
|
|
1851
|
+
|
|
1852
|
+
// 若恢复轮在进入主 stream try/finally 前即准备失败,它不会自然
|
|
1853
|
+
// 消费此前保留的用户缓存;在错误回调中补做一次,避免队列悬挂。
|
|
1854
|
+
const queued = dequeueMessage(sessionId);
|
|
1855
|
+
if (queued) {
|
|
1856
|
+
consumeQueuedMessage(target.platform, queued);
|
|
1857
|
+
}
|
|
1858
|
+
});
|
|
1859
|
+
}, RESPONSE_STALL_RECOVERY_DELAY_MS);
|
|
1860
|
+
}
|
|
1861
|
+
} finally {
|
|
1862
|
+
clearSessionFinalizing(sessionId);
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1862
1866
|
|
|
1863
1867
|
// ---------------------------------------------------------------------------
|
|
1864
1868
|
// startUnifiedDisplayLoop — 全局统一 display 循环,遍历 displayCards 更新卡片
|
|
@@ -1932,14 +1936,14 @@ export function startUnifiedDisplayLoop(): void {
|
|
|
1932
1936
|
// 还没执行,当前 stream state 可能是 stopSession fire-and-forget
|
|
1933
1937
|
// 写入的,finalReply 滞后于内存态。跳过发送,等 finally 落盘后
|
|
1934
1938
|
// 下一次 tick 再处理,避免发送过期内容或与后续发送重复。
|
|
1935
|
-
if (activePrompts.has(sessionId)) continue;
|
|
1936
|
-
|
|
1937
|
-
const tail = "━━━ 回答结束 ━━━";
|
|
1938
|
-
const finalMsg = state.status === "auto_ended"
|
|
1939
|
-
? formatAutoEndedReply(remaining)
|
|
1940
|
-
: remaining
|
|
1941
|
-
? remaining + "\n" + tail
|
|
1942
|
-
: tail;
|
|
1939
|
+
if (activePrompts.has(sessionId)) continue;
|
|
1940
|
+
|
|
1941
|
+
const tail = "━━━ 回答结束 ━━━";
|
|
1942
|
+
const finalMsg = state.status === "auto_ended"
|
|
1943
|
+
? formatAutoEndedReply(remaining)
|
|
1944
|
+
: remaining
|
|
1945
|
+
? remaining + "\n" + tail
|
|
1946
|
+
: tail;
|
|
1943
1947
|
if (!isFinalReplySentForTurn(state)) {
|
|
1944
1948
|
await sendFinalReplyTextOnce(p, chatId, sessionId, state.turnCount, finalMsg);
|
|
1945
1949
|
}
|
|
@@ -1963,7 +1967,7 @@ export function startUnifiedDisplayLoop(): void {
|
|
|
1963
1967
|
const nextSeq = display.sequence + 1;
|
|
1964
1968
|
const { title: headerTitle, template: headerTemplate } = formatTerminalHeader(state.status);
|
|
1965
1969
|
const cardContent = truncateContent(state.accumulatedContent + state.finalReply) || " ";
|
|
1966
|
-
const doneCard = buildProgressCard(cardContent,
|
|
1970
|
+
const doneCard = buildProgressCard(progressView({ text: cardContent, status: "done", showStop: false, headerTitle, headerTemplate }));
|
|
1967
1971
|
await p.cardUpdate(display.cardId, doneCard, nextSeq).then(() => {
|
|
1968
1972
|
display.sequence = nextSeq;
|
|
1969
1973
|
terminalCardUpdateAccepted = true;
|
|
@@ -1988,12 +1992,12 @@ export function startUnifiedDisplayLoop(): void {
|
|
|
1988
1992
|
continue;
|
|
1989
1993
|
}
|
|
1990
1994
|
|
|
1991
|
-
let terminalTextDelivered = true;
|
|
1992
|
-
const terminalReply = formatTerminalReply(state.status, state.finalReply);
|
|
1993
|
-
if (terminalReply) {
|
|
1994
|
-
if (!isFinalReplySentForTurn(state)) {
|
|
1995
|
-
terminalTextDelivered = await sendFinalReplyTextOnce(p, chatId, sessionId, state.turnCount, terminalReply);
|
|
1996
|
-
}
|
|
1995
|
+
let terminalTextDelivered = true;
|
|
1996
|
+
const terminalReply = formatTerminalReply(state.status, state.finalReply);
|
|
1997
|
+
if (terminalReply) {
|
|
1998
|
+
if (!isFinalReplySentForTurn(state)) {
|
|
1999
|
+
terminalTextDelivered = await sendFinalReplyTextOnce(p, chatId, sessionId, state.turnCount, terminalReply);
|
|
2000
|
+
}
|
|
1997
2001
|
} else if (state.accumulatedContent.trim()) {
|
|
1998
2002
|
const short = truncateContent(state.accumulatedContent, 30, 4000);
|
|
1999
2003
|
terminalTextDelivered = await p.sendText(chatId, `[生成过程]\n${short}`).then((ok) => ok !== false).catch(() => false);
|
|
@@ -2008,7 +2012,7 @@ export function startUnifiedDisplayLoop(): void {
|
|
|
2008
2012
|
finalizeTurnCards(sessionId, state.turnCount, finalSt).catch(() => {});
|
|
2009
2013
|
displayCards.delete(chatId);
|
|
2010
2014
|
}
|
|
2011
|
-
setSessionChatAvatar(p, chatId, state.tool, "idle", sessionId).catch(() => {});
|
|
2015
|
+
setSessionChatAvatar(p, chatId, state.tool, "idle", sessionId).catch(() => {});
|
|
2012
2016
|
console.log(`[${ts()}] [DISPLAY] unified loop deleted display for ${chatId} (terminal: ${state.status})`);
|
|
2013
2017
|
} else {
|
|
2014
2018
|
// running: 创建或更新展示
|
|
@@ -2045,16 +2049,16 @@ export function startUnifiedDisplayLoop(): void {
|
|
|
2045
2049
|
}
|
|
2046
2050
|
} else {
|
|
2047
2051
|
// 非 WeChat: 卡片流程
|
|
2048
|
-
if (display.turnCount !== state.turnCount) {
|
|
2052
|
+
if (display.turnCount !== state.turnCount) {
|
|
2049
2053
|
console.log(`[${ts()}] [DISPLAY] turn mismatch for ${chatId}: display.turnCount=${display.turnCount} state.turnCount=${state.turnCount}, resetting`);
|
|
2050
2054
|
finalizeTurnCards(sessionId, display.turnCount, "done").catch(() => {});
|
|
2051
2055
|
displayCards.delete(chatId);
|
|
2052
|
-
continue;
|
|
2053
|
-
}
|
|
2054
|
-
|
|
2055
|
-
const activityHeaderTitle = formatAgentActivityTitle(state.activity, Date.now());
|
|
2056
|
-
|
|
2057
|
-
// 卡片轮转
|
|
2056
|
+
continue;
|
|
2057
|
+
}
|
|
2058
|
+
|
|
2059
|
+
const activityHeaderTitle = formatAgentActivityTitle(state.activity, Date.now());
|
|
2060
|
+
|
|
2061
|
+
// 卡片轮转
|
|
2058
2062
|
if (Date.now() - display.cardCreatedAt > CARD_ROTATE_MS) {
|
|
2059
2063
|
display.cardBusy = true;
|
|
2060
2064
|
try {
|
|
@@ -2062,9 +2066,9 @@ export function startUnifiedDisplayLoop(): void {
|
|
|
2062
2066
|
p,
|
|
2063
2067
|
chatId,
|
|
2064
2068
|
sessionId,
|
|
2065
|
-
display.turnCount,
|
|
2066
|
-
display.streamErrorNotified ? undefined : "生成中卡片发送失败,结果将继续更新在上一张卡片中。",
|
|
2067
|
-
activityHeaderTitle,
|
|
2069
|
+
display.turnCount,
|
|
2070
|
+
display.streamErrorNotified ? undefined : "生成中卡片发送失败,结果将继续更新在上一张卡片中。",
|
|
2071
|
+
activityHeaderTitle,
|
|
2068
2072
|
);
|
|
2069
2073
|
if (!newCardId) {
|
|
2070
2074
|
display.streamErrorNotified = true;
|
|
@@ -2072,7 +2076,7 @@ export function startUnifiedDisplayLoop(): void {
|
|
|
2072
2076
|
}
|
|
2073
2077
|
const oldSeqBase = display.sequence;
|
|
2074
2078
|
const oldContent = state.accumulatedContent + state.finalReply;
|
|
2075
|
-
const oldCard = buildProgressCard(truncateContent(oldContent) || " ",
|
|
2079
|
+
const oldCard = buildProgressCard(progressView({ text: truncateContent(oldContent) || " ", status: "done", showStop: false, headerTitle: "上一阶段记录" }));
|
|
2076
2080
|
await p.cardUpdate(display.cardId, oldCard, oldSeqBase + 1).then(() => {
|
|
2077
2081
|
display.sequence = oldSeqBase + 1;
|
|
2078
2082
|
}).catch(err => {
|
|
@@ -2083,10 +2087,10 @@ export function startUnifiedDisplayLoop(): void {
|
|
|
2083
2087
|
display.sequence = 1;
|
|
2084
2088
|
display.cardCreatedAt = Date.now();
|
|
2085
2089
|
display.rotationAccLen = state.accumulatedContent.length;
|
|
2086
|
-
display.rotationFinalReply = state.finalReply;
|
|
2087
|
-
display.lastSentContent = "";
|
|
2088
|
-
display.lastSentHeaderTitle = activityHeaderTitle;
|
|
2089
|
-
display.streamErrorNotified = false;
|
|
2090
|
+
display.rotationFinalReply = state.finalReply;
|
|
2091
|
+
display.lastSentContent = "";
|
|
2092
|
+
display.lastSentHeaderTitle = activityHeaderTitle;
|
|
2093
|
+
display.streamErrorNotified = false;
|
|
2090
2094
|
} catch (err) {
|
|
2091
2095
|
console.error(`[${ts()}] [CARDIKT] rotation FAIL for ${chatId}: ${(err as Error).message}`);
|
|
2092
2096
|
} finally {
|
|
@@ -2104,24 +2108,21 @@ export function startUnifiedDisplayLoop(): void {
|
|
|
2104
2108
|
replyDelta = state.finalReply.slice(rotReply.length);
|
|
2105
2109
|
} else {
|
|
2106
2110
|
replyDelta = state.finalReply;
|
|
2107
|
-
}
|
|
2108
|
-
const delta = (accDelta + replyDelta).trim();
|
|
2109
|
-
|
|
2110
|
-
display.dotCount = (display.dotCount % 9) + 1;
|
|
2111
|
-
let deltaBase = delta;
|
|
2112
|
-
if (isCodeBlockOpen(deltaBase)) deltaBase += "\n```";
|
|
2113
|
-
const displayContent = deltaBase + "\n" + "。".repeat(display.dotCount);
|
|
2114
|
-
if (
|
|
2115
|
-
displayContent === display.lastSentContent
|
|
2116
|
-
&& activityHeaderTitle === display.lastSentHeaderTitle
|
|
2117
|
-
) continue;
|
|
2118
|
-
|
|
2119
|
-
display.lastSentContent = displayContent;
|
|
2120
|
-
display.lastSentHeaderTitle = activityHeaderTitle;
|
|
2121
|
-
const deltaCard = buildProgressCard(truncateContent(displayContent) || "等待 Agent 输出...",
|
|
2122
|
-
showStop: true,
|
|
2123
|
-
headerTitle: activityHeaderTitle,
|
|
2124
|
-
});
|
|
2111
|
+
}
|
|
2112
|
+
const delta = (accDelta + replyDelta).trim();
|
|
2113
|
+
|
|
2114
|
+
display.dotCount = (display.dotCount % 9) + 1;
|
|
2115
|
+
let deltaBase = delta;
|
|
2116
|
+
if (isCodeBlockOpen(deltaBase)) deltaBase += "\n```";
|
|
2117
|
+
const displayContent = deltaBase + "\n" + "。".repeat(display.dotCount);
|
|
2118
|
+
if (
|
|
2119
|
+
displayContent === display.lastSentContent
|
|
2120
|
+
&& activityHeaderTitle === display.lastSentHeaderTitle
|
|
2121
|
+
) continue;
|
|
2122
|
+
|
|
2123
|
+
display.lastSentContent = displayContent;
|
|
2124
|
+
display.lastSentHeaderTitle = activityHeaderTitle;
|
|
2125
|
+
const deltaCard = buildProgressCard(progressView({ text: truncateContent(displayContent) || "等待 Agent 输出...", showStop: true, headerTitle: activityHeaderTitle }));
|
|
2125
2126
|
display.cardBusy = true;
|
|
2126
2127
|
const mySeq = display.sequence + 1;
|
|
2127
2128
|
try {
|
|
@@ -2140,24 +2141,24 @@ export function startUnifiedDisplayLoop(): void {
|
|
|
2140
2141
|
display.cardBusy = false;
|
|
2141
2142
|
}
|
|
2142
2143
|
continue;
|
|
2143
|
-
}
|
|
2144
|
-
|
|
2145
|
-
display.dotCount = (display.dotCount % 9) + 1;
|
|
2146
|
-
let contentBase = state.accumulatedContent + state.finalReply;
|
|
2147
|
-
if (isCodeBlockOpen(contentBase)) contentBase += "\n```";
|
|
2148
|
-
const fullContent = contentBase + "\n" + "。".repeat(display.dotCount);
|
|
2149
|
-
if (
|
|
2150
|
-
fullContent === display.lastSentContent
|
|
2151
|
-
&& activityHeaderTitle === display.lastSentHeaderTitle
|
|
2152
|
-
) continue;
|
|
2153
|
-
|
|
2154
|
-
display.lastSentContent = fullContent;
|
|
2155
|
-
display.lastSentHeaderTitle = activityHeaderTitle;
|
|
2156
|
-
const cardContent = truncateContent(fullContent) || "等待 Agent 输出...";
|
|
2144
|
+
}
|
|
2145
|
+
|
|
2146
|
+
display.dotCount = (display.dotCount % 9) + 1;
|
|
2147
|
+
let contentBase = state.accumulatedContent + state.finalReply;
|
|
2148
|
+
if (isCodeBlockOpen(contentBase)) contentBase += "\n```";
|
|
2149
|
+
const fullContent = contentBase + "\n" + "。".repeat(display.dotCount);
|
|
2150
|
+
if (
|
|
2151
|
+
fullContent === display.lastSentContent
|
|
2152
|
+
&& activityHeaderTitle === display.lastSentHeaderTitle
|
|
2153
|
+
) continue;
|
|
2154
|
+
|
|
2155
|
+
display.lastSentContent = fullContent;
|
|
2156
|
+
display.lastSentHeaderTitle = activityHeaderTitle;
|
|
2157
|
+
const cardContent = truncateContent(fullContent) || "等待 Agent 输出...";
|
|
2157
2158
|
display.cardBusy = true;
|
|
2158
2159
|
const mySeq = display.sequence + 1;
|
|
2159
2160
|
try {
|
|
2160
|
-
const card = buildProgressCard(cardContent,
|
|
2161
|
+
const card = buildProgressCard(progressView({ text: cardContent, showStop: true, headerTitle: activityHeaderTitle }));
|
|
2161
2162
|
await p.cardUpdate(display.cardId, card, mySeq);
|
|
2162
2163
|
display.sequence = mySeq;
|
|
2163
2164
|
} catch (err) {
|
|
@@ -2214,37 +2215,37 @@ export function stopUnifiedDisplayLoop(): void {
|
|
|
2214
2215
|
// 虽然很快但仍是异步,期间 display loop 可能多读到 1–2 帧 "running",
|
|
2215
2216
|
// 用户体验上"按下停止后还要等几秒卡片才变成已停止"。先把状态标好,
|
|
2216
2217
|
// finally 后续再写一次也不冲突——status 最终值仍然是 stopped。
|
|
2217
|
-
export function stopSession(sessionId: string): boolean {
|
|
2218
|
-
// /stop 拥有高于内部自动恢复的优先级。旧轮已经完成、恢复轮尚在 200ms
|
|
2219
|
-
// 预约窗口时 activePrompts 为空,因此必须单独取消 reservation。
|
|
2220
|
-
const cancelledRecovery = cancelAutoRecoveryReservation(sessionId);
|
|
2221
|
-
const prompt = activePrompts.get(sessionId);
|
|
2222
|
-
if (!prompt) {
|
|
2223
|
-
if (cancelledRecovery) {
|
|
2224
|
-
cancelQueuedMessage(sessionId);
|
|
2225
|
-
console.log(`[${ts()}] [STOP] Reserved automatic recovery for ${sessionId} cancelled`);
|
|
2226
|
-
return true;
|
|
2227
|
-
}
|
|
2228
|
-
return false;
|
|
2229
|
-
}
|
|
2230
|
-
prompt.stopped = true;
|
|
2231
|
-
clearPromptResponseStallMonitor(sessionId);
|
|
2232
|
-
clearPromptProcessMonitor(sessionId);
|
|
2233
|
-
clearPromptFinalResponseCloseTimer(sessionId);
|
|
2234
|
-
cancelQueuedMessage(sessionId);
|
|
2235
|
-
|
|
2236
|
-
// 先发起整棵进程树清理,再触发 close/abort。Windows 上 CLI 由
|
|
2237
|
-
// cmd.exe → node → 实际二进制组成;若先 process.kill(cmd.exe),taskkill
|
|
2238
|
-
// 随后便无法从已消失的根 PID 找到后代,正是幽灵 Codex/Cursor 的来源。
|
|
2239
|
-
// killProcessTree 在返回 Promise 前已启动 taskkill,因此这里无需阻塞。
|
|
2240
|
-
void killProcessTree(prompt.processPid);
|
|
2241
|
-
try {
|
|
2242
|
-
prompt.closeSession?.();
|
|
2243
|
-
} catch (err) {
|
|
2218
|
+
export function stopSession(sessionId: string): boolean {
|
|
2219
|
+
// /stop 拥有高于内部自动恢复的优先级。旧轮已经完成、恢复轮尚在 200ms
|
|
2220
|
+
// 预约窗口时 activePrompts 为空,因此必须单独取消 reservation。
|
|
2221
|
+
const cancelledRecovery = cancelAutoRecoveryReservation(sessionId);
|
|
2222
|
+
const prompt = activePrompts.get(sessionId);
|
|
2223
|
+
if (!prompt) {
|
|
2224
|
+
if (cancelledRecovery) {
|
|
2225
|
+
cancelQueuedMessage(sessionId);
|
|
2226
|
+
console.log(`[${ts()}] [STOP] Reserved automatic recovery for ${sessionId} cancelled`);
|
|
2227
|
+
return true;
|
|
2228
|
+
}
|
|
2229
|
+
return false;
|
|
2230
|
+
}
|
|
2231
|
+
prompt.stopped = true;
|
|
2232
|
+
clearPromptResponseStallMonitor(sessionId);
|
|
2233
|
+
clearPromptProcessMonitor(sessionId);
|
|
2234
|
+
clearPromptFinalResponseCloseTimer(sessionId);
|
|
2235
|
+
cancelQueuedMessage(sessionId);
|
|
2236
|
+
|
|
2237
|
+
// 先发起整棵进程树清理,再触发 close/abort。Windows 上 CLI 由
|
|
2238
|
+
// cmd.exe → node → 实际二进制组成;若先 process.kill(cmd.exe),taskkill
|
|
2239
|
+
// 随后便无法从已消失的根 PID 找到后代,正是幽灵 Codex/Cursor 的来源。
|
|
2240
|
+
// killProcessTree 在返回 Promise 前已启动 taskkill,因此这里无需阻塞。
|
|
2241
|
+
void killProcessTree(prompt.processPid);
|
|
2242
|
+
try {
|
|
2243
|
+
prompt.closeSession?.();
|
|
2244
|
+
} catch (err) {
|
|
2244
2245
|
console.warn(`[${ts()}] [STOP] closeSession failed for ${sessionId}: ${(err as Error).message}`);
|
|
2245
|
-
}
|
|
2246
|
-
prompt.controller.abort();
|
|
2247
|
-
console.log(`[${ts()}] [STOP] Session ${sessionId} aborted`);
|
|
2246
|
+
}
|
|
2247
|
+
prompt.controller.abort();
|
|
2248
|
+
console.log(`[${ts()}] [STOP] Session ${sessionId} aborted`);
|
|
2248
2249
|
|
|
2249
2250
|
// fire-and-forget:立刻把 stream-state.status 改成 stopped,
|
|
2250
2251
|
// 让 display loop 下一次扫到立刻渲染"已停止"卡片,不必再等几秒。
|
|
@@ -2314,27 +2315,27 @@ async function resolveModelEffort(
|
|
|
2314
2315
|
// adapter 异常时降级为占位符(不阻塞 /state 卡片)
|
|
2315
2316
|
}
|
|
2316
2317
|
return { model, effort: null };
|
|
2317
|
-
}
|
|
2318
|
-
if (tool === "codex") {
|
|
2319
|
-
const m = getEffectiveModelForTool(tool, sessionId);
|
|
2320
|
-
const e = getEffectiveEffortForTool(tool, sessionId);
|
|
2321
|
-
return {
|
|
2322
|
-
model: m.trim() !== "" ? m : UNKNOWN_MODEL_PLACEHOLDER,
|
|
2323
|
-
effort: e.trim() !== "" ? e : UNKNOWN_MODEL_PLACEHOLDER,
|
|
2324
|
-
};
|
|
2325
|
-
}
|
|
2326
|
-
if (tool === "ccc") {
|
|
2327
|
-
const m = getEffectiveModelForTool(tool, sessionId);
|
|
2328
|
-
return {
|
|
2329
|
-
model: m.trim() !== "" ? m : UNKNOWN_MODEL_PLACEHOLDER,
|
|
2330
|
-
effort: null,
|
|
2331
|
-
};
|
|
2332
|
-
}
|
|
2333
|
-
return {
|
|
2334
|
-
model: anthropicConfigDisplay(getModelForSession(sessionId)),
|
|
2335
|
-
effort: anthropicConfigDisplay(getEffectiveEffortForTool(tool, sessionId)),
|
|
2336
|
-
};
|
|
2337
|
-
}
|
|
2318
|
+
}
|
|
2319
|
+
if (tool === "codex") {
|
|
2320
|
+
const m = getEffectiveModelForTool(tool, sessionId);
|
|
2321
|
+
const e = getEffectiveEffortForTool(tool, sessionId);
|
|
2322
|
+
return {
|
|
2323
|
+
model: m.trim() !== "" ? m : UNKNOWN_MODEL_PLACEHOLDER,
|
|
2324
|
+
effort: e.trim() !== "" ? e : UNKNOWN_MODEL_PLACEHOLDER,
|
|
2325
|
+
};
|
|
2326
|
+
}
|
|
2327
|
+
if (tool === "ccc") {
|
|
2328
|
+
const m = getEffectiveModelForTool(tool, sessionId);
|
|
2329
|
+
return {
|
|
2330
|
+
model: m.trim() !== "" ? m : UNKNOWN_MODEL_PLACEHOLDER,
|
|
2331
|
+
effort: null,
|
|
2332
|
+
};
|
|
2333
|
+
}
|
|
2334
|
+
return {
|
|
2335
|
+
model: anthropicConfigDisplay(getModelForSession(sessionId)),
|
|
2336
|
+
effort: anthropicConfigDisplay(getEffectiveEffortForTool(tool, sessionId)),
|
|
2337
|
+
};
|
|
2338
|
+
}
|
|
2338
2339
|
|
|
2339
2340
|
export async function getSessionStatus(chatId: string): Promise<SessionStatus | null> {
|
|
2340
2341
|
const info = sessionInfoMap.get(chatId);
|
|
@@ -2367,10 +2368,10 @@ export async function getSessionStatus(chatId: string): Promise<SessionStatus |
|
|
|
2367
2368
|
};
|
|
2368
2369
|
}
|
|
2369
2370
|
|
|
2370
|
-
export interface SessionsListEntry {
|
|
2371
|
-
chatId: string;
|
|
2372
|
-
chatType?: string;
|
|
2373
|
-
sessionId: string;
|
|
2371
|
+
export interface SessionsListEntry {
|
|
2372
|
+
chatId: string;
|
|
2373
|
+
chatType?: string;
|
|
2374
|
+
sessionId: string;
|
|
2374
2375
|
chatName: string;
|
|
2375
2376
|
active: boolean;
|
|
2376
2377
|
turnCount: number;
|
|
@@ -2393,10 +2394,10 @@ export async function getAllSessionsStatus(): Promise<SessionsListEntry[]> {
|
|
|
2393
2394
|
.map(([sessionId, record]) => {
|
|
2394
2395
|
const createdAt = Number.isFinite(record.createdAt) ? record.createdAt : 0;
|
|
2395
2396
|
const active = activePrompts.get(sessionId);
|
|
2396
|
-
return {
|
|
2397
|
-
chatId: "",
|
|
2398
|
-
chatType: undefined,
|
|
2399
|
-
sessionId,
|
|
2397
|
+
return {
|
|
2398
|
+
chatId: "",
|
|
2399
|
+
chatType: undefined,
|
|
2400
|
+
sessionId,
|
|
2400
2401
|
tool: record.tool,
|
|
2401
2402
|
chatName: record.chatName ?? "",
|
|
2402
2403
|
turnCount: 0,
|
|
@@ -2414,10 +2415,10 @@ export async function getAllSessionsStatus(): Promise<SessionsListEntry[]> {
|
|
|
2414
2415
|
return Promise.all(
|
|
2415
2416
|
entries.map(async (info) => {
|
|
2416
2417
|
const { model, effort } = await resolveModelEffort(info.tool, info.sessionId);
|
|
2417
|
-
return {
|
|
2418
|
-
chatId: info.chatId,
|
|
2419
|
-
chatType: info.chatType,
|
|
2420
|
-
sessionId: info.sessionId,
|
|
2418
|
+
return {
|
|
2419
|
+
chatId: info.chatId,
|
|
2420
|
+
chatType: info.chatType,
|
|
2421
|
+
sessionId: info.sessionId,
|
|
2421
2422
|
chatName: info.chatName || "",
|
|
2422
2423
|
active: !!activePrompts.get(info.sessionId) &&
|
|
2423
2424
|
!activePrompts.get(info.sessionId)?.stopped &&
|
|
@@ -2442,11 +2443,11 @@ export async function getAllSessionsStatus(): Promise<SessionsListEntry[]> {
|
|
|
2442
2443
|
export function _setAdapterForToolForTest(tool: string, adapter: ToolAdapter): void {
|
|
2443
2444
|
adapterCache.set(tool, adapter);
|
|
2444
2445
|
// 同时设置当前配置模型对应的 key(getAdapterForTool 会优先 lookup 含 model 的 key)
|
|
2445
|
-
const effective = getEffectiveModelForTool(tool);
|
|
2446
|
-
const effort = getEffectiveEffortForTool(tool);
|
|
2447
|
-
const fastMode = getEffectiveFastModeForTool(tool);
|
|
2448
|
-
adapterCache.set(`${tool}:${effective || ""}:${effort || ""}:${fastMode ? "fast" : "default"}`, adapter);
|
|
2449
|
-
if (effective) adapterCache.set(`${tool}:${effective}`, adapter);
|
|
2446
|
+
const effective = getEffectiveModelForTool(tool);
|
|
2447
|
+
const effort = getEffectiveEffortForTool(tool);
|
|
2448
|
+
const fastMode = getEffectiveFastModeForTool(tool);
|
|
2449
|
+
adapterCache.set(`${tool}:${effective || ""}:${effort || ""}:${fastMode ? "fast" : "default"}`, adapter);
|
|
2450
|
+
if (effective) adapterCache.set(`${tool}:${effective}`, adapter);
|
|
2450
2451
|
}
|
|
2451
2452
|
|
|
2452
2453
|
export function clearAdapterCache(): void {
|