chatccc 0.2.114 → 0.2.116
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/__tests__/card-plain-text.test.ts +1 -1
- package/src/__tests__/cards.test.ts +1 -1
- package/src/__tests__/claude-adapter.test.ts +1 -1
- package/src/__tests__/cursor-adapter.test.ts +2 -2
- package/src/__tests__/cursor-session-meta-store.test.ts +1 -1
- package/src/__tests__/session.test.ts +3 -3
- package/src/adapters/adapter-interface.ts +2 -2
- package/src/adapters/claude-adapter.ts +6 -0
- package/src/adapters/cursor-adapter.ts +6 -0
- package/src/adapters/cursor-session-meta-store.ts +1 -1
- package/src/adapters/resource-monitor.ts +141 -0
- package/src/cards.ts +1 -1
- package/src/index.ts +1 -1
- package/src/orchestrator.ts +2 -2
- package/src/session-chat-binding.ts +2 -0
- package/src/session.ts +30 -5
package/README.md
CHANGED
|
@@ -305,7 +305,7 @@ Codex 的默认模型和推理强度可继续由 `~/.codex/config.toml` 管理
|
|
|
305
305
|
| `/newh` | 重置当前会话,保留工作目录 |
|
|
306
306
|
| `/model` | 查看或切换当前会话的模型 |
|
|
307
307
|
| `/stop` | 停止当前回复 |
|
|
308
|
-
| `/
|
|
308
|
+
| `/state` | 查看当前会话状态 |
|
|
309
309
|
| `/cd` | 查看或设置当前会话工作目录 |
|
|
310
310
|
| `/sessions` | 查看所有会话状态 |
|
|
311
311
|
| `/git <子命令>` | 在当前会话工作目录执行 `git ...` 并回传输出 |
|
package/package.json
CHANGED
|
@@ -107,7 +107,7 @@ describe("buildProgressCard", () => {
|
|
|
107
107
|
const parsed = JSON.parse(card);
|
|
108
108
|
const buttons = parsed.body.elements.filter((e: any) => e.tag === "button");
|
|
109
109
|
expect(buttons).toHaveLength(2);
|
|
110
|
-
expect(buttons[0].text.content).toBe("查看状态(/
|
|
110
|
+
expect(buttons[0].text.content).toBe("查看状态(/state)");
|
|
111
111
|
expect(buttons[1].text.content).toBe("停止生成(/stop)");
|
|
112
112
|
});
|
|
113
113
|
|
|
@@ -369,7 +369,7 @@ describe("createClaudeAdapter", () => {
|
|
|
369
369
|
// -------------------------------------------------------------------------
|
|
370
370
|
// getSessionInfo 行为契约
|
|
371
371
|
// - cwd 决定 /git 是否可用
|
|
372
|
-
// - model 用于 /
|
|
372
|
+
// - model 用于 /state、/sessions 显示
|
|
373
373
|
// -------------------------------------------------------------------------
|
|
374
374
|
|
|
375
375
|
it("getSessionInfo: store 中无该 sessionId 时只返回 sessionId", async () => {
|
|
@@ -296,7 +296,7 @@ describe("createCursorAdapter", () => {
|
|
|
296
296
|
// -------------------------------------------------------------------------
|
|
297
297
|
// getSessionInfo 行为契约
|
|
298
298
|
// - cwd 决定 /git 是否可用
|
|
299
|
-
// - model 决定 /
|
|
299
|
+
// - model 决定 /state、/sessions 显示的是否是 Cursor 真实模型
|
|
300
300
|
// -------------------------------------------------------------------------
|
|
301
301
|
|
|
302
302
|
it("getSessionInfo: store 中无该 sessionId 时只返回 sessionId(cwd / model 都 undefined,让上层走错误分支)", async () => {
|
|
@@ -316,7 +316,7 @@ describe("createCursorAdapter", () => {
|
|
|
316
316
|
expect(info?.model).toBeUndefined();
|
|
317
317
|
});
|
|
318
318
|
|
|
319
|
-
it("getSessionInfo: store 同时有 cwd + model 时一并返回(这是 /
|
|
319
|
+
it("getSessionInfo: store 同时有 cwd + model 时一并返回(这是 /state 显示真实模型的关键)", async () => {
|
|
320
320
|
const store = createInMemoryMetaStore({
|
|
321
321
|
"sid-known": { cwd: "F:/proj/Foo", model: "Composer 2 Fast" },
|
|
322
322
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// =============================================================================
|
|
2
2
|
// cursor-session-meta-store.test.ts — sessionId→{cwd,model} 持久化的护栏单测
|
|
3
3
|
// =============================================================================
|
|
4
|
-
// 行为契约(与 cursor-adapter.getSessionInfo 配合,决定 /git、/
|
|
4
|
+
// 行为契约(与 cursor-adapter.getSessionInfo 配合,决定 /git、/state、/sessions
|
|
5
5
|
// 在 Cursor 会话上是否显示正确):
|
|
6
6
|
// - 文件不存在 / 损坏 / 非法 schema 时 get 返回 undefined,不抛异常
|
|
7
7
|
// - set 部分合并:只覆盖非空字段,已有字段保持不变
|
|
@@ -160,7 +160,7 @@ function mockSessionInfo(chatId: string, overrides: Partial<{
|
|
|
160
160
|
|
|
161
161
|
/**
|
|
162
162
|
* 简易 mock adapter:getSessionInfo 返回固定 SessionInfo,其他方法不实现
|
|
163
|
-
* (仅 /
|
|
163
|
+
* (仅 /state、/sessions 路径会触发 getSessionInfo,无需完整接口)。
|
|
164
164
|
*/
|
|
165
165
|
function mockAdapter(getInfo: (sid: string) => SessionInfo | undefined): ToolAdapter {
|
|
166
166
|
return {
|
|
@@ -597,7 +597,7 @@ describe("getSessionStatus", () => {
|
|
|
597
597
|
});
|
|
598
598
|
|
|
599
599
|
// -------------------------------------------------------------------------
|
|
600
|
-
// model/effort 来源:按 tool 分支(核心契约——决定 /
|
|
600
|
+
// model/effort 来源:按 tool 分支(核心契约——决定 /state 显示是否真实)
|
|
601
601
|
// -------------------------------------------------------------------------
|
|
602
602
|
|
|
603
603
|
it("Claude 会话:effort 非 null(始终显示该行);model 来自全局配置", async () => {
|
|
@@ -643,7 +643,7 @@ describe("getSessionStatus", () => {
|
|
|
643
643
|
expect(status!.model).toBe(UNKNOWN_MODEL_PLACEHOLDER);
|
|
644
644
|
});
|
|
645
645
|
|
|
646
|
-
it("Cursor 会话:adapter.getSessionInfo 抛错时降级为占位符(不阻塞 /
|
|
646
|
+
it("Cursor 会话:adapter.getSessionInfo 抛错时降级为占位符(不阻塞 /state)", async () => {
|
|
647
647
|
mockSessionInfo("chat-cursor", { sessionId: "sid-cur", tool: "cursor" });
|
|
648
648
|
_setAdapterForToolForTest(
|
|
649
649
|
"cursor",
|
|
@@ -101,7 +101,7 @@ export interface CreateSessionResult {
|
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
// ---------------------------------------------------------------------------
|
|
104
|
-
// SessionInfo — 会话元数据(/
|
|
104
|
+
// SessionInfo — 会话元数据(/state、/cd 等命令使用)
|
|
105
105
|
// ---------------------------------------------------------------------------
|
|
106
106
|
|
|
107
107
|
export interface SessionInfo {
|
|
@@ -113,7 +113,7 @@ export interface SessionInfo {
|
|
|
113
113
|
* 会话实际使用的模型展示名(如 Cursor 的 `Composer 2 Fast`)。
|
|
114
114
|
* - Cursor adapter:从 cursor-agent system/init 事件学习并持久化到 store
|
|
115
115
|
* - Claude adapter:留空(model 由 ChatCCC 配置 `CLAUDE_MODEL` 决定,不从 SDK 取)
|
|
116
|
-
* 上层 /
|
|
116
|
+
* 上层 /state、/sessions 渲染时按 tool 决定显示哪一来源。
|
|
117
117
|
*/
|
|
118
118
|
model?: string;
|
|
119
119
|
}
|
|
@@ -458,6 +458,12 @@ class ClaudeAdapter implements ToolAdapter {
|
|
|
458
458
|
|
|
459
459
|
const normalized = normalizeSdkMessage(raw);
|
|
460
460
|
if (normalized) yield normalized;
|
|
461
|
+
|
|
462
|
+
// result 是流末事件,收到后立即结束进程,防止 CLI 僵死导致 readline 挂起。
|
|
463
|
+
if (raw.type === "result") {
|
|
464
|
+
void killProcessTree(proc.pid);
|
|
465
|
+
break;
|
|
466
|
+
}
|
|
461
467
|
}
|
|
462
468
|
} finally {
|
|
463
469
|
signal?.removeEventListener("abort", onAbort);
|
|
@@ -394,6 +394,12 @@ class CursorAdapter implements ToolAdapter {
|
|
|
394
394
|
}
|
|
395
395
|
const normalized = normalizeCursorMessage(raw);
|
|
396
396
|
if (normalized) yield normalized;
|
|
397
|
+
|
|
398
|
+
// result 是流末事件,收到后立即结束进程,防止 CLI 僵死导致 readline 挂起。
|
|
399
|
+
if (raw.type === "result") {
|
|
400
|
+
void killProcessTree(proc.pid);
|
|
401
|
+
break;
|
|
402
|
+
}
|
|
397
403
|
}
|
|
398
404
|
} finally {
|
|
399
405
|
signal?.removeEventListener("abort", onAbort);
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// 内部已持久化)。Cursor CLI 没有等价机制,因此 ChatCCC 必须自己维护一份
|
|
6
6
|
// sessionId → { cwd, model } 映射,否则:
|
|
7
7
|
// 1. /git、/cd 等需要"会话真实工作目录"的命令将在 Cursor 会话上 100% 失败
|
|
8
|
-
// 2. /
|
|
8
|
+
// 2. /state、/sessions 显示的"模型"只能硬塞 ChatCCC 的 ANTHROPIC 环境变量,
|
|
9
9
|
// 与 Cursor 实际跑的 Composer 2 Fast 等真实模型无关
|
|
10
10
|
//
|
|
11
11
|
// 存储:
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// resource-monitor.ts — CLI 进程资源监控(CPU + 内存)
|
|
3
|
+
// =============================================================================
|
|
4
|
+
// 对所有 chatccc 启动的 CLI 进程持续监控 CPU 和内存占用。
|
|
5
|
+
// 若连续 3 分钟两项指标均无变化,判定为僵死,发出 "stuck" 事件。
|
|
6
|
+
// =============================================================================
|
|
7
|
+
|
|
8
|
+
import { exec } from "node:child_process";
|
|
9
|
+
import { EventEmitter } from "node:events";
|
|
10
|
+
|
|
11
|
+
const CHECK_INTERVAL_MS = 30_000; // 30 秒检查一次
|
|
12
|
+
const STUCK_THRESHOLD = 6; // 连续 6 次无变化 = 3 分钟
|
|
13
|
+
|
|
14
|
+
interface ProcessSnapshot {
|
|
15
|
+
cpu: number;
|
|
16
|
+
memory: number;
|
|
17
|
+
unchangedCount: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface TrackedProcess {
|
|
21
|
+
pid: number;
|
|
22
|
+
sessionId: string;
|
|
23
|
+
snapshot: ProcessSnapshot;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const resourceMonitor = new EventEmitter();
|
|
27
|
+
|
|
28
|
+
const tracked = new Map<number, TrackedProcess>();
|
|
29
|
+
|
|
30
|
+
let timer: ReturnType<typeof setInterval> | null = null;
|
|
31
|
+
|
|
32
|
+
function startIfNeeded(): void {
|
|
33
|
+
if (timer) return;
|
|
34
|
+
timer = setInterval(checkAll, CHECK_INTERVAL_MS);
|
|
35
|
+
timer.unref?.();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function stopIfIdle(): void {
|
|
39
|
+
if (tracked.size > 0) return;
|
|
40
|
+
if (timer) { clearInterval(timer); timer = null; }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function registerProcess(pid: number, sessionId: string): void {
|
|
44
|
+
tracked.set(pid, { pid, sessionId, snapshot: { cpu: -1, memory: -1, unchangedCount: 0 } });
|
|
45
|
+
startIfNeeded();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function unregisterProcess(pid: number): void {
|
|
49
|
+
tracked.delete(pid);
|
|
50
|
+
stopIfIdle();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
// 批量查询进程指标(Windows PowerShell)
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
|
|
57
|
+
function execPowerShell(script: string, timeoutMs: number): Promise<string> {
|
|
58
|
+
return new Promise((resolve, reject) => {
|
|
59
|
+
exec(
|
|
60
|
+
`powershell -NoProfile -Command "${script}"`,
|
|
61
|
+
{ timeout: timeoutMs, windowsHide: true },
|
|
62
|
+
(err, stdout) => {
|
|
63
|
+
if (err) reject(err);
|
|
64
|
+
else resolve(stdout);
|
|
65
|
+
},
|
|
66
|
+
);
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function getProcessMetrics(pids: number[]): Promise<Map<number, { cpu: number; memory: number }>> {
|
|
71
|
+
const result = new Map<number, { cpu: number; memory: number }>();
|
|
72
|
+
if (pids.length === 0) return result;
|
|
73
|
+
|
|
74
|
+
const psScript = `Get-Process -Id ${pids.join(",")} -ErrorAction SilentlyContinue | ForEach-Object { "$($_.Id)|$($_.CPU)|$($_.WorkingSet64)" }`;
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
const stdout = await execPowerShell(psScript, 10_000);
|
|
78
|
+
for (const line of stdout.trim().split(/\r?\n/)) {
|
|
79
|
+
const trimmed = line.trim();
|
|
80
|
+
if (!trimmed) continue;
|
|
81
|
+
const [idStr, cpuStr, memStr] = trimmed.split("|");
|
|
82
|
+
const id = parseInt(idStr, 10);
|
|
83
|
+
const cpu = parseFloat(cpuStr);
|
|
84
|
+
const memory = parseInt(memStr, 10);
|
|
85
|
+
if (!isNaN(id) && !isNaN(cpu) && !isNaN(memory)) {
|
|
86
|
+
result.set(id, { cpu, memory });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
} catch {
|
|
90
|
+
// PowerShell 查询失败时跳过本轮,下次重试
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return result;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
// 定时检查
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
|
|
100
|
+
async function checkAll(): Promise<void> {
|
|
101
|
+
if (tracked.size === 0) return;
|
|
102
|
+
|
|
103
|
+
const pids = [...tracked.keys()];
|
|
104
|
+
const metrics = await getProcessMetrics(pids);
|
|
105
|
+
|
|
106
|
+
for (const [pid, tp] of tracked) {
|
|
107
|
+
const m = metrics.get(pid);
|
|
108
|
+
if (!m) {
|
|
109
|
+
// 进程已不存在,停止追踪
|
|
110
|
+
tracked.delete(pid);
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const prev = tp.snapshot;
|
|
115
|
+
const cpuChanged = m.cpu !== prev.cpu;
|
|
116
|
+
// 内存允许 ±1% 波动,避免正常抖动触发误判
|
|
117
|
+
const memTolerance = prev.memory > 0 ? Math.max(prev.memory * 0.01, 1024 * 1024) : 1024 * 1024;
|
|
118
|
+
const memChanged = Math.abs(m.memory - prev.memory) > memTolerance;
|
|
119
|
+
|
|
120
|
+
if (!cpuChanged && !memChanged) {
|
|
121
|
+
tp.snapshot.unchangedCount++;
|
|
122
|
+
if (tp.snapshot.unchangedCount >= STUCK_THRESHOLD) {
|
|
123
|
+
const idleMinutes = Math.round(
|
|
124
|
+
(tp.snapshot.unchangedCount * CHECK_INTERVAL_MS) / 60_000,
|
|
125
|
+
);
|
|
126
|
+
resourceMonitor.emit("stuck", {
|
|
127
|
+
pid: tp.pid,
|
|
128
|
+
sessionId: tp.sessionId,
|
|
129
|
+
idleMinutes,
|
|
130
|
+
});
|
|
131
|
+
tracked.delete(pid);
|
|
132
|
+
}
|
|
133
|
+
} else {
|
|
134
|
+
tp.snapshot.unchangedCount = 0;
|
|
135
|
+
}
|
|
136
|
+
tp.snapshot.cpu = m.cpu;
|
|
137
|
+
tp.snapshot.memory = m.memory;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
stopIfIdle();
|
|
141
|
+
}
|
package/src/cards.ts
CHANGED
|
@@ -89,7 +89,7 @@ export function buildProgressCard(
|
|
|
89
89
|
elements.push({ tag: "hr" });
|
|
90
90
|
elements.push({
|
|
91
91
|
tag: "button",
|
|
92
|
-
text: { tag: "plain_text", content: "查看状态(/
|
|
92
|
+
text: { tag: "plain_text", content: "查看状态(/state)" },
|
|
93
93
|
type: "default",
|
|
94
94
|
value: { action: "status" },
|
|
95
95
|
element_id: "action_status",
|
package/src/index.ts
CHANGED
|
@@ -287,7 +287,7 @@ function parseCardAction(data: unknown): CardActionResult | null {
|
|
|
287
287
|
}
|
|
288
288
|
if (!cmd) return null;
|
|
289
289
|
|
|
290
|
-
const CMD_MAP: Record<string, string> = { stop: "/stop", cancel: "/cancel", new: "/new", "new claude": "/new claude", "new cursor": "/new cursor", "new codex": "/new codex", restart: "/restart", status: "/
|
|
290
|
+
const CMD_MAP: Record<string, string> = { stop: "/stop", cancel: "/cancel", new: "/new", "new claude": "/new claude", "new cursor": "/new cursor", "new codex": "/new codex", restart: "/restart", status: "/state", cd: "/cd", sessions: "/sessions", newh: "/newh" };
|
|
291
291
|
let text = CMD_MAP[cmd] ?? "";
|
|
292
292
|
if (cmd === "cd" && typeof action.value === "object" && action.value !== null) {
|
|
293
293
|
const path = (action.value as Record<string, string>).path;
|
package/src/orchestrator.ts
CHANGED
|
@@ -739,8 +739,8 @@ export async function handleCommand(
|
|
|
739
739
|
return;
|
|
740
740
|
}
|
|
741
741
|
|
|
742
|
-
if (textLower === "/
|
|
743
|
-
logTrace(tid, "BRANCH", { cmd: "/
|
|
742
|
+
if (textLower === "/state") {
|
|
743
|
+
logTrace(tid, "BRANCH", { cmd: "/state" });
|
|
744
744
|
const status = await getSessionStatus(chatId);
|
|
745
745
|
const isActive = isSessionRunning(sessionId);
|
|
746
746
|
const statusText = [
|
|
@@ -71,6 +71,8 @@ export interface ActivePrompt {
|
|
|
71
71
|
/** Set when the watchdog detects that the CLI process disappeared before stream finalization. */
|
|
72
72
|
abnormalExit?: boolean;
|
|
73
73
|
abnormalExitNotified?: boolean;
|
|
74
|
+
/** Set when the resource monitor detects CPU + memory unchanged for 3 minutes. */
|
|
75
|
+
resourceStuck?: boolean;
|
|
74
76
|
}
|
|
75
77
|
|
|
76
78
|
export const activePrompts = new Map<string, ActivePrompt>();
|
package/src/session.ts
CHANGED
|
@@ -29,6 +29,7 @@ import type { ToolProcessInfo } from "./adapters/adapter-interface.ts";
|
|
|
29
29
|
import { createClaudeAdapter } from "./adapters/claude-adapter.ts";
|
|
30
30
|
import { createCursorAdapter } from "./adapters/cursor-adapter.ts";
|
|
31
31
|
import { createCodexAdapter } from "./adapters/codex-adapter.ts";
|
|
32
|
+
import { resourceMonitor, registerProcess, unregisterProcess } from "./adapters/resource-monitor.ts";
|
|
32
33
|
import { buildImSkillsPromptCached, exportSkillSubDocs, clearImSkillsPromptCache } from "./im-skills.ts";
|
|
33
34
|
import type { PlatformAdapter } from "./platform-adapter.ts";
|
|
34
35
|
|
|
@@ -181,7 +182,7 @@ function startPromptProcessMonitor(sessionId: string, info: ToolProcessInfo): vo
|
|
|
181
182
|
clearPromptProcessMonitor(sessionId);
|
|
182
183
|
return;
|
|
183
184
|
}
|
|
184
|
-
if (current.stopped || current.abnormalExit) return;
|
|
185
|
+
if (current.stopped || current.abnormalExit || current.resourceStuck) return;
|
|
185
186
|
if (isProcessAliveImpl(info.pid)) return;
|
|
186
187
|
|
|
187
188
|
current.abnormalExit = true;
|
|
@@ -877,6 +878,26 @@ export async function runAgentSession(
|
|
|
877
878
|
startTime: now,
|
|
878
879
|
});
|
|
879
880
|
|
|
881
|
+
// 资源监控僵死检测:CPU + 内存连续 3 分钟无变化 → 强制停止
|
|
882
|
+
const onResourceStuck = (data: { pid: number; sessionId: string; idleMinutes: number }) => {
|
|
883
|
+
if (data.sessionId !== sessionId) return;
|
|
884
|
+
const prompt = activePrompts.get(sessionId);
|
|
885
|
+
if (!prompt || prompt.stopped || prompt.abnormalExit || prompt.resourceStuck) return;
|
|
886
|
+
prompt.resourceStuck = true;
|
|
887
|
+
|
|
888
|
+
const chatId = pickDisplayChat(sessionId) ?? getLastActiveChat(sessionId) ?? getChatsForSession(sessionId)[0];
|
|
889
|
+
const p = chatId ? platformForChat(chatId) : null;
|
|
890
|
+
if (chatId && p) {
|
|
891
|
+
p.sendText(
|
|
892
|
+
chatId,
|
|
893
|
+
`⚠️ 会话僵死:session ${sessionId.slice(0, 8)} 对应的 CLI 进程 PID ${data.pid} CPU 和内存连续 ${data.idleMinutes} 分钟无变化,已强制停止。若回复不完整,请重新发送上一条指令。`,
|
|
894
|
+
).catch(() => {});
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
controller.abort();
|
|
898
|
+
};
|
|
899
|
+
resourceMonitor.on("stuck", onResourceStuck);
|
|
900
|
+
|
|
880
901
|
// 异步准备工作(session info、IM skills prompt 等)
|
|
881
902
|
let adapter: ToolAdapter;
|
|
882
903
|
let info: Awaited<ReturnType<ToolAdapter["getSessionInfo"]>>;
|
|
@@ -1079,9 +1100,11 @@ export async function runAgentSession(
|
|
|
1079
1100
|
for await (const unifiedMsg of adapter.prompt(sessionId, userTextWithCapabilities, cwd, controller.signal, {
|
|
1080
1101
|
onProcessStart: (processInfo) => {
|
|
1081
1102
|
startPromptProcessMonitor(sessionId, processInfo);
|
|
1103
|
+
if (processInfo.pid !== undefined) registerProcess(processInfo.pid, sessionId);
|
|
1082
1104
|
},
|
|
1083
|
-
onProcessExit: () => {
|
|
1105
|
+
onProcessExit: (exitInfo) => {
|
|
1084
1106
|
clearPromptProcessMonitor(sessionId);
|
|
1107
|
+
if (exitInfo.pid !== undefined) unregisterProcess(exitInfo.pid);
|
|
1085
1108
|
},
|
|
1086
1109
|
})) {
|
|
1087
1110
|
for (const block of unifiedMsg.blocks) {
|
|
@@ -1124,9 +1147,11 @@ export async function runAgentSession(
|
|
|
1124
1147
|
console.error(`[${ts()}] [STREAM] Error in stream loop for ${sessionId}: ${(streamErr as Error).message}`);
|
|
1125
1148
|
} finally {
|
|
1126
1149
|
// 标记 prompt 结束
|
|
1150
|
+
resourceMonitor.off("stuck", onResourceStuck);
|
|
1127
1151
|
const prompt = activePrompts.get(sessionId);
|
|
1128
1152
|
const wasStopped = prompt?.stopped ?? false;
|
|
1129
1153
|
const wasAbnormalExit = prompt?.abnormalExit ?? false;
|
|
1154
|
+
const wasResourceStuck = prompt?.resourceStuck ?? false;
|
|
1130
1155
|
clearPromptProcessMonitor(sessionId);
|
|
1131
1156
|
activePrompts.delete(sessionId);
|
|
1132
1157
|
|
|
@@ -1134,7 +1159,7 @@ export async function runAgentSession(
|
|
|
1134
1159
|
// 读到新状态并终结旧卡片。否则 setImmediate 在 CHECK 阶段先于
|
|
1135
1160
|
// writeFile I/O(POLL 阶段)执行,display loop 会误以为旧轮仍在
|
|
1136
1161
|
// 运行中并更新旧卡片,而不是新建卡片。
|
|
1137
|
-
const finalStatus = wasAbnormalExit ? "error" : wasStopped ? "stopped" : "done";
|
|
1162
|
+
const finalStatus = (wasAbnormalExit || wasResourceStuck) ? "error" : wasStopped ? "stopped" : "done";
|
|
1138
1163
|
const finalReply = pickFinalReply(state).trim();
|
|
1139
1164
|
await writeStreamState({
|
|
1140
1165
|
sessionId,
|
|
@@ -1549,7 +1574,7 @@ export function stopSession(sessionId: string): boolean {
|
|
|
1549
1574
|
}
|
|
1550
1575
|
|
|
1551
1576
|
// ---------------------------------------------------------------------------
|
|
1552
|
-
// Session status query (供 /
|
|
1577
|
+
// Session status query (供 /state、/sessions 命令使用)
|
|
1553
1578
|
// ---------------------------------------------------------------------------
|
|
1554
1579
|
//
|
|
1555
1580
|
// model / effort 的来源策略(按 tool 区分,避免硬塞 ChatCCC 全局配置导致显示
|
|
@@ -1590,7 +1615,7 @@ async function resolveModelEffort(
|
|
|
1590
1615
|
const info = await adapter.getSessionInfo(sessionId);
|
|
1591
1616
|
if (info?.model) model = info.model;
|
|
1592
1617
|
} catch {
|
|
1593
|
-
// adapter 异常时降级为占位符(不阻塞 /
|
|
1618
|
+
// adapter 异常时降级为占位符(不阻塞 /state 卡片)
|
|
1594
1619
|
}
|
|
1595
1620
|
return { model, effort: null };
|
|
1596
1621
|
}
|