chatccc 0.2.222 → 0.2.223
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/__tests__/card-plain-text.test.ts +7 -6
- package/src/__tests__/cards.test.ts +179 -178
- package/src/__tests__/progress-reducer.test.ts +110 -0
- package/src/__tests__/terminal-renderer.test.ts +143 -0
- package/src/builtin/cli.ts +38 -1
- package/src/cards.ts +280 -275
- 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 +935 -937
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") {
|
|
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,53 @@ 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({ model: effectiveModel || undefined });
|
|
600
|
-
} else {
|
|
601
|
-
adapter = createClaudeAdapter({
|
|
602
|
-
model: effectiveModel,
|
|
603
|
-
subagentModel: CLAUDE_SUBAGENT_MODEL,
|
|
604
|
-
effort: effectiveEffort,
|
|
605
|
-
apiKey: CLAUDE_API_KEY,
|
|
606
|
-
baseUrl: CLAUDE_BASE_URL,
|
|
607
|
-
isEmpty: isAnthropicConfigEmpty,
|
|
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({ model: effectiveModel || undefined });
|
|
601
|
+
} else {
|
|
602
|
+
adapter = createClaudeAdapter({
|
|
603
|
+
model: effectiveModel,
|
|
604
|
+
subagentModel: CLAUDE_SUBAGENT_MODEL,
|
|
605
|
+
effort: effectiveEffort,
|
|
606
|
+
apiKey: CLAUDE_API_KEY,
|
|
607
|
+
baseUrl: CLAUDE_BASE_URL,
|
|
608
|
+
isEmpty: isAnthropicConfigEmpty,
|
|
608
609
|
maxTurn: CLAUDE_MAX_TURN,
|
|
609
610
|
});
|
|
610
611
|
}
|
|
@@ -676,13 +677,13 @@ export function _resetSessionToolsFileForTest(): void {
|
|
|
676
677
|
export const SESSION_REGISTRY_FILE = join(USER_DATA_DIR, "state", "session-registry.json");
|
|
677
678
|
let sessionRegistryFile = SESSION_REGISTRY_FILE;
|
|
678
679
|
|
|
679
|
-
export interface SessionRegistryUpdate {
|
|
680
|
-
chatId: string;
|
|
681
|
-
sessionId: string;
|
|
682
|
-
tool: string;
|
|
683
|
-
/** 会话容器类型;旧 registry 没有该字段,读取时必须兼容。 */
|
|
684
|
-
chatType?: string;
|
|
685
|
-
chatName?: string;
|
|
680
|
+
export interface SessionRegistryUpdate {
|
|
681
|
+
chatId: string;
|
|
682
|
+
sessionId: string;
|
|
683
|
+
tool: string;
|
|
684
|
+
/** 会话容器类型;旧 registry 没有该字段,读取时必须兼容。 */
|
|
685
|
+
chatType?: string;
|
|
686
|
+
chatName?: string;
|
|
686
687
|
turnCount?: number;
|
|
687
688
|
lastContextTokens?: number;
|
|
688
689
|
startTime?: number;
|
|
@@ -690,12 +691,12 @@ export interface SessionRegistryUpdate {
|
|
|
690
691
|
running?: boolean;
|
|
691
692
|
}
|
|
692
693
|
|
|
693
|
-
interface SessionRegistryRecord {
|
|
694
|
-
chatId: string;
|
|
695
|
-
sessionId: string;
|
|
696
|
-
tool: string;
|
|
697
|
-
chatType?: string;
|
|
698
|
-
chatName: string;
|
|
694
|
+
interface SessionRegistryRecord {
|
|
695
|
+
chatId: string;
|
|
696
|
+
sessionId: string;
|
|
697
|
+
tool: string;
|
|
698
|
+
chatType?: string;
|
|
699
|
+
chatName: string;
|
|
699
700
|
turnCount: number;
|
|
700
701
|
lastContextTokens: number;
|
|
701
702
|
startTime: number;
|
|
@@ -756,11 +757,11 @@ export async function recordSessionRegistry(update: SessionRegistryUpdate): Prom
|
|
|
756
757
|
const now = update.updatedAt ?? Date.now();
|
|
757
758
|
|
|
758
759
|
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 ?? "",
|
|
760
|
+
chatId: update.chatId,
|
|
761
|
+
sessionId: update.sessionId,
|
|
762
|
+
tool: update.tool,
|
|
763
|
+
chatType: update.chatType ?? existing?.chatType,
|
|
764
|
+
chatName: update.chatName ?? existing?.chatName ?? "",
|
|
764
765
|
turnCount: update.turnCount ?? existing?.turnCount ?? 0,
|
|
765
766
|
lastContextTokens: update.lastContextTokens ?? existing?.lastContextTokens ?? 0,
|
|
766
767
|
startTime: update.startTime ?? existing?.startTime ?? now,
|
|
@@ -1009,12 +1010,12 @@ export async function switchChatBinding(args: SwitchChatBindingArgs): Promise<Sw
|
|
|
1009
1010
|
|
|
1010
1011
|
// Step 3: 持久化(registry + sessions.json)。
|
|
1011
1012
|
// 这两步即使失败也不影响内存正确性,下次 prompt 会再写一次。
|
|
1012
|
-
await recordSessionRegistry({
|
|
1013
|
-
chatId,
|
|
1014
|
-
sessionId: newSessionId,
|
|
1015
|
-
tool,
|
|
1016
|
-
chatType,
|
|
1017
|
-
chatName,
|
|
1013
|
+
await recordSessionRegistry({
|
|
1014
|
+
chatId,
|
|
1015
|
+
sessionId: newSessionId,
|
|
1016
|
+
tool,
|
|
1017
|
+
chatType,
|
|
1018
|
+
chatName,
|
|
1018
1019
|
turnCount: initialTurnCount,
|
|
1019
1020
|
lastContextTokens: initialContextTokens,
|
|
1020
1021
|
startTime: now,
|
|
@@ -1037,58 +1038,58 @@ export async function switchChatBinding(args: SwitchChatBindingArgs): Promise<Sw
|
|
|
1037
1038
|
function formatToolConfigForLog(tool: string, sessionModel?: string, sessionId?: string): string {
|
|
1038
1039
|
if (tool === "cursor") {
|
|
1039
1040
|
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);
|
|
1041
|
+
}
|
|
1042
|
+
if (tool === "codex") {
|
|
1043
|
+
const m = getEffectiveModelForTool(tool, sessionId);
|
|
1044
|
+
const e = getEffectiveEffortForTool(tool, sessionId);
|
|
1045
|
+
const modelStr = m.trim() !== "" ? m : "(由 codex config.toml 决定)";
|
|
1046
|
+
const effortStr = e.trim() !== ""
|
|
1047
|
+
? `effort=${e}`
|
|
1048
|
+
: "effort=(由 codex config.toml 决定)";
|
|
1049
|
+
return `model=${modelStr}, ${effortStr}, fast=${getEffectiveFastModeForTool(tool, sessionId) ? "on" : "off"}`;
|
|
1050
|
+
}
|
|
1051
|
+
if (tool === "ccc") {
|
|
1052
|
+
const m = getEffectiveModelForTool(tool, sessionId);
|
|
1053
|
+
const modelStr = m.trim() !== "" ? m : "(not configured)";
|
|
1054
|
+
return `model=${modelStr}, baseURL=${config.ccc.DEEPSEEK_BASE_URL}`;
|
|
1055
|
+
}
|
|
1056
|
+
return `model=${anthropicConfigDisplay(getModelForSession(sessionId))}, subagentModel=${anthropicConfigDisplay(CLAUDE_SUBAGENT_MODEL)}, effort=${anthropicConfigDisplay(getEffectiveEffortForTool(tool, sessionId))}`;
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
export async function initClaudeSession(tool: string, overrideCwd?: string, chatId?: string): Promise<{ sessionId: string; cwd: string }> {
|
|
1060
|
+
const cwd = overrideCwd ?? (await getDefaultCwd(chatId));
|
|
1061
|
+
const adapter = getAdapterForTool(tool);
|
|
1061
1062
|
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;
|
|
1063
|
+
`[${ts()}] [STEP 1/5] Creating ${adapter.displayName} session (${formatToolConfigForLog(tool)}, cwd=${cwd})`
|
|
1064
|
+
);
|
|
1065
|
+
|
|
1066
|
+
// Claude/Cursor 创建会话时需要先等待 SDK/CLI 的 init 事件。它们若在首个
|
|
1067
|
+
// 事件前卡死,正式 turn 尚未建立,runAgentSession 的看门狗无法介入。
|
|
1068
|
+
// 因此创建入口也使用相同的三分钟阈值,并通过 AbortSignal 释放底层资源。
|
|
1069
|
+
const createController = new AbortController();
|
|
1070
|
+
let createTimeout: ReturnType<typeof setTimeout> | undefined;
|
|
1071
|
+
const timeoutError = new Error(
|
|
1072
|
+
`${adapter.displayName} session creation timed out after 3 minutes without an init event`,
|
|
1073
|
+
);
|
|
1074
|
+
const timeoutPromise = new Promise<never>((_resolve, reject) => {
|
|
1075
|
+
createTimeout = setTimeout(() => {
|
|
1076
|
+
// 先固定对外错误,再 abort 适配器,避免适配器自己的 abort 错误赢得竞态。
|
|
1077
|
+
reject(timeoutError);
|
|
1078
|
+
createController.abort();
|
|
1079
|
+
}, responseStallTimeoutMs);
|
|
1080
|
+
createTimeout.unref?.();
|
|
1081
|
+
});
|
|
1082
|
+
|
|
1083
|
+
let result: Awaited<ReturnType<ToolAdapter["createSession"]>>;
|
|
1084
|
+
try {
|
|
1085
|
+
result = await Promise.race([
|
|
1086
|
+
adapter.createSession(cwd, createController.signal),
|
|
1087
|
+
timeoutPromise,
|
|
1088
|
+
]);
|
|
1089
|
+
} finally {
|
|
1090
|
+
if (createTimeout) clearTimeout(createTimeout);
|
|
1091
|
+
}
|
|
1092
|
+
const sessionId = result.sessionId;
|
|
1092
1093
|
console.log(`[${ts()}] → sessionId: ${sessionId}`);
|
|
1093
1094
|
|
|
1094
1095
|
await saveSessionTool(sessionId, tool);
|
|
@@ -1110,41 +1111,41 @@ export async function resumeAndPrompt(
|
|
|
1110
1111
|
return runAgentSession(sessionId, userText, platform, chatId, msgTimestamp, tool, traceId);
|
|
1111
1112
|
}
|
|
1112
1113
|
|
|
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 只推送到该群)
|
|
1114
|
+
// ---------------------------------------------------------------------------
|
|
1115
|
+
// runAgentSession — session 中心的 agent prompt(文件持久化 + display 解耦)
|
|
1116
|
+
// ---------------------------------------------------------------------------
|
|
1117
|
+
|
|
1118
|
+
interface RunAgentSessionOptions {
|
|
1119
|
+
/**
|
|
1120
|
+
* 标记本轮是 response-stall 后的唯一一次内部续跑。若本轮再次因相同原因
|
|
1121
|
+
* 停滞,不再递归创建第三轮,避免服务异常期间无限消耗 token。
|
|
1122
|
+
*/
|
|
1123
|
+
autoRecovery?: boolean;
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
export async function runAgentSession(
|
|
1127
|
+
sessionId: string,
|
|
1128
|
+
userText: string,
|
|
1129
|
+
platform: PlatformAdapter,
|
|
1130
|
+
_chatId: string,
|
|
1131
|
+
msgTimestamp: number,
|
|
1132
|
+
tool: string,
|
|
1133
|
+
traceId?: string,
|
|
1134
|
+
options: RunAgentSessionOptions = {},
|
|
1135
|
+
): Promise<void> {
|
|
1136
|
+
const tid = traceId ?? "";
|
|
1137
|
+
|
|
1138
|
+
// runAgentSession 是飞书用户消息、队列消息与内部自动恢复共同使用的唯一
|
|
1139
|
+
// prompt 执行入口。即使冷启动后的历史群只靠群描述解析出 sessionId、registry
|
|
1140
|
+
// 尚未重建出内存映射,也必须在任何异步操作前补齐绑定,确保三种来源都有完全
|
|
1141
|
+
// 相同的卡片、状态和收尾行为。
|
|
1142
|
+
const previousSessionId = sessionInfoMap.get(_chatId)?.sessionId;
|
|
1143
|
+
if (previousSessionId && previousSessionId !== sessionId) {
|
|
1144
|
+
unbindChatFromSession(previousSessionId, _chatId);
|
|
1145
|
+
}
|
|
1146
|
+
bindChatToSession(sessionId, _chatId);
|
|
1147
|
+
|
|
1148
|
+
// 记录用户最后发送消息的群(display loop 只推送到该群)
|
|
1148
1149
|
// 如果是从队列消费且队列消息来自其他群,保留原来的 display chat
|
|
1149
1150
|
recordChatPlatform(_chatId, platform);
|
|
1150
1151
|
recordLastActiveChat(sessionId, consumeQueuePreservedChat(sessionId) ?? _chatId);
|
|
@@ -1165,19 +1166,19 @@ export async function runAgentSession(
|
|
|
1165
1166
|
// 注意:下面的 try/catch 在准备失败时会清理 activePrompts。
|
|
1166
1167
|
const controller = new AbortController();
|
|
1167
1168
|
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
|
-
});
|
|
1169
|
+
activePrompts.set(sessionId, {
|
|
1170
|
+
controller,
|
|
1171
|
+
stopped: false,
|
|
1172
|
+
startTime: now,
|
|
1173
|
+
autoRecovery: options.autoRecovery === true,
|
|
1174
|
+
finalResponseObserved: false,
|
|
1175
|
+
});
|
|
1175
1176
|
|
|
1176
1177
|
// 资源监控僵死检测:CPU + 内存连续 3 分钟无变化 → 强制停止
|
|
1177
1178
|
const onResourceStuck = (data: { pid: number; sessionId: string; idleMinutes: number }) => {
|
|
1178
1179
|
if (data.sessionId !== sessionId) return;
|
|
1179
1180
|
const prompt = activePrompts.get(sessionId);
|
|
1180
|
-
if (!prompt || prompt.stopped || prompt.abnormalExit || prompt.resourceStuck || prompt.autoEnded) return;
|
|
1181
|
+
if (!prompt || prompt.stopped || prompt.abnormalExit || prompt.resourceStuck || prompt.autoEnded) return;
|
|
1181
1182
|
prompt.resourceStuck = true;
|
|
1182
1183
|
|
|
1183
1184
|
const chatId = pickDisplayChat(sessionId) ?? getLastActiveChat(sessionId) ?? getChatsForSession(sessionId)[0];
|
|
@@ -1214,12 +1215,12 @@ export async function runAgentSession(
|
|
|
1214
1215
|
const imSkillsCacheDir = join(USER_DATA_DIR, "im-skills");
|
|
1215
1216
|
const skillVariables = {
|
|
1216
1217
|
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"),
|
|
1218
|
+
session_id: sessionId,
|
|
1219
|
+
im_skills_cache_dir: imSkillsCacheDir,
|
|
1220
|
+
delegate_task_url: `http://127.0.0.1:${CHATCCC_PORT}/api/agent/delegate-task`,
|
|
1221
|
+
send_image_url: `http://127.0.0.1:${CHATCCC_PORT}/api/agent/send-image`,
|
|
1222
|
+
send_file_url: `http://127.0.0.1:${CHATCCC_PORT}/api/agent/send-file`,
|
|
1223
|
+
send_image_script: join(feishuSkillDir, "send-image.mjs"),
|
|
1223
1224
|
send_file_script: join(feishuSkillDir, "send-file.mjs"),
|
|
1224
1225
|
download_video_script: join(feishuSkillDir, "download-video.mjs"),
|
|
1225
1226
|
wechat_send_image_script: join(wechatImageSkillDir, "send-image.mjs"),
|
|
@@ -1284,10 +1285,10 @@ export async function runAgentSession(
|
|
|
1284
1285
|
// 导致 finalReply 丢失、完成卡片空白。此处主动读取上一轮终端状态完成
|
|
1285
1286
|
// 卡片终结和回复发送,不依赖 display loop 时序,保证"先发完上一个回答
|
|
1286
1287
|
// 再开始缓存问题对应的任务"。
|
|
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);
|
|
1288
|
+
const prevState = await readStreamState(sessionId);
|
|
1289
|
+
if (prevState && prevState.status !== "running") {
|
|
1290
|
+
const prevTerminalReply = formatTerminalReply(prevState.status, prevState.finalReply);
|
|
1291
|
+
const displayChatId = pickDisplayChat(sessionId);
|
|
1291
1292
|
if (displayChatId) {
|
|
1292
1293
|
const pp = platformForChat(displayChatId);
|
|
1293
1294
|
const display = displayCards.get(displayChatId);
|
|
@@ -1302,12 +1303,12 @@ export async function runAgentSession(
|
|
|
1302
1303
|
if (displayCards.get(displayChatId) !== display) {
|
|
1303
1304
|
const finalStatus = turnFinalStatus(prevState.status);
|
|
1304
1305
|
finalizeTurnCards(sessionId, prevState.turnCount, finalStatus).catch(() => {});
|
|
1305
|
-
setSessionChatAvatar(pp, displayChatId, prevState.tool, "idle", sessionId).catch(() => {});
|
|
1306
|
+
setSessionChatAvatar(pp, displayChatId, prevState.tool, "idle", sessionId).catch(() => {});
|
|
1306
1307
|
} else {
|
|
1307
1308
|
const nextSeq = display.sequence + 1;
|
|
1308
1309
|
const { title: headerTitle, template: headerTemplate } = formatTerminalHeader(prevState.status);
|
|
1309
1310
|
const cardContent = truncateContent(prevState.accumulatedContent + prevState.finalReply) || " ";
|
|
1310
|
-
const doneCard = buildProgressCard(cardContent,
|
|
1311
|
+
const doneCard = buildProgressCard(progressView({ text: cardContent, status: "done", showStop: false, headerTitle, headerTemplate }));
|
|
1311
1312
|
await pp.cardUpdate(display.cardId, doneCard, nextSeq).catch(err => {
|
|
1312
1313
|
console.error(`[${ts()}] [DISPLAY] prevState final cardUpdate failed: ${(err as Error).message}`);
|
|
1313
1314
|
});
|
|
@@ -1319,49 +1320,49 @@ export async function runAgentSession(
|
|
|
1319
1320
|
const finalStatus = turnFinalStatus(prevState.status);
|
|
1320
1321
|
finalizeTurnCards(sessionId, prevState.turnCount, finalStatus).catch(() => {});
|
|
1321
1322
|
|
|
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(() => {});
|
|
1323
|
+
if (prevTerminalReply && stillOursAfterUpdate && !isFinalReplySentForTurn(prevState)) {
|
|
1324
|
+
await sendFinalReplyTextOnce(pp, displayChatId, sessionId, prevState.turnCount, prevTerminalReply);
|
|
1325
|
+
}
|
|
1326
|
+
setSessionChatAvatar(pp, displayChatId, prevState.tool, "idle", sessionId).catch(() => {});
|
|
1326
1327
|
}
|
|
1327
|
-
} else if (pp && prevTerminalReply && !isFinalReplySentForTurn(prevState)) {
|
|
1328
|
+
} else if (pp && prevTerminalReply && !isFinalReplySentForTurn(prevState)) {
|
|
1328
1329
|
// 无 display 记录但上一轮有 finalReply(极快轮次),至少发送
|
|
1329
1330
|
const finalStatus = turnFinalStatus(prevState.status);
|
|
1330
1331
|
finalizeTurnCards(sessionId, prevState.turnCount, finalStatus).catch(() => {});
|
|
1331
|
-
await sendFinalReplyTextOnce(pp, displayChatId, sessionId, prevState.turnCount, prevTerminalReply);
|
|
1332
|
+
await sendFinalReplyTextOnce(pp, displayChatId, sessionId, prevState.turnCount, prevTerminalReply);
|
|
1332
1333
|
}
|
|
1333
1334
|
// else: displayCards 无记录且无 finalReply → 无需处理
|
|
1334
1335
|
}
|
|
1335
1336
|
}
|
|
1336
1337
|
|
|
1337
1338
|
// 初始化 stream-state.json
|
|
1338
|
-
const initialState = createEmptyStreamState(sessionId, cwd, tool, nextTurnCount);
|
|
1339
|
-
const activityTracker = createAgentActivityTracker(initialState.activity?.startedAt ?? Date.now());
|
|
1340
|
-
await writeStreamState(initialState);
|
|
1339
|
+
const initialState = createEmptyStreamState(sessionId, cwd, tool, nextTurnCount);
|
|
1340
|
+
const activityTracker = createAgentActivityTracker(initialState.activity?.startedAt ?? Date.now());
|
|
1341
|
+
await writeStreamState(initialState);
|
|
1341
1342
|
|
|
1342
1343
|
// 为新 turn 创建第一张展示卡片,同时注册到 turn-cards 持久化。
|
|
1343
1344
|
// 统一 display loop 始终运行,卡片创建后下一个 tick 即自动开始更新。
|
|
1344
1345
|
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(
|
|
1346
|
+
if (displayChatIdForNew) {
|
|
1347
|
+
const ppNew = platformForChat(displayChatIdForNew);
|
|
1348
|
+
if (ppNew && ppNew.kind !== "wechat") {
|
|
1349
|
+
const initialHeaderTitle = formatAgentActivityTitle(activityTracker.activity);
|
|
1350
|
+
const cardId = await createVisibleProgressCard(
|
|
1350
1351
|
ppNew,
|
|
1351
1352
|
displayChatIdForNew,
|
|
1352
1353
|
sessionId,
|
|
1353
|
-
nextTurnCount,
|
|
1354
|
-
"生成中卡片发送失败,结果将以文本形式发送。",
|
|
1355
|
-
initialHeaderTitle,
|
|
1354
|
+
nextTurnCount,
|
|
1355
|
+
"生成中卡片发送失败,结果将以文本形式发送。",
|
|
1356
|
+
initialHeaderTitle,
|
|
1356
1357
|
);
|
|
1357
1358
|
if (cardId) {
|
|
1358
1359
|
displayCards.set(displayChatIdForNew, {
|
|
1359
1360
|
cardId,
|
|
1360
1361
|
sequence: 1,
|
|
1361
1362
|
cardBusy: false,
|
|
1362
|
-
cardCreatedAt: Date.now(),
|
|
1363
|
-
lastSentContent: "",
|
|
1364
|
-
lastSentHeaderTitle: initialHeaderTitle,
|
|
1363
|
+
cardCreatedAt: Date.now(),
|
|
1364
|
+
lastSentContent: "",
|
|
1365
|
+
lastSentHeaderTitle: initialHeaderTitle,
|
|
1365
1366
|
streamErrorNotified: false,
|
|
1366
1367
|
sessionId,
|
|
1367
1368
|
turnCount: nextTurnCount,
|
|
@@ -1387,7 +1388,7 @@ export async function runAgentSession(
|
|
|
1387
1388
|
// 设置最后活跃群头像为 busy
|
|
1388
1389
|
const activeCid = getLastActiveChat(sessionId) ?? getChatsForSession(sessionId)[0];
|
|
1389
1390
|
if (activeCid) {
|
|
1390
|
-
setSessionChatAvatar(platform, activeCid, tool, "busy", sessionId).catch(() => {});
|
|
1391
|
+
setSessionChatAvatar(platform, activeCid, tool, "busy", sessionId).catch(() => {});
|
|
1391
1392
|
}
|
|
1392
1393
|
|
|
1393
1394
|
const state: AccumulatorState = {
|
|
@@ -1397,106 +1398,106 @@ export async function runAgentSession(
|
|
|
1397
1398
|
chunkCount: 0,
|
|
1398
1399
|
};
|
|
1399
1400
|
|
|
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, {
|
|
1401
|
+
let lastFileWrite = Date.now();
|
|
1402
|
+
const FILE_WRITE_INTERVAL_MS = 2000;
|
|
1403
|
+
const toolCallMap = new Map<string, { name: string; input: unknown }>();
|
|
1404
|
+
let streamErrored = false;
|
|
1405
|
+
|
|
1406
|
+
const runningPrompt = activePrompts.get(sessionId);
|
|
1407
|
+
if (runningPrompt) {
|
|
1408
|
+
// 必须在消费第一个事件前建立零字符基线。部分 CLI 卡死时只启动了进程,
|
|
1409
|
+
// 甚至一个事件都不会 yield;若等循环体更新进度,这种会话会永久停在
|
|
1410
|
+
// “正在启动 Agent”,也永远触发不了三分钟保护。
|
|
1411
|
+
runningPrompt.responseProgress = observeResponseProgress(
|
|
1412
|
+
undefined,
|
|
1413
|
+
true,
|
|
1414
|
+
0,
|
|
1415
|
+
activityTracker.activity.startedAt,
|
|
1416
|
+
);
|
|
1417
|
+
|
|
1418
|
+
const checkResponseStall = async () => {
|
|
1419
|
+
const current = activePrompts.get(sessionId);
|
|
1420
|
+
if (!current || current !== runningPrompt) {
|
|
1421
|
+
clearPromptResponseStallMonitor(sessionId);
|
|
1422
|
+
return;
|
|
1423
|
+
}
|
|
1424
|
+
if (
|
|
1425
|
+
current.stopped
|
|
1426
|
+
|| current.abnormalExit
|
|
1427
|
+
|| current.resourceStuck
|
|
1428
|
+
|| current.autoEnded
|
|
1429
|
+
|| current.finalResponseObserved
|
|
1430
|
+
|| !monitorsOutputProgress(activityTracker.activity.kind)
|
|
1431
|
+
|| !hasResponseStalled(current.responseProgress, Date.now(), responseStallTimeoutMs)
|
|
1432
|
+
) {
|
|
1433
|
+
return;
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
const autoEndedAt = Date.now();
|
|
1437
|
+
// 普通轮第一次因回复停滞结束时,立即预约同 session 的内部续跑。
|
|
1438
|
+
// 预约先于 abort/收尾建立,isSessionRunning 会在整个交接窗口保持 true,
|
|
1439
|
+
// 因此恰好到达的用户消息只能排队,绝不可能抢在恢复 prompt 前。
|
|
1440
|
+
// 若当前已经是恢复轮,则不再预约第三轮。
|
|
1441
|
+
if (!current.autoRecovery) {
|
|
1442
|
+
reserveAutoRecovery(sessionId);
|
|
1443
|
+
}
|
|
1444
|
+
current.autoEnded = true;
|
|
1445
|
+
current.autoEndedAt = autoEndedAt;
|
|
1446
|
+
clearPromptResponseStallMonitor(sessionId);
|
|
1447
|
+
clearPromptProcessMonitor(sessionId);
|
|
1448
|
+
|
|
1449
|
+
// First publish an atomic terminal state so the card cannot keep claiming the
|
|
1450
|
+
// Agent is running while process cleanup is underway.
|
|
1451
|
+
await writeStreamState({
|
|
1452
|
+
sessionId,
|
|
1453
|
+
status: "auto_ended",
|
|
1454
|
+
accumulatedContent: state.accumulatedContent,
|
|
1455
|
+
finalReply: pickFinalReply(state).trim(),
|
|
1456
|
+
activity: activityTracker.activity,
|
|
1457
|
+
chunkCount: state.chunkCount,
|
|
1458
|
+
turnCount: nextTurnCount,
|
|
1459
|
+
contextTokens: existingInfo?.lastContextTokens ?? 0,
|
|
1460
|
+
updatedAt: autoEndedAt,
|
|
1461
|
+
cwd,
|
|
1462
|
+
tool,
|
|
1463
|
+
autoEndedAt,
|
|
1464
|
+
});
|
|
1465
|
+
|
|
1466
|
+
// 最终事件可能在上面的落盘 I/O 期间到达。只有适配器明确标记的完整
|
|
1467
|
+
// final response 才能赢得这场竞态;普通文本片段绝不能取消超时。
|
|
1468
|
+
if (current.finalResponseObserved) {
|
|
1469
|
+
current.autoEnded = false;
|
|
1470
|
+
current.autoEndedAt = undefined;
|
|
1471
|
+
cancelAutoRecoveryReservation(sessionId);
|
|
1472
|
+
console.log(
|
|
1473
|
+
`[${ts()}] [RESPONSE-STALL] Authoritative final response won timeout race for ${sessionId}`,
|
|
1474
|
+
);
|
|
1475
|
+
return;
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
try {
|
|
1479
|
+
current.closeSession?.();
|
|
1480
|
+
} catch (err) {
|
|
1481
|
+
console.warn(`[${ts()}] [RESPONSE-STALL] closeSession failed for ${sessionId}: ${(err as Error).message}`);
|
|
1482
|
+
}
|
|
1483
|
+
current.controller.abort();
|
|
1484
|
+
await killProcessTree(current.processPid);
|
|
1485
|
+
console.warn(
|
|
1486
|
+
`[${ts()}] [RESPONSE-STALL] Session ${sessionId} auto-ended after 3 minutes without startup or reply progress`,
|
|
1487
|
+
);
|
|
1488
|
+
};
|
|
1489
|
+
|
|
1490
|
+
const responseStallMonitor = setInterval(() => {
|
|
1491
|
+
void checkResponseStall().catch((err) => {
|
|
1492
|
+
console.warn(`[${ts()}] [RESPONSE-STALL] check failed for ${sessionId}: ${(err as Error).message}`);
|
|
1493
|
+
});
|
|
1494
|
+
}, responseStallCheckIntervalMs);
|
|
1495
|
+
responseStallMonitor.unref?.();
|
|
1496
|
+
runningPrompt.responseStallMonitor = responseStallMonitor;
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
try {
|
|
1500
|
+
for await (const unifiedMsg of adapter.prompt(sessionId, userTextWithCapabilities, cwd, controller.signal, {
|
|
1500
1501
|
onProcessStart: (processInfo) => {
|
|
1501
1502
|
startPromptProcessMonitor(sessionId, processInfo);
|
|
1502
1503
|
if (processInfo.pid !== undefined) registerProcess(processInfo.pid, sessionId);
|
|
@@ -1505,27 +1506,27 @@ export async function runAgentSession(
|
|
|
1505
1506
|
clearPromptProcessMonitor(sessionId);
|
|
1506
1507
|
if (exitInfo.pid !== undefined) unregisterProcess(exitInfo.pid);
|
|
1507
1508
|
},
|
|
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);
|
|
1509
|
+
onSessionCreated: (closeSession) => {
|
|
1510
|
+
const prompt = activePrompts.get(sessionId);
|
|
1511
|
+
if (prompt) prompt.closeSession = closeSession;
|
|
1512
|
+
},
|
|
1513
|
+
})) {
|
|
1514
|
+
if (unifiedMsg.isFinalResponse) {
|
|
1515
|
+
const prompt = activePrompts.get(sessionId);
|
|
1516
|
+
if (prompt && prompt === runningPrompt) {
|
|
1517
|
+
// 同步标记必须发生在任何 await 之前,让 watchdog 无法在已收到完整
|
|
1518
|
+
// 最终事件后仍把本轮判为停滞。
|
|
1519
|
+
if (!prompt.finalResponseObserved) {
|
|
1520
|
+
prompt.finalResponseObserved = true;
|
|
1521
|
+
scheduleFinalResponseCloseGuard(sessionId, prompt);
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
let activityChanged = false;
|
|
1527
|
+
for (const block of unifiedMsg.blocks) {
|
|
1528
|
+
if (updateAgentActivity(activityTracker, block)) activityChanged = true;
|
|
1529
|
+
accumulateBlockContent(block, state, toolCallMap);
|
|
1529
1530
|
|
|
1530
1531
|
if (block.type === "compact_boundary" && block.post_tokens) {
|
|
1531
1532
|
for (const cid of getChatsForSession(sessionId)) {
|
|
@@ -1539,32 +1540,32 @@ export async function runAgentSession(
|
|
|
1539
1540
|
lastContextTokens: block.post_tokens,
|
|
1540
1541
|
running: true,
|
|
1541
1542
|
});
|
|
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
|
-
// 定时写入文件
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
const prompt = activePrompts.get(sessionId);
|
|
1547
|
+
if (prompt && !prompt.autoEnded) {
|
|
1548
|
+
const totalChars = state.accumulatedContent.length + pickFinalReply(state).length;
|
|
1549
|
+
prompt.responseProgress = observeResponseProgress(
|
|
1550
|
+
// starting → responding 本身是一次有效进展,即便首个文本块仍为空也应
|
|
1551
|
+
// 重新计时;其它活动阶段会清空观察窗口。
|
|
1552
|
+
activityChanged ? undefined : prompt.responseProgress,
|
|
1553
|
+
monitorsOutputProgress(activityTracker.activity.kind),
|
|
1554
|
+
totalChars,
|
|
1555
|
+
Date.now(),
|
|
1556
|
+
);
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
// 定时写入文件
|
|
1559
1560
|
const now2 = Date.now();
|
|
1560
|
-
if (activityChanged || now2 - lastFileWrite >= FILE_WRITE_INTERVAL_MS) {
|
|
1561
|
-
lastFileWrite = now2;
|
|
1562
|
-
await writeStreamState({
|
|
1561
|
+
if (activityChanged || now2 - lastFileWrite >= FILE_WRITE_INTERVAL_MS) {
|
|
1562
|
+
lastFileWrite = now2;
|
|
1563
|
+
await writeStreamState({
|
|
1563
1564
|
sessionId,
|
|
1564
1565
|
status: "running",
|
|
1565
|
-
accumulatedContent: state.accumulatedContent,
|
|
1566
|
-
finalReply: pickFinalReply(state),
|
|
1567
|
-
activity: activityTracker.activity,
|
|
1566
|
+
accumulatedContent: state.accumulatedContent,
|
|
1567
|
+
finalReply: pickFinalReply(state),
|
|
1568
|
+
activity: activityTracker.activity,
|
|
1568
1569
|
chunkCount: state.chunkCount,
|
|
1569
1570
|
turnCount: nextTurnCount,
|
|
1570
1571
|
contextTokens: existingInfo?.lastContextTokens ?? 0,
|
|
@@ -1573,58 +1574,58 @@ export async function runAgentSession(
|
|
|
1573
1574
|
tool,
|
|
1574
1575
|
});
|
|
1575
1576
|
}
|
|
1576
|
-
}
|
|
1577
|
-
} catch (streamErr) {
|
|
1578
|
-
streamErrored = true;
|
|
1579
|
-
console.error(`[${ts()}] [STREAM] Error in stream loop for ${sessionId}: ${(streamErr as Error).message}`);
|
|
1580
|
-
} finally {
|
|
1577
|
+
}
|
|
1578
|
+
} catch (streamErr) {
|
|
1579
|
+
streamErrored = true;
|
|
1580
|
+
console.error(`[${ts()}] [STREAM] Error in stream loop for ${sessionId}: ${(streamErr as Error).message}`);
|
|
1581
|
+
} finally {
|
|
1581
1582
|
// 标记 prompt 结束
|
|
1582
1583
|
resourceMonitor.off("stuck", onResourceStuck);
|
|
1583
1584
|
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();
|
|
1585
|
+
const wasStopped = prompt?.stopped ?? false;
|
|
1586
|
+
const wasAbnormalExit = prompt?.abnormalExit ?? false;
|
|
1587
|
+
const wasResourceStuck = prompt?.resourceStuck ?? false;
|
|
1588
|
+
const timeoutTriggered = prompt?.autoEnded ?? false;
|
|
1589
|
+
const completedAtTimeoutBoundary =
|
|
1590
|
+
timeoutTriggered && (prompt?.finalResponseObserved ?? false);
|
|
1591
|
+
const wasAutoEnded = timeoutTriggered && !completedAtTimeoutBoundary;
|
|
1592
|
+
const wasAutoRecovery = prompt?.autoRecovery ?? false;
|
|
1593
|
+
const autoEndedAt = wasAutoEnded ? prompt?.autoEndedAt : undefined;
|
|
1594
|
+
clearPromptResponseStallMonitor(sessionId);
|
|
1595
|
+
clearPromptProcessMonitor(sessionId);
|
|
1596
|
+
clearPromptFinalResponseCloseTimer(sessionId);
|
|
1597
|
+
markSessionFinalizing(sessionId);
|
|
1598
|
+
activePrompts.delete(sessionId);
|
|
1599
|
+
|
|
1600
|
+
try {
|
|
1601
|
+
if (completedAtTimeoutBoundary) {
|
|
1602
|
+
// reservation 可能在 watchdog 开始终止普通轮时已经建立。最终回复若在
|
|
1603
|
+
// abort/kill 清理边界到达,本轮按完成处理,并原子取消尚未启动的恢复轮。
|
|
1604
|
+
cancelAutoRecoveryReservation(sessionId);
|
|
1605
|
+
console.log(
|
|
1606
|
+
`[${ts()}] [RESPONSE-STALL] Session ${sessionId} completed with an authoritative final response during timeout cleanup`,
|
|
1607
|
+
);
|
|
1608
|
+
}
|
|
1609
|
+
// 即使运行期间映射被异常清空,也必须更新本次实际触发 chat,避免 registry
|
|
1610
|
+
// 永久残留 running=true。
|
|
1611
|
+
const finalizationChatIds = [...new Set([
|
|
1612
|
+
...getChatsForSession(sessionId),
|
|
1613
|
+
_chatId,
|
|
1614
|
+
])];
|
|
1615
|
+
// 先写最终状态(done/stopped),确保 display loop 在下一轮消费前
|
|
1616
|
+
// 读到新状态并终结旧卡片。否则 setImmediate 在 CHECK 阶段先于
|
|
1617
|
+
// writeFile I/O(POLL 阶段)执行,display loop 会误以为旧轮仍在
|
|
1618
|
+
// 运行中并更新旧卡片,而不是新建卡片。
|
|
1619
|
+
const finalStatus = completedAtTimeoutBoundary
|
|
1620
|
+
? "done"
|
|
1621
|
+
: wasAutoEnded
|
|
1622
|
+
? "auto_ended"
|
|
1623
|
+
: (streamErrored || wasAbnormalExit || wasResourceStuck)
|
|
1624
|
+
? "error"
|
|
1625
|
+
: wasStopped
|
|
1626
|
+
? "stopped"
|
|
1627
|
+
: "done";
|
|
1628
|
+
const finalReply = pickFinalReply(state).trim();
|
|
1628
1629
|
|
|
1629
1630
|
// stop-stuck-loop 接口可能在 fire-and-forget 中已写入带 final_reply 的
|
|
1630
1631
|
// stream state,finally 不应覆盖它。同时保留 stuckAt 标记,防止
|
|
@@ -1644,25 +1645,25 @@ export async function runAgentSession(
|
|
|
1644
1645
|
await writeStreamState({
|
|
1645
1646
|
sessionId,
|
|
1646
1647
|
status: finalStatus,
|
|
1647
|
-
accumulatedContent: state.accumulatedContent,
|
|
1648
|
-
finalReply: finalReplyToWrite,
|
|
1649
|
-
activity: activityTracker.activity,
|
|
1648
|
+
accumulatedContent: state.accumulatedContent,
|
|
1649
|
+
finalReply: finalReplyToWrite,
|
|
1650
|
+
activity: activityTracker.activity,
|
|
1650
1651
|
chunkCount: state.chunkCount,
|
|
1651
1652
|
turnCount: nextTurnCount,
|
|
1652
1653
|
contextTokens: existingInfo?.lastContextTokens ?? 0,
|
|
1653
1654
|
updatedAt: Date.now(),
|
|
1654
1655
|
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) {
|
|
1656
|
+
tool,
|
|
1657
|
+
...(preserveStuckAt ? { stuckAt: preserveStuckAt } : {}),
|
|
1658
|
+
...(autoEndedAt !== undefined ? { autoEndedAt } : {}),
|
|
1659
|
+
});
|
|
1660
|
+
|
|
1661
|
+
// display loop 下一轮会读到最终状态并发送消息
|
|
1662
|
+
|
|
1663
|
+
let autoRecoveryTarget: { chatId: string; platform: PlatformAdapter } | undefined;
|
|
1664
|
+
|
|
1665
|
+
if (wasStopped) {
|
|
1666
|
+
for (const cid of finalizationChatIds) {
|
|
1666
1667
|
const finfo = sessionInfoMap.get(cid);
|
|
1667
1668
|
await recordSessionRegistry({
|
|
1668
1669
|
chatId: cid,
|
|
@@ -1674,62 +1675,62 @@ export async function runAgentSession(
|
|
|
1674
1675
|
running: false,
|
|
1675
1676
|
});
|
|
1676
1677
|
}
|
|
1677
|
-
const active1 = getLastActiveChat(sessionId) ?? finalizationChatIds[0];
|
|
1678
|
+
const active1 = getLastActiveChat(sessionId) ?? finalizationChatIds[0];
|
|
1678
1679
|
if (active1) {
|
|
1679
1680
|
await platform.sendText(active1, "会话已停止。").catch(() => {});
|
|
1680
|
-
setSessionChatAvatar(platform, active1, tool, "idle", sessionId).catch(() => {});
|
|
1681
|
+
setSessionChatAvatar(platform, active1, tool, "idle", sessionId).catch(() => {});
|
|
1681
1682
|
}
|
|
1682
1683
|
console.log(`[${ts()}] Session ${sessionId} stopped (content chunks: ${state.chunkCount})`);
|
|
1683
1684
|
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) {
|
|
1685
|
+
} else if (wasAutoEnded) {
|
|
1686
|
+
for (const cid of finalizationChatIds) {
|
|
1687
|
+
const finfo = sessionInfoMap.get(cid);
|
|
1688
|
+
await recordSessionRegistry({
|
|
1689
|
+
chatId: cid,
|
|
1690
|
+
sessionId,
|
|
1691
|
+
tool,
|
|
1692
|
+
turnCount: finfo?.turnCount ?? nextTurnCount,
|
|
1693
|
+
lastContextTokens: finfo?.lastContextTokens ?? nextContextTokens,
|
|
1694
|
+
startTime: finfo?.startTime ?? now,
|
|
1695
|
+
running: false,
|
|
1696
|
+
});
|
|
1697
|
+
}
|
|
1698
|
+
const activeAutoEnded = getLastActiveChat(sessionId) ?? finalizationChatIds[0];
|
|
1699
|
+
if (activeAutoEnded) {
|
|
1700
|
+
const pp = platformForChat(activeAutoEnded) ?? platform;
|
|
1701
|
+
const terminalState = await readStreamState(sessionId);
|
|
1702
|
+
if (!displayCards.has(activeAutoEnded) && (!terminalState || !isFinalReplySentForTurn(terminalState))) {
|
|
1703
|
+
await sendFinalReplyTextOnce(
|
|
1704
|
+
pp,
|
|
1705
|
+
activeAutoEnded,
|
|
1706
|
+
sessionId,
|
|
1707
|
+
nextTurnCount,
|
|
1708
|
+
formatAutoEndedReply(finalReplyToWrite),
|
|
1709
|
+
);
|
|
1710
|
+
}
|
|
1711
|
+
setSessionChatAvatar(pp, activeAutoEnded, tool, "idle", sessionId).catch(() => {});
|
|
1712
|
+
|
|
1713
|
+
if (wasAutoRecovery) {
|
|
1714
|
+
// 这是紧接第一次停滞而启动的恢复轮;再次发生相同停滞即终止
|
|
1715
|
+
// 自动链,避免第三轮及之后的无限续跑。
|
|
1716
|
+
await pp.sendText(
|
|
1717
|
+
activeAutoEnded,
|
|
1718
|
+
RESPONSE_STALL_RECOVERY_EXHAUSTED_NOTICE,
|
|
1719
|
+
).catch(() => {});
|
|
1720
|
+
} else if (hasAutoRecoveryReservation(sessionId)) {
|
|
1721
|
+
// 用户可见提示与内部恢复 prompt 分离:提示发送失败不影响恢复,
|
|
1722
|
+
// 内部恢复仍由 reservation 保证先于普通缓存消息。
|
|
1723
|
+
await pp.sendText(
|
|
1724
|
+
activeAutoEnded,
|
|
1725
|
+
RESPONSE_STALL_RECOVERY_NOTICE,
|
|
1726
|
+
).catch(() => {});
|
|
1727
|
+
autoRecoveryTarget = { chatId: activeAutoEnded, platform: pp };
|
|
1728
|
+
}
|
|
1729
|
+
}
|
|
1730
|
+
console.warn(`[${ts()}] Session ${sessionId} auto-ended after stalled startup or response output (content chunks: ${state.chunkCount})`);
|
|
1731
|
+
if (tid) logTrace(tid, "SESSION_END", { sessionId, outcome: "response_stall", chunks: state.chunkCount });
|
|
1732
|
+
} else if (wasAbnormalExit) {
|
|
1733
|
+
for (const cid of finalizationChatIds) {
|
|
1733
1734
|
const finfo = sessionInfoMap.get(cid);
|
|
1734
1735
|
await recordSessionRegistry({
|
|
1735
1736
|
chatId: cid,
|
|
@@ -1741,12 +1742,12 @@ export async function runAgentSession(
|
|
|
1741
1742
|
running: false,
|
|
1742
1743
|
});
|
|
1743
1744
|
}
|
|
1744
|
-
const activeErr = getLastActiveChat(sessionId) ?? finalizationChatIds[0];
|
|
1745
|
-
if (activeErr) setSessionChatAvatar(platform, activeErr, tool, "idle", sessionId).catch(() => {});
|
|
1745
|
+
const activeErr = getLastActiveChat(sessionId) ?? finalizationChatIds[0];
|
|
1746
|
+
if (activeErr) setSessionChatAvatar(platform, activeErr, tool, "idle", sessionId).catch(() => {});
|
|
1746
1747
|
console.log(`[${ts()}] Session ${sessionId} process exited unexpectedly (content chunks: ${state.chunkCount})`);
|
|
1747
1748
|
if (tid) logTrace(tid, "SESSION_END", { sessionId, outcome: "process_missing", chunks: state.chunkCount });
|
|
1748
|
-
} else {
|
|
1749
|
-
for (const cid of finalizationChatIds) {
|
|
1749
|
+
} else {
|
|
1750
|
+
for (const cid of finalizationChatIds) {
|
|
1750
1751
|
const finfo = sessionInfoMap.get(cid);
|
|
1751
1752
|
await recordSessionRegistry({
|
|
1752
1753
|
chatId: cid,
|
|
@@ -1758,107 +1759,107 @@ export async function runAgentSession(
|
|
|
1758
1759
|
running: false,
|
|
1759
1760
|
});
|
|
1760
1761
|
}
|
|
1761
|
-
const active2 = getLastActiveChat(sessionId) ?? finalizationChatIds[0];
|
|
1762
|
+
const active2 = getLastActiveChat(sessionId) ?? finalizationChatIds[0];
|
|
1762
1763
|
if (active2) {
|
|
1763
1764
|
const terminalState = await readStreamState(sessionId);
|
|
1764
1765
|
if (finalReply && !displayCards.has(active2) && (!terminalState || !isFinalReplySentForTurn(terminalState))) {
|
|
1765
1766
|
const pp = platformForChat(active2) ?? platform;
|
|
1766
1767
|
await sendFinalReplyTextOnce(pp, active2, sessionId, nextTurnCount, finalReply);
|
|
1767
1768
|
}
|
|
1768
|
-
setSessionChatAvatar(platform, active2, tool, "idle", sessionId).catch(() => {});
|
|
1769
|
+
setSessionChatAvatar(platform, active2, tool, "idle", sessionId).catch(() => {});
|
|
1770
|
+
}
|
|
1771
|
+
console.log(`[${ts()}] Session ${sessionId} stream complete (content chunks: ${state.chunkCount})`);
|
|
1772
|
+
if (tid) logTrace(tid, "SESSION_END", { sessionId, chunks: state.chunkCount, finalTextLen: finalReply.length });
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1775
|
+
// 失去聊天绑定时无法安全选择恢复轮的展示目标,取消本次进程内预约。
|
|
1776
|
+
if (
|
|
1777
|
+
wasAutoEnded
|
|
1778
|
+
&& hasAutoRecoveryReservation(sessionId)
|
|
1779
|
+
&& !autoRecoveryTarget
|
|
1780
|
+
) {
|
|
1781
|
+
cancelAutoRecoveryReservation(sessionId);
|
|
1782
|
+
}
|
|
1783
|
+
const shouldScheduleAutoRecovery =
|
|
1784
|
+
autoRecoveryTarget !== undefined
|
|
1785
|
+
&& hasAutoRecoveryReservation(sessionId);
|
|
1786
|
+
|
|
1787
|
+
// 必须等本轮最终卡片、registry 和头像全部收尾后再取出并调度队列消息。
|
|
1788
|
+
// 在收尾期间新到达的消息也会因 finalizingSessions 被正确排入这里。
|
|
1789
|
+
let queuedForConsumption: ReturnType<typeof dequeueMessage> = undefined;
|
|
1790
|
+
if (wasStopped) {
|
|
1791
|
+
const discarded = dequeueMessage(sessionId);
|
|
1792
|
+
if (discarded) {
|
|
1793
|
+
console.log(`[${ts()}] [QUEUE] Discarding queued message for stopped session ${sessionId}`);
|
|
1794
|
+
}
|
|
1795
|
+
} else if (!shouldScheduleAutoRecovery) {
|
|
1796
|
+
// 第一次 response-stall 后保留普通缓存;恢复轮结束后由恢复轮的
|
|
1797
|
+
// finally 再消费,顺序固定为“自动恢复 → 用户缓存”。
|
|
1798
|
+
queuedForConsumption = dequeueMessage(sessionId);
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1801
|
+
if (queuedForConsumption) {
|
|
1802
|
+
const queued = queuedForConsumption;
|
|
1803
|
+
// 队列消息可能来自其他群,保存当前 display chat 避免 display loop 被
|
|
1804
|
+
// 错误重定向(runAgentSession 会 consumeQueuePreservedChat 并在存在时
|
|
1805
|
+
// 用保存的 chat 替代 queued.chatId 作为 display 目标)。
|
|
1806
|
+
const preservedChat = getLastActiveChat(sessionId);
|
|
1807
|
+
if (preservedChat && preservedChat !== queued.chatId) {
|
|
1808
|
+
setQueuePreservedChat(sessionId, preservedChat);
|
|
1769
1809
|
}
|
|
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
|
-
}
|
|
1810
|
+
console.log(`[${ts()}] [QUEUE] Consuming queued message for session ${sessionId}: "${queued.text.slice(0, 50)}"`);
|
|
1811
|
+
// setTimeout 而非 setImmediate:给 display loop 的 setInterval
|
|
1812
|
+
// 足够时间读到最终状态并终结旧卡片,避免新轮更新旧卡片。
|
|
1813
|
+
setTimeout(() => {
|
|
1814
|
+
consumeQueuedMessage(platform, queued);
|
|
1815
|
+
}, RESPONSE_STALL_RECOVERY_DELAY_MS);
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1818
|
+
if (shouldScheduleAutoRecovery && autoRecoveryTarget) {
|
|
1819
|
+
const target = autoRecoveryTarget;
|
|
1820
|
+
console.log(
|
|
1821
|
+
`[${ts()}] [RESPONSE-STALL] Reserved automatic recovery for session ${sessionId}`,
|
|
1822
|
+
);
|
|
1823
|
+
// 延迟与普通队列原有策略一致,让上一轮终态先完成展示。reservation
|
|
1824
|
+
// 在定时器等待期间仍令 isSessionRunning=true;调用 runAgentSession
|
|
1825
|
+
// 时会在首次 await 前同步写入 activePrompts,然后才消费 reservation,
|
|
1826
|
+
// 因而不存在普通用户消息可插入的事件循环空窗。
|
|
1827
|
+
setTimeout(() => {
|
|
1828
|
+
if (!hasAutoRecoveryReservation(sessionId)) return;
|
|
1829
|
+
const recoveryRun = runAgentSession(
|
|
1830
|
+
sessionId,
|
|
1831
|
+
RESPONSE_STALL_RECOVERY_PROMPT,
|
|
1832
|
+
target.platform,
|
|
1833
|
+
target.chatId,
|
|
1834
|
+
Date.now(),
|
|
1835
|
+
tool,
|
|
1836
|
+
undefined,
|
|
1837
|
+
{ autoRecovery: true },
|
|
1838
|
+
);
|
|
1839
|
+
consumeAutoRecoveryReservation(sessionId);
|
|
1840
|
+
void recoveryRun.catch((err) => {
|
|
1841
|
+
console.error(
|
|
1842
|
+
`[${ts()}] [RESPONSE-STALL] Automatic recovery failed for ${sessionId}: ${(err as Error).message}`,
|
|
1843
|
+
);
|
|
1844
|
+
target.platform.sendText(
|
|
1845
|
+
target.chatId,
|
|
1846
|
+
`⚠️ 自动续跑启动失败:${(err as Error).message}`,
|
|
1847
|
+
).catch(() => {});
|
|
1848
|
+
|
|
1849
|
+
// 若恢复轮在进入主 stream try/finally 前即准备失败,它不会自然
|
|
1850
|
+
// 消费此前保留的用户缓存;在错误回调中补做一次,避免队列悬挂。
|
|
1851
|
+
const queued = dequeueMessage(sessionId);
|
|
1852
|
+
if (queued) {
|
|
1853
|
+
consumeQueuedMessage(target.platform, queued);
|
|
1854
|
+
}
|
|
1855
|
+
});
|
|
1856
|
+
}, RESPONSE_STALL_RECOVERY_DELAY_MS);
|
|
1857
|
+
}
|
|
1858
|
+
} finally {
|
|
1859
|
+
clearSessionFinalizing(sessionId);
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1862
1863
|
|
|
1863
1864
|
// ---------------------------------------------------------------------------
|
|
1864
1865
|
// startUnifiedDisplayLoop — 全局统一 display 循环,遍历 displayCards 更新卡片
|
|
@@ -1932,14 +1933,14 @@ export function startUnifiedDisplayLoop(): void {
|
|
|
1932
1933
|
// 还没执行,当前 stream state 可能是 stopSession fire-and-forget
|
|
1933
1934
|
// 写入的,finalReply 滞后于内存态。跳过发送,等 finally 落盘后
|
|
1934
1935
|
// 下一次 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;
|
|
1936
|
+
if (activePrompts.has(sessionId)) continue;
|
|
1937
|
+
|
|
1938
|
+
const tail = "━━━ 回答结束 ━━━";
|
|
1939
|
+
const finalMsg = state.status === "auto_ended"
|
|
1940
|
+
? formatAutoEndedReply(remaining)
|
|
1941
|
+
: remaining
|
|
1942
|
+
? remaining + "\n" + tail
|
|
1943
|
+
: tail;
|
|
1943
1944
|
if (!isFinalReplySentForTurn(state)) {
|
|
1944
1945
|
await sendFinalReplyTextOnce(p, chatId, sessionId, state.turnCount, finalMsg);
|
|
1945
1946
|
}
|
|
@@ -1963,7 +1964,7 @@ export function startUnifiedDisplayLoop(): void {
|
|
|
1963
1964
|
const nextSeq = display.sequence + 1;
|
|
1964
1965
|
const { title: headerTitle, template: headerTemplate } = formatTerminalHeader(state.status);
|
|
1965
1966
|
const cardContent = truncateContent(state.accumulatedContent + state.finalReply) || " ";
|
|
1966
|
-
const doneCard = buildProgressCard(cardContent,
|
|
1967
|
+
const doneCard = buildProgressCard(progressView({ text: cardContent, status: "done", showStop: false, headerTitle, headerTemplate }));
|
|
1967
1968
|
await p.cardUpdate(display.cardId, doneCard, nextSeq).then(() => {
|
|
1968
1969
|
display.sequence = nextSeq;
|
|
1969
1970
|
terminalCardUpdateAccepted = true;
|
|
@@ -1988,12 +1989,12 @@ export function startUnifiedDisplayLoop(): void {
|
|
|
1988
1989
|
continue;
|
|
1989
1990
|
}
|
|
1990
1991
|
|
|
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
|
-
}
|
|
1992
|
+
let terminalTextDelivered = true;
|
|
1993
|
+
const terminalReply = formatTerminalReply(state.status, state.finalReply);
|
|
1994
|
+
if (terminalReply) {
|
|
1995
|
+
if (!isFinalReplySentForTurn(state)) {
|
|
1996
|
+
terminalTextDelivered = await sendFinalReplyTextOnce(p, chatId, sessionId, state.turnCount, terminalReply);
|
|
1997
|
+
}
|
|
1997
1998
|
} else if (state.accumulatedContent.trim()) {
|
|
1998
1999
|
const short = truncateContent(state.accumulatedContent, 30, 4000);
|
|
1999
2000
|
terminalTextDelivered = await p.sendText(chatId, `[生成过程]\n${short}`).then((ok) => ok !== false).catch(() => false);
|
|
@@ -2008,7 +2009,7 @@ export function startUnifiedDisplayLoop(): void {
|
|
|
2008
2009
|
finalizeTurnCards(sessionId, state.turnCount, finalSt).catch(() => {});
|
|
2009
2010
|
displayCards.delete(chatId);
|
|
2010
2011
|
}
|
|
2011
|
-
setSessionChatAvatar(p, chatId, state.tool, "idle", sessionId).catch(() => {});
|
|
2012
|
+
setSessionChatAvatar(p, chatId, state.tool, "idle", sessionId).catch(() => {});
|
|
2012
2013
|
console.log(`[${ts()}] [DISPLAY] unified loop deleted display for ${chatId} (terminal: ${state.status})`);
|
|
2013
2014
|
} else {
|
|
2014
2015
|
// running: 创建或更新展示
|
|
@@ -2045,16 +2046,16 @@ export function startUnifiedDisplayLoop(): void {
|
|
|
2045
2046
|
}
|
|
2046
2047
|
} else {
|
|
2047
2048
|
// 非 WeChat: 卡片流程
|
|
2048
|
-
if (display.turnCount !== state.turnCount) {
|
|
2049
|
+
if (display.turnCount !== state.turnCount) {
|
|
2049
2050
|
console.log(`[${ts()}] [DISPLAY] turn mismatch for ${chatId}: display.turnCount=${display.turnCount} state.turnCount=${state.turnCount}, resetting`);
|
|
2050
2051
|
finalizeTurnCards(sessionId, display.turnCount, "done").catch(() => {});
|
|
2051
2052
|
displayCards.delete(chatId);
|
|
2052
|
-
continue;
|
|
2053
|
-
}
|
|
2054
|
-
|
|
2055
|
-
const activityHeaderTitle = formatAgentActivityTitle(state.activity, Date.now());
|
|
2056
|
-
|
|
2057
|
-
// 卡片轮转
|
|
2053
|
+
continue;
|
|
2054
|
+
}
|
|
2055
|
+
|
|
2056
|
+
const activityHeaderTitle = formatAgentActivityTitle(state.activity, Date.now());
|
|
2057
|
+
|
|
2058
|
+
// 卡片轮转
|
|
2058
2059
|
if (Date.now() - display.cardCreatedAt > CARD_ROTATE_MS) {
|
|
2059
2060
|
display.cardBusy = true;
|
|
2060
2061
|
try {
|
|
@@ -2062,9 +2063,9 @@ export function startUnifiedDisplayLoop(): void {
|
|
|
2062
2063
|
p,
|
|
2063
2064
|
chatId,
|
|
2064
2065
|
sessionId,
|
|
2065
|
-
display.turnCount,
|
|
2066
|
-
display.streamErrorNotified ? undefined : "生成中卡片发送失败,结果将继续更新在上一张卡片中。",
|
|
2067
|
-
activityHeaderTitle,
|
|
2066
|
+
display.turnCount,
|
|
2067
|
+
display.streamErrorNotified ? undefined : "生成中卡片发送失败,结果将继续更新在上一张卡片中。",
|
|
2068
|
+
activityHeaderTitle,
|
|
2068
2069
|
);
|
|
2069
2070
|
if (!newCardId) {
|
|
2070
2071
|
display.streamErrorNotified = true;
|
|
@@ -2072,7 +2073,7 @@ export function startUnifiedDisplayLoop(): void {
|
|
|
2072
2073
|
}
|
|
2073
2074
|
const oldSeqBase = display.sequence;
|
|
2074
2075
|
const oldContent = state.accumulatedContent + state.finalReply;
|
|
2075
|
-
const oldCard = buildProgressCard(truncateContent(oldContent) || " ",
|
|
2076
|
+
const oldCard = buildProgressCard(progressView({ text: truncateContent(oldContent) || " ", status: "done", showStop: false, headerTitle: "上一阶段记录" }));
|
|
2076
2077
|
await p.cardUpdate(display.cardId, oldCard, oldSeqBase + 1).then(() => {
|
|
2077
2078
|
display.sequence = oldSeqBase + 1;
|
|
2078
2079
|
}).catch(err => {
|
|
@@ -2083,10 +2084,10 @@ export function startUnifiedDisplayLoop(): void {
|
|
|
2083
2084
|
display.sequence = 1;
|
|
2084
2085
|
display.cardCreatedAt = Date.now();
|
|
2085
2086
|
display.rotationAccLen = state.accumulatedContent.length;
|
|
2086
|
-
display.rotationFinalReply = state.finalReply;
|
|
2087
|
-
display.lastSentContent = "";
|
|
2088
|
-
display.lastSentHeaderTitle = activityHeaderTitle;
|
|
2089
|
-
display.streamErrorNotified = false;
|
|
2087
|
+
display.rotationFinalReply = state.finalReply;
|
|
2088
|
+
display.lastSentContent = "";
|
|
2089
|
+
display.lastSentHeaderTitle = activityHeaderTitle;
|
|
2090
|
+
display.streamErrorNotified = false;
|
|
2090
2091
|
} catch (err) {
|
|
2091
2092
|
console.error(`[${ts()}] [CARDIKT] rotation FAIL for ${chatId}: ${(err as Error).message}`);
|
|
2092
2093
|
} finally {
|
|
@@ -2104,24 +2105,21 @@ export function startUnifiedDisplayLoop(): void {
|
|
|
2104
2105
|
replyDelta = state.finalReply.slice(rotReply.length);
|
|
2105
2106
|
} else {
|
|
2106
2107
|
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
|
-
});
|
|
2108
|
+
}
|
|
2109
|
+
const delta = (accDelta + replyDelta).trim();
|
|
2110
|
+
|
|
2111
|
+
display.dotCount = (display.dotCount % 9) + 1;
|
|
2112
|
+
let deltaBase = delta;
|
|
2113
|
+
if (isCodeBlockOpen(deltaBase)) deltaBase += "\n```";
|
|
2114
|
+
const displayContent = deltaBase + "\n" + "。".repeat(display.dotCount);
|
|
2115
|
+
if (
|
|
2116
|
+
displayContent === display.lastSentContent
|
|
2117
|
+
&& activityHeaderTitle === display.lastSentHeaderTitle
|
|
2118
|
+
) continue;
|
|
2119
|
+
|
|
2120
|
+
display.lastSentContent = displayContent;
|
|
2121
|
+
display.lastSentHeaderTitle = activityHeaderTitle;
|
|
2122
|
+
const deltaCard = buildProgressCard(progressView({ text: truncateContent(displayContent) || "等待 Agent 输出...", showStop: true, headerTitle: activityHeaderTitle }));
|
|
2125
2123
|
display.cardBusy = true;
|
|
2126
2124
|
const mySeq = display.sequence + 1;
|
|
2127
2125
|
try {
|
|
@@ -2140,24 +2138,24 @@ export function startUnifiedDisplayLoop(): void {
|
|
|
2140
2138
|
display.cardBusy = false;
|
|
2141
2139
|
}
|
|
2142
2140
|
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 输出...";
|
|
2141
|
+
}
|
|
2142
|
+
|
|
2143
|
+
display.dotCount = (display.dotCount % 9) + 1;
|
|
2144
|
+
let contentBase = state.accumulatedContent + state.finalReply;
|
|
2145
|
+
if (isCodeBlockOpen(contentBase)) contentBase += "\n```";
|
|
2146
|
+
const fullContent = contentBase + "\n" + "。".repeat(display.dotCount);
|
|
2147
|
+
if (
|
|
2148
|
+
fullContent === display.lastSentContent
|
|
2149
|
+
&& activityHeaderTitle === display.lastSentHeaderTitle
|
|
2150
|
+
) continue;
|
|
2151
|
+
|
|
2152
|
+
display.lastSentContent = fullContent;
|
|
2153
|
+
display.lastSentHeaderTitle = activityHeaderTitle;
|
|
2154
|
+
const cardContent = truncateContent(fullContent) || "等待 Agent 输出...";
|
|
2157
2155
|
display.cardBusy = true;
|
|
2158
2156
|
const mySeq = display.sequence + 1;
|
|
2159
2157
|
try {
|
|
2160
|
-
const card = buildProgressCard(cardContent,
|
|
2158
|
+
const card = buildProgressCard(progressView({ text: cardContent, showStop: true, headerTitle: activityHeaderTitle }));
|
|
2161
2159
|
await p.cardUpdate(display.cardId, card, mySeq);
|
|
2162
2160
|
display.sequence = mySeq;
|
|
2163
2161
|
} catch (err) {
|
|
@@ -2214,37 +2212,37 @@ export function stopUnifiedDisplayLoop(): void {
|
|
|
2214
2212
|
// 虽然很快但仍是异步,期间 display loop 可能多读到 1–2 帧 "running",
|
|
2215
2213
|
// 用户体验上"按下停止后还要等几秒卡片才变成已停止"。先把状态标好,
|
|
2216
2214
|
// 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) {
|
|
2215
|
+
export function stopSession(sessionId: string): boolean {
|
|
2216
|
+
// /stop 拥有高于内部自动恢复的优先级。旧轮已经完成、恢复轮尚在 200ms
|
|
2217
|
+
// 预约窗口时 activePrompts 为空,因此必须单独取消 reservation。
|
|
2218
|
+
const cancelledRecovery = cancelAutoRecoveryReservation(sessionId);
|
|
2219
|
+
const prompt = activePrompts.get(sessionId);
|
|
2220
|
+
if (!prompt) {
|
|
2221
|
+
if (cancelledRecovery) {
|
|
2222
|
+
cancelQueuedMessage(sessionId);
|
|
2223
|
+
console.log(`[${ts()}] [STOP] Reserved automatic recovery for ${sessionId} cancelled`);
|
|
2224
|
+
return true;
|
|
2225
|
+
}
|
|
2226
|
+
return false;
|
|
2227
|
+
}
|
|
2228
|
+
prompt.stopped = true;
|
|
2229
|
+
clearPromptResponseStallMonitor(sessionId);
|
|
2230
|
+
clearPromptProcessMonitor(sessionId);
|
|
2231
|
+
clearPromptFinalResponseCloseTimer(sessionId);
|
|
2232
|
+
cancelQueuedMessage(sessionId);
|
|
2233
|
+
|
|
2234
|
+
// 先发起整棵进程树清理,再触发 close/abort。Windows 上 CLI 由
|
|
2235
|
+
// cmd.exe → node → 实际二进制组成;若先 process.kill(cmd.exe),taskkill
|
|
2236
|
+
// 随后便无法从已消失的根 PID 找到后代,正是幽灵 Codex/Cursor 的来源。
|
|
2237
|
+
// killProcessTree 在返回 Promise 前已启动 taskkill,因此这里无需阻塞。
|
|
2238
|
+
void killProcessTree(prompt.processPid);
|
|
2239
|
+
try {
|
|
2240
|
+
prompt.closeSession?.();
|
|
2241
|
+
} catch (err) {
|
|
2244
2242
|
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`);
|
|
2243
|
+
}
|
|
2244
|
+
prompt.controller.abort();
|
|
2245
|
+
console.log(`[${ts()}] [STOP] Session ${sessionId} aborted`);
|
|
2248
2246
|
|
|
2249
2247
|
// fire-and-forget:立刻把 stream-state.status 改成 stopped,
|
|
2250
2248
|
// 让 display loop 下一次扫到立刻渲染"已停止"卡片,不必再等几秒。
|
|
@@ -2314,27 +2312,27 @@ async function resolveModelEffort(
|
|
|
2314
2312
|
// adapter 异常时降级为占位符(不阻塞 /state 卡片)
|
|
2315
2313
|
}
|
|
2316
2314
|
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
|
-
}
|
|
2315
|
+
}
|
|
2316
|
+
if (tool === "codex") {
|
|
2317
|
+
const m = getEffectiveModelForTool(tool, sessionId);
|
|
2318
|
+
const e = getEffectiveEffortForTool(tool, sessionId);
|
|
2319
|
+
return {
|
|
2320
|
+
model: m.trim() !== "" ? m : UNKNOWN_MODEL_PLACEHOLDER,
|
|
2321
|
+
effort: e.trim() !== "" ? e : UNKNOWN_MODEL_PLACEHOLDER,
|
|
2322
|
+
};
|
|
2323
|
+
}
|
|
2324
|
+
if (tool === "ccc") {
|
|
2325
|
+
const m = getEffectiveModelForTool(tool, sessionId);
|
|
2326
|
+
return {
|
|
2327
|
+
model: m.trim() !== "" ? m : UNKNOWN_MODEL_PLACEHOLDER,
|
|
2328
|
+
effort: null,
|
|
2329
|
+
};
|
|
2330
|
+
}
|
|
2331
|
+
return {
|
|
2332
|
+
model: anthropicConfigDisplay(getModelForSession(sessionId)),
|
|
2333
|
+
effort: anthropicConfigDisplay(getEffectiveEffortForTool(tool, sessionId)),
|
|
2334
|
+
};
|
|
2335
|
+
}
|
|
2338
2336
|
|
|
2339
2337
|
export async function getSessionStatus(chatId: string): Promise<SessionStatus | null> {
|
|
2340
2338
|
const info = sessionInfoMap.get(chatId);
|
|
@@ -2367,10 +2365,10 @@ export async function getSessionStatus(chatId: string): Promise<SessionStatus |
|
|
|
2367
2365
|
};
|
|
2368
2366
|
}
|
|
2369
2367
|
|
|
2370
|
-
export interface SessionsListEntry {
|
|
2371
|
-
chatId: string;
|
|
2372
|
-
chatType?: string;
|
|
2373
|
-
sessionId: string;
|
|
2368
|
+
export interface SessionsListEntry {
|
|
2369
|
+
chatId: string;
|
|
2370
|
+
chatType?: string;
|
|
2371
|
+
sessionId: string;
|
|
2374
2372
|
chatName: string;
|
|
2375
2373
|
active: boolean;
|
|
2376
2374
|
turnCount: number;
|
|
@@ -2393,10 +2391,10 @@ export async function getAllSessionsStatus(): Promise<SessionsListEntry[]> {
|
|
|
2393
2391
|
.map(([sessionId, record]) => {
|
|
2394
2392
|
const createdAt = Number.isFinite(record.createdAt) ? record.createdAt : 0;
|
|
2395
2393
|
const active = activePrompts.get(sessionId);
|
|
2396
|
-
return {
|
|
2397
|
-
chatId: "",
|
|
2398
|
-
chatType: undefined,
|
|
2399
|
-
sessionId,
|
|
2394
|
+
return {
|
|
2395
|
+
chatId: "",
|
|
2396
|
+
chatType: undefined,
|
|
2397
|
+
sessionId,
|
|
2400
2398
|
tool: record.tool,
|
|
2401
2399
|
chatName: record.chatName ?? "",
|
|
2402
2400
|
turnCount: 0,
|
|
@@ -2414,10 +2412,10 @@ export async function getAllSessionsStatus(): Promise<SessionsListEntry[]> {
|
|
|
2414
2412
|
return Promise.all(
|
|
2415
2413
|
entries.map(async (info) => {
|
|
2416
2414
|
const { model, effort } = await resolveModelEffort(info.tool, info.sessionId);
|
|
2417
|
-
return {
|
|
2418
|
-
chatId: info.chatId,
|
|
2419
|
-
chatType: info.chatType,
|
|
2420
|
-
sessionId: info.sessionId,
|
|
2415
|
+
return {
|
|
2416
|
+
chatId: info.chatId,
|
|
2417
|
+
chatType: info.chatType,
|
|
2418
|
+
sessionId: info.sessionId,
|
|
2421
2419
|
chatName: info.chatName || "",
|
|
2422
2420
|
active: !!activePrompts.get(info.sessionId) &&
|
|
2423
2421
|
!activePrompts.get(info.sessionId)?.stopped &&
|
|
@@ -2442,11 +2440,11 @@ export async function getAllSessionsStatus(): Promise<SessionsListEntry[]> {
|
|
|
2442
2440
|
export function _setAdapterForToolForTest(tool: string, adapter: ToolAdapter): void {
|
|
2443
2441
|
adapterCache.set(tool, adapter);
|
|
2444
2442
|
// 同时设置当前配置模型对应的 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);
|
|
2443
|
+
const effective = getEffectiveModelForTool(tool);
|
|
2444
|
+
const effort = getEffectiveEffortForTool(tool);
|
|
2445
|
+
const fastMode = getEffectiveFastModeForTool(tool);
|
|
2446
|
+
adapterCache.set(`${tool}:${effective || ""}:${effort || ""}:${fastMode ? "fast" : "default"}`, adapter);
|
|
2447
|
+
if (effective) adapterCache.set(`${tool}:${effective}`, adapter);
|
|
2450
2448
|
}
|
|
2451
2449
|
|
|
2452
2450
|
export function clearAdapterCache(): void {
|