chatccc 0.2.94 → 0.2.96
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/im-skills/wechat-file-skill/receive-send-file.md +38 -38
- package/im-skills/wechat-file-skill/send-file.mjs +83 -83
- package/im-skills/wechat-file-skill/skill.md +10 -10
- package/im-skills/wechat-image-skill/skill.md +10 -10
- package/im-skills/wechat-video-skill/receive-send-video.md +38 -38
- package/im-skills/wechat-video-skill/send-video.mjs +79 -79
- package/im-skills/wechat-video-skill/skill.md +10 -10
- package/package.json +1 -1
- package/scripts/postinstall-sharp-check.mjs +58 -58
- package/src/__tests__/session.test.ts +91 -1
- package/src/__tests__/stream-state.test.ts +42 -42
- package/src/adapters/adapter-interface.ts +13 -0
- package/src/adapters/codex-adapter.ts +4 -0
- package/src/adapters/cursor-adapter.ts +4 -0
- package/src/builtin/cli.ts +196 -196
- package/src/builtin/index.ts +166 -166
- package/src/cards.ts +60 -0
- package/src/config.ts +5 -0
- package/src/index.ts +1 -1
- package/src/orchestrator.ts +116 -6
- package/src/session-chat-binding.ts +9 -0
- package/src/session.ts +185 -23
- package/src/stream-state.ts +33 -33
- package/src/turn-cards.ts +117 -117
package/src/session.ts
CHANGED
|
@@ -25,6 +25,7 @@ import { simplifyToolUse, simplifyToolResult } from "./simplify.ts";
|
|
|
25
25
|
import { logTrace } from "./trace.ts";
|
|
26
26
|
import type { UnifiedBlock } from "./adapters/adapter-interface.ts";
|
|
27
27
|
import type { ToolAdapter } from "./adapters/adapter-interface.ts";
|
|
28
|
+
import type { ToolProcessInfo } from "./adapters/adapter-interface.ts";
|
|
28
29
|
import { createClaudeAdapter } from "./adapters/claude-adapter.ts";
|
|
29
30
|
import { createCursorAdapter } from "./adapters/cursor-adapter.ts";
|
|
30
31
|
import { createCodexAdapter } from "./adapters/codex-adapter.ts";
|
|
@@ -114,6 +115,110 @@ function platformForChat(chatId: string): PlatformAdapter | null {
|
|
|
114
115
|
return chatPlatformMap.get(chatId) ?? platformRef;
|
|
115
116
|
}
|
|
116
117
|
|
|
118
|
+
const DEFAULT_PROCESS_MONITOR_INTERVAL_MS = 5000;
|
|
119
|
+
let processMonitorIntervalMs = DEFAULT_PROCESS_MONITOR_INTERVAL_MS;
|
|
120
|
+
let isProcessAliveImpl = (pid: number): boolean => {
|
|
121
|
+
try {
|
|
122
|
+
process.kill(pid, 0);
|
|
123
|
+
return true;
|
|
124
|
+
} catch {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
export function _setProcessAliveForTest(impl: (pid: number) => boolean): void {
|
|
130
|
+
isProcessAliveImpl = impl;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function _resetProcessAliveForTest(): void {
|
|
134
|
+
isProcessAliveImpl = (pid: number): boolean => {
|
|
135
|
+
try {
|
|
136
|
+
process.kill(pid, 0);
|
|
137
|
+
return true;
|
|
138
|
+
} catch {
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function _setProcessMonitorIntervalForTest(ms: number): void {
|
|
145
|
+
processMonitorIntervalMs = ms;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function _resetProcessMonitorIntervalForTest(): void {
|
|
149
|
+
processMonitorIntervalMs = DEFAULT_PROCESS_MONITOR_INTERVAL_MS;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function clearPromptProcessMonitor(sessionId: string): void {
|
|
153
|
+
const prompt = activePrompts.get(sessionId);
|
|
154
|
+
if (!prompt?.processMonitor) return;
|
|
155
|
+
clearInterval(prompt.processMonitor);
|
|
156
|
+
prompt.processMonitor = undefined;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function formatTerminalHeader(status: "running" | "done" | "stopped" | "error"): {
|
|
160
|
+
title: string;
|
|
161
|
+
template?: string;
|
|
162
|
+
} {
|
|
163
|
+
if (status === "stopped") return { title: "已停止", template: "red" };
|
|
164
|
+
if (status === "error") return { title: "异常结束", template: "red" };
|
|
165
|
+
return { title: "完成" };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function turnFinalStatus(status: "running" | "done" | "stopped" | "error"): "done" | "stopped" {
|
|
169
|
+
return status === "stopped" || status === "error" ? "stopped" : "done";
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function startPromptProcessMonitor(sessionId: string, info: ToolProcessInfo): void {
|
|
173
|
+
const prompt = activePrompts.get(sessionId);
|
|
174
|
+
if (!prompt) return;
|
|
175
|
+
prompt.processPid = info.pid;
|
|
176
|
+
clearPromptProcessMonitor(sessionId);
|
|
177
|
+
|
|
178
|
+
const check = async () => {
|
|
179
|
+
const current = activePrompts.get(sessionId);
|
|
180
|
+
if (!current || current !== prompt) {
|
|
181
|
+
clearPromptProcessMonitor(sessionId);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
if (current.stopped || current.abnormalExit) return;
|
|
185
|
+
if (isProcessAliveImpl(info.pid)) return;
|
|
186
|
+
|
|
187
|
+
current.abnormalExit = true;
|
|
188
|
+
clearPromptProcessMonitor(sessionId);
|
|
189
|
+
|
|
190
|
+
const state = await readStreamState(sessionId);
|
|
191
|
+
if (state?.status === "running") {
|
|
192
|
+
await writeStreamState({
|
|
193
|
+
...state,
|
|
194
|
+
status: "error",
|
|
195
|
+
updatedAt: Date.now(),
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const chatId = pickDisplayChat(sessionId) ?? getLastActiveChat(sessionId) ?? getChatsForSession(sessionId)[0];
|
|
200
|
+
const p = chatId ? platformForChat(chatId) : null;
|
|
201
|
+
if (chatId && p && !current.abnormalExitNotified) {
|
|
202
|
+
current.abnormalExitNotified = true;
|
|
203
|
+
await p.sendText(
|
|
204
|
+
chatId,
|
|
205
|
+
`⚠️ 进程异常结束:session ${sessionId} 对应的 CLI 进程 PID ${info.pid} 已不存在,已按完成处理。若回复不完整,请重新发送上一条指令。`,
|
|
206
|
+
).catch(() => {});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// 主动关闭 readline,让 runAgentSession 的 finally 落盘 error 终态并清理 activePrompts。
|
|
210
|
+
current.controller.abort();
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const handle = setInterval(() => {
|
|
214
|
+
void check().catch((err) => {
|
|
215
|
+
console.warn(`[${ts()}] [PROCESS-MONITOR] check failed for ${sessionId}: ${(err as Error).message}`);
|
|
216
|
+
});
|
|
217
|
+
}, processMonitorIntervalMs);
|
|
218
|
+
handle.unref?.();
|
|
219
|
+
prompt.processMonitor = handle;
|
|
220
|
+
}
|
|
221
|
+
|
|
117
222
|
export function _getPlatformForChatForTest(chatId: string): PlatformAdapter | null {
|
|
118
223
|
return platformForChat(chatId);
|
|
119
224
|
}
|
|
@@ -189,6 +294,9 @@ export function resetState(): void {
|
|
|
189
294
|
processedMessages.clear();
|
|
190
295
|
lastMsgTimestamps.clear();
|
|
191
296
|
chatPlatformMap.clear();
|
|
297
|
+
for (const prompt of activePrompts.values()) {
|
|
298
|
+
if (prompt.processMonitor) clearInterval(prompt.processMonitor);
|
|
299
|
+
}
|
|
192
300
|
activePrompts.clear();
|
|
193
301
|
displayCards.clear();
|
|
194
302
|
stopUnifiedDisplayLoop();
|
|
@@ -199,13 +307,39 @@ export function resetState(): void {
|
|
|
199
307
|
// 是 onReady/onReconnected 取代 resetState 的正确入口。
|
|
200
308
|
|
|
201
309
|
// ---------------------------------------------------------------------------
|
|
202
|
-
// Adapter: 按 tool 创建并缓存
|
|
310
|
+
// Adapter: 按 tool + effectiveModel 创建并缓存
|
|
203
311
|
// ---------------------------------------------------------------------------
|
|
204
312
|
|
|
205
313
|
const adapterCache = new Map<string, ToolAdapter>();
|
|
206
314
|
|
|
207
|
-
|
|
208
|
-
|
|
315
|
+
// Per-session 模型覆盖(/model 命令设置,不持久化)
|
|
316
|
+
const sessionModelOverrides = new Map<string, string>();
|
|
317
|
+
|
|
318
|
+
/** 返回 session 的生效模型:优先 per-session 覆盖,其次全局配置 */
|
|
319
|
+
function getModelForSession(sessionId?: string): string {
|
|
320
|
+
if (sessionId) {
|
|
321
|
+
const override = sessionModelOverrides.get(sessionId);
|
|
322
|
+
if (override) return override;
|
|
323
|
+
}
|
|
324
|
+
return CLAUDE_MODEL;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/** 为指定 session 设置模型覆盖(/model pro、/model flash) */
|
|
328
|
+
export function setSessionModelOverride(sessionId: string, model: string): void {
|
|
329
|
+
sessionModelOverrides.set(sessionId, model);
|
|
330
|
+
adapterCache.clear();
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/** 清除指定 session 的模型覆盖(/model clear) */
|
|
334
|
+
export function clearSessionModelOverride(sessionId: string): void {
|
|
335
|
+
sessionModelOverrides.delete(sessionId);
|
|
336
|
+
adapterCache.clear();
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
export function getAdapterForTool(tool: string, sessionId?: string): ToolAdapter {
|
|
340
|
+
const effectiveModel = tool === "claude" ? getModelForSession(sessionId) : "";
|
|
341
|
+
const cacheKey = tool === "claude" ? `${tool}:${effectiveModel}` : tool;
|
|
342
|
+
const cached = adapterCache.get(cacheKey);
|
|
209
343
|
if (cached) return cached;
|
|
210
344
|
|
|
211
345
|
let adapter: ToolAdapter;
|
|
@@ -215,7 +349,7 @@ export function getAdapterForTool(tool: string): ToolAdapter {
|
|
|
215
349
|
adapter = createCodexAdapter();
|
|
216
350
|
} else {
|
|
217
351
|
adapter = createClaudeAdapter({
|
|
218
|
-
model:
|
|
352
|
+
model: effectiveModel,
|
|
219
353
|
subagentModel: CLAUDE_SUBAGENT_MODEL,
|
|
220
354
|
effort: CLAUDE_EFFORT,
|
|
221
355
|
isEmpty: isAnthropicConfigEmpty,
|
|
@@ -223,7 +357,7 @@ export function getAdapterForTool(tool: string): ToolAdapter {
|
|
|
223
357
|
baseUrl: CLAUDE_BASE_URL,
|
|
224
358
|
});
|
|
225
359
|
}
|
|
226
|
-
adapterCache.set(
|
|
360
|
+
adapterCache.set(cacheKey, adapter);
|
|
227
361
|
return adapter;
|
|
228
362
|
}
|
|
229
363
|
|
|
@@ -640,7 +774,7 @@ export async function switchChatBinding(args: SwitchChatBindingArgs): Promise<Sw
|
|
|
640
774
|
* Claude 显示 model/effort(来自环境变量);Cursor 显示 model(运行时由
|
|
641
775
|
* cursor-agent 决定,初次创建时尚未学习到,故显示占位)。
|
|
642
776
|
*/
|
|
643
|
-
function formatToolConfigForLog(tool: string, sessionModel?: string): string {
|
|
777
|
+
function formatToolConfigForLog(tool: string, sessionModel?: string, sessionId?: string): string {
|
|
644
778
|
if (tool === "cursor") {
|
|
645
779
|
return `model=${sessionModel ?? "(由 cursor-agent 决定,init 事件后学习)"}`;
|
|
646
780
|
}
|
|
@@ -653,7 +787,7 @@ function formatToolConfigForLog(tool: string, sessionModel?: string): string {
|
|
|
653
787
|
: "effort=(由 codex config.toml 决定)";
|
|
654
788
|
return `model=${modelStr}, ${effortStr}`;
|
|
655
789
|
}
|
|
656
|
-
return `model=${anthropicConfigDisplay(
|
|
790
|
+
return `model=${anthropicConfigDisplay(getModelForSession(sessionId))}, subagentModel=${anthropicConfigDisplay(CLAUDE_SUBAGENT_MODEL)}, effort=${anthropicConfigDisplay(CLAUDE_EFFORT)}`;
|
|
657
791
|
}
|
|
658
792
|
|
|
659
793
|
export async function initClaudeSession(tool: string, overrideCwd?: string, chatId?: string): Promise<{ sessionId: string; cwd: string }> {
|
|
@@ -733,12 +867,12 @@ export async function runAgentSession(
|
|
|
733
867
|
let info: Awaited<ReturnType<ToolAdapter["getSessionInfo"]>>;
|
|
734
868
|
let cwd: string;
|
|
735
869
|
try {
|
|
736
|
-
adapter = getAdapterForTool(tool);
|
|
870
|
+
adapter = getAdapterForTool(tool, sessionId);
|
|
737
871
|
info = await adapter.getSessionInfo(sessionId);
|
|
738
872
|
cwd = info?.cwd ?? (await getDefaultCwd(_chatId));
|
|
739
873
|
if (tid) logTrace(tid, "SESSION_START", { sessionId, tool, cwd, turn: (sessionInfoMap.get(_chatId)?.turnCount ?? 0) + 1 });
|
|
740
874
|
console.log(
|
|
741
|
-
`[${ts()}] Running ${adapter.displayName} session: ${sessionId} (${formatToolConfigForLog(tool, info?.model)}, cwd=${cwd})`
|
|
875
|
+
`[${ts()}] Running ${adapter.displayName} session: ${sessionId} (${formatToolConfigForLog(tool, info?.model, sessionId)}, cwd=${cwd})`
|
|
742
876
|
);
|
|
743
877
|
|
|
744
878
|
// 构建 IM skills prompt(sessionId 方式,无 token)
|
|
@@ -833,13 +967,12 @@ export async function runAgentSession(
|
|
|
833
967
|
// terminal state,发送了 finalReply 并删除/替换了 displayCards 条目。
|
|
834
968
|
// 通过引用比较检测——若统一 loop 已处理则只补持久化和头像,不重复发。
|
|
835
969
|
if (displayCards.get(displayChatId) !== display) {
|
|
836
|
-
const finalStatus = prevState.status
|
|
970
|
+
const finalStatus = turnFinalStatus(prevState.status);
|
|
837
971
|
finalizeTurnCards(sessionId, prevState.turnCount, finalStatus).catch(() => {});
|
|
838
972
|
pp.setChatAvatar(displayChatId, prevState.tool, "idle").catch(() => {});
|
|
839
973
|
} else {
|
|
840
974
|
const nextSeq = display.sequence + 1;
|
|
841
|
-
const headerTitle = prevState.status
|
|
842
|
-
const headerTemplate = prevState.status === "stopped" ? "red" : undefined;
|
|
975
|
+
const { title: headerTitle, template: headerTemplate } = formatTerminalHeader(prevState.status);
|
|
843
976
|
const cardContent = truncateContent(prevState.accumulatedContent + prevState.finalReply) || " ";
|
|
844
977
|
const doneCard = buildProgressCard(cardContent, { showStop: false, headerTitle, headerTemplate });
|
|
845
978
|
await pp.cardUpdate(display.cardId, doneCard, nextSeq).catch(() => {});
|
|
@@ -848,7 +981,7 @@ export async function runAgentSession(
|
|
|
848
981
|
displayCards.delete(displayChatId);
|
|
849
982
|
|
|
850
983
|
// 持久化:标记上一轮所有卡片为终态
|
|
851
|
-
const finalStatus = prevState.status
|
|
984
|
+
const finalStatus = turnFinalStatus(prevState.status);
|
|
852
985
|
finalizeTurnCards(sessionId, prevState.turnCount, finalStatus).catch(() => {});
|
|
853
986
|
|
|
854
987
|
if (prevState.finalReply && stillOursAfterUpdate && !isFinalReplySentForTurn(prevState)) {
|
|
@@ -858,7 +991,7 @@ export async function runAgentSession(
|
|
|
858
991
|
}
|
|
859
992
|
} else if (pp && prevState.finalReply && !isFinalReplySentForTurn(prevState)) {
|
|
860
993
|
// 无 display 记录但上一轮有 finalReply(极快轮次),至少发送
|
|
861
|
-
const finalStatus = prevState.status
|
|
994
|
+
const finalStatus = turnFinalStatus(prevState.status);
|
|
862
995
|
finalizeTurnCards(sessionId, prevState.turnCount, finalStatus).catch(() => {});
|
|
863
996
|
await sendFinalReplyTextOnce(pp, displayChatId, sessionId, prevState.turnCount, prevState.finalReply);
|
|
864
997
|
}
|
|
@@ -928,7 +1061,14 @@ export async function runAgentSession(
|
|
|
928
1061
|
const toolCallMap = new Map<string, { name: string; input: unknown }>();
|
|
929
1062
|
|
|
930
1063
|
try {
|
|
931
|
-
for await (const unifiedMsg of adapter.prompt(sessionId, userTextWithCapabilities, cwd, controller.signal
|
|
1064
|
+
for await (const unifiedMsg of adapter.prompt(sessionId, userTextWithCapabilities, cwd, controller.signal, {
|
|
1065
|
+
onProcessStart: (processInfo) => {
|
|
1066
|
+
startPromptProcessMonitor(sessionId, processInfo);
|
|
1067
|
+
},
|
|
1068
|
+
onProcessExit: () => {
|
|
1069
|
+
clearPromptProcessMonitor(sessionId);
|
|
1070
|
+
},
|
|
1071
|
+
})) {
|
|
932
1072
|
for (const block of unifiedMsg.blocks) {
|
|
933
1073
|
accumulateBlockContent(block, state, toolCallMap);
|
|
934
1074
|
|
|
@@ -971,13 +1111,15 @@ export async function runAgentSession(
|
|
|
971
1111
|
// 标记 prompt 结束
|
|
972
1112
|
const prompt = activePrompts.get(sessionId);
|
|
973
1113
|
const wasStopped = prompt?.stopped ?? false;
|
|
1114
|
+
const wasAbnormalExit = prompt?.abnormalExit ?? false;
|
|
1115
|
+
clearPromptProcessMonitor(sessionId);
|
|
974
1116
|
activePrompts.delete(sessionId);
|
|
975
1117
|
|
|
976
1118
|
// 先写最终状态(done/stopped),确保 display loop 在下一轮消费前
|
|
977
1119
|
// 读到新状态并终结旧卡片。否则 setImmediate 在 CHECK 阶段先于
|
|
978
1120
|
// writeFile I/O(POLL 阶段)执行,display loop 会误以为旧轮仍在
|
|
979
1121
|
// 运行中并更新旧卡片,而不是新建卡片。
|
|
980
|
-
const finalStatus = wasStopped ? "stopped" : "done";
|
|
1122
|
+
const finalStatus = wasAbnormalExit ? "error" : wasStopped ? "stopped" : "done";
|
|
981
1123
|
const finalReply = pickFinalReply(state).trim();
|
|
982
1124
|
await writeStreamState({
|
|
983
1125
|
sessionId,
|
|
@@ -1039,6 +1181,23 @@ export async function runAgentSession(
|
|
|
1039
1181
|
if (active1) platform.setChatAvatar(active1, tool, "idle").catch(() => {});
|
|
1040
1182
|
console.log(`[${ts()}] Session ${sessionId} stopped (content chunks: ${state.chunkCount})`);
|
|
1041
1183
|
if (tid) logTrace(tid, "SESSION_END", { sessionId, outcome: "stopped", chunks: state.chunkCount });
|
|
1184
|
+
} else if (wasAbnormalExit) {
|
|
1185
|
+
for (const cid of getChatsForSession(sessionId)) {
|
|
1186
|
+
const finfo = sessionInfoMap.get(cid);
|
|
1187
|
+
await recordSessionRegistry({
|
|
1188
|
+
chatId: cid,
|
|
1189
|
+
sessionId,
|
|
1190
|
+
tool,
|
|
1191
|
+
turnCount: finfo?.turnCount ?? nextTurnCount,
|
|
1192
|
+
lastContextTokens: finfo?.lastContextTokens ?? nextContextTokens,
|
|
1193
|
+
startTime: finfo?.startTime ?? now,
|
|
1194
|
+
running: false,
|
|
1195
|
+
});
|
|
1196
|
+
}
|
|
1197
|
+
const activeErr = getLastActiveChat(sessionId) ?? getChatsForSession(sessionId)[0];
|
|
1198
|
+
if (activeErr) platform.setChatAvatar(activeErr, tool, "idle").catch(() => {});
|
|
1199
|
+
console.log(`[${ts()}] Session ${sessionId} process exited unexpectedly (content chunks: ${state.chunkCount})`);
|
|
1200
|
+
if (tid) logTrace(tid, "SESSION_END", { sessionId, outcome: "process_missing", chunks: state.chunkCount });
|
|
1042
1201
|
} else {
|
|
1043
1202
|
for (const cid of getChatsForSession(sessionId)) {
|
|
1044
1203
|
const finfo = sessionInfoMap.get(cid);
|
|
@@ -1140,8 +1299,7 @@ export function startUnifiedDisplayLoop(): void {
|
|
|
1140
1299
|
// 发送最终结果(卡片平台)
|
|
1141
1300
|
while (display.cardBusy) await new Promise(r => setTimeout(r, 20));
|
|
1142
1301
|
const nextSeq = display.sequence + 1;
|
|
1143
|
-
const headerTitle = state.status
|
|
1144
|
-
const headerTemplate = state.status === "stopped" ? "red" : undefined;
|
|
1302
|
+
const { title: headerTitle, template: headerTemplate } = formatTerminalHeader(state.status);
|
|
1145
1303
|
const cardContent = truncateContent(state.accumulatedContent + state.finalReply) || " ";
|
|
1146
1304
|
const doneCard = buildProgressCard(cardContent, { showStop: false, headerTitle, headerTemplate });
|
|
1147
1305
|
await p.cardUpdate(display.cardId, doneCard, nextSeq).catch(() => {});
|
|
@@ -1156,7 +1314,7 @@ export function startUnifiedDisplayLoop(): void {
|
|
|
1156
1314
|
continue;
|
|
1157
1315
|
}
|
|
1158
1316
|
|
|
1159
|
-
const finalSt = state.status
|
|
1317
|
+
const finalSt = turnFinalStatus(state.status);
|
|
1160
1318
|
finalizeTurnCards(sessionId, state.turnCount, finalSt).catch(() => {});
|
|
1161
1319
|
displayCards.delete(chatId);
|
|
1162
1320
|
if (state.finalReply) {
|
|
@@ -1343,6 +1501,7 @@ export function stopSession(sessionId: string): boolean {
|
|
|
1343
1501
|
const prompt = activePrompts.get(sessionId);
|
|
1344
1502
|
if (!prompt) return false;
|
|
1345
1503
|
prompt.stopped = true;
|
|
1504
|
+
clearPromptProcessMonitor(sessionId);
|
|
1346
1505
|
cancelQueuedMessage(sessionId);
|
|
1347
1506
|
prompt.controller.abort();
|
|
1348
1507
|
console.log(`[${ts()}] [STOP] Session ${sessionId} aborted`);
|
|
@@ -1408,7 +1567,7 @@ async function resolveModelEffort(
|
|
|
1408
1567
|
if (tool === "cursor") {
|
|
1409
1568
|
let model = UNKNOWN_MODEL_PLACEHOLDER;
|
|
1410
1569
|
try {
|
|
1411
|
-
const adapter = getAdapterForTool(tool);
|
|
1570
|
+
const adapter = getAdapterForTool(tool, sessionId);
|
|
1412
1571
|
const info = await adapter.getSessionInfo(sessionId);
|
|
1413
1572
|
if (info?.model) model = info.model;
|
|
1414
1573
|
} catch {
|
|
@@ -1425,7 +1584,7 @@ async function resolveModelEffort(
|
|
|
1425
1584
|
};
|
|
1426
1585
|
}
|
|
1427
1586
|
return {
|
|
1428
|
-
model: anthropicConfigDisplay(
|
|
1587
|
+
model: anthropicConfigDisplay(getModelForSession(sessionId)),
|
|
1429
1588
|
effort: anthropicConfigDisplay(CLAUDE_EFFORT),
|
|
1430
1589
|
};
|
|
1431
1590
|
}
|
|
@@ -1434,7 +1593,8 @@ export async function getSessionStatus(chatId: string): Promise<SessionStatus |
|
|
|
1434
1593
|
const info = sessionInfoMap.get(chatId);
|
|
1435
1594
|
if (!info) return null;
|
|
1436
1595
|
|
|
1437
|
-
const
|
|
1596
|
+
const activePrompt = activePrompts.get(info.sessionId);
|
|
1597
|
+
const isActive = !!activePrompt && !activePrompt.stopped && !activePrompt.abnormalExit;
|
|
1438
1598
|
const { model, effort } = await resolveModelEffort(info.tool, info.sessionId);
|
|
1439
1599
|
|
|
1440
1600
|
const registry = await loadSessionRegistry();
|
|
@@ -1509,7 +1669,9 @@ export async function getAllSessionsStatus(): Promise<SessionsListEntry[]> {
|
|
|
1509
1669
|
chatId: info.chatId,
|
|
1510
1670
|
sessionId: info.sessionId,
|
|
1511
1671
|
chatName: info.chatName || "",
|
|
1512
|
-
active: activePrompts.
|
|
1672
|
+
active: !!activePrompts.get(info.sessionId) &&
|
|
1673
|
+
!activePrompts.get(info.sessionId)?.stopped &&
|
|
1674
|
+
!activePrompts.get(info.sessionId)?.abnormalExit,
|
|
1513
1675
|
turnCount: info.turnCount,
|
|
1514
1676
|
startTime: info.startTime,
|
|
1515
1677
|
model,
|
package/src/stream-state.ts
CHANGED
|
@@ -9,17 +9,17 @@ import { USER_DATA_DIR, ts } from "./config.ts";
|
|
|
9
9
|
|
|
10
10
|
export const STREAMS_DIR = join(USER_DATA_DIR, "state", "streams");
|
|
11
11
|
|
|
12
|
-
export interface StreamState {
|
|
13
|
-
sessionId: string;
|
|
14
|
-
status: "running" | "done" | "stopped" | "error";
|
|
15
|
-
accumulatedContent: string;
|
|
16
|
-
finalReply: string;
|
|
17
|
-
/** The turn whose terminal text reply has already been delivered to IM. */
|
|
18
|
-
finalReplySentTurn?: number;
|
|
19
|
-
finalReplySentAt?: number;
|
|
20
|
-
chunkCount: number;
|
|
21
|
-
turnCount: number;
|
|
22
|
-
contextTokens: number;
|
|
12
|
+
export interface StreamState {
|
|
13
|
+
sessionId: string;
|
|
14
|
+
status: "running" | "done" | "stopped" | "error";
|
|
15
|
+
accumulatedContent: string;
|
|
16
|
+
finalReply: string;
|
|
17
|
+
/** The turn whose terminal text reply has already been delivered to IM. */
|
|
18
|
+
finalReplySentTurn?: number;
|
|
19
|
+
finalReplySentAt?: number;
|
|
20
|
+
chunkCount: number;
|
|
21
|
+
turnCount: number;
|
|
22
|
+
contextTokens: number;
|
|
23
23
|
updatedAt: number;
|
|
24
24
|
cwd: string;
|
|
25
25
|
tool: string;
|
|
@@ -74,7 +74,7 @@ export function _resetRenameForTest(): void {
|
|
|
74
74
|
* 真文件,这一帧可能被 reader 读到半截,但 readStreamState 的 try/catch 兜底
|
|
75
75
|
* 会让 loop 跳过这一帧,2 秒后下次 write 大概率成功——比写盘失败丢数据好。
|
|
76
76
|
*/
|
|
77
|
-
export async function writeStreamState(state: StreamState): Promise<void> {
|
|
77
|
+
export async function writeStreamState(state: StreamState): Promise<void> {
|
|
78
78
|
const filePath = getStreamStatePath(state.sessionId);
|
|
79
79
|
const payload = JSON.stringify(state, null, 2);
|
|
80
80
|
try {
|
|
@@ -98,26 +98,26 @@ export async function writeStreamState(state: StreamState): Promise<void> {
|
|
|
98
98
|
} catch (err) {
|
|
99
99
|
console.error(`[${ts()}] Failed to write stream-state for ${state.sessionId}: ${(err as Error).message}`);
|
|
100
100
|
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
export function isFinalReplySentForTurn(state: Pick<StreamState, "turnCount" | "finalReplySentTurn">): boolean {
|
|
104
|
-
return state.finalReplySentTurn === state.turnCount;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
export async function markFinalReplySent(sessionId: string, turnCount: number, sentAt = Date.now()): Promise<void> {
|
|
108
|
-
const state = await readStreamState(sessionId);
|
|
109
|
-
if (!state) return;
|
|
110
|
-
if (state.turnCount !== turnCount) return;
|
|
111
|
-
if (state.status === "running") return;
|
|
112
|
-
|
|
113
|
-
state.finalReplySentTurn = turnCount;
|
|
114
|
-
state.finalReplySentAt = sentAt;
|
|
115
|
-
state.updatedAt = Date.now();
|
|
116
|
-
await writeStreamState(state);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
export function createEmptyStreamState(sessionId: string, cwd: string, tool: string, turnCount: number): StreamState {
|
|
120
|
-
return {
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function isFinalReplySentForTurn(state: Pick<StreamState, "turnCount" | "finalReplySentTurn">): boolean {
|
|
104
|
+
return state.finalReplySentTurn === state.turnCount;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export async function markFinalReplySent(sessionId: string, turnCount: number, sentAt = Date.now()): Promise<void> {
|
|
108
|
+
const state = await readStreamState(sessionId);
|
|
109
|
+
if (!state) return;
|
|
110
|
+
if (state.turnCount !== turnCount) return;
|
|
111
|
+
if (state.status === "running") return;
|
|
112
|
+
|
|
113
|
+
state.finalReplySentTurn = turnCount;
|
|
114
|
+
state.finalReplySentAt = sentAt;
|
|
115
|
+
state.updatedAt = Date.now();
|
|
116
|
+
await writeStreamState(state);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function createEmptyStreamState(sessionId: string, cwd: string, tool: string, turnCount: number): StreamState {
|
|
120
|
+
return {
|
|
121
121
|
sessionId,
|
|
122
122
|
status: "running",
|
|
123
123
|
accumulatedContent: "",
|
|
@@ -157,4 +157,4 @@ export async function fixStaleStreamStates(): Promise<void> {
|
|
|
157
157
|
} catch (err) {
|
|
158
158
|
console.error(`[${ts()}] [STREAM-STATE] fixStaleStreamStates failed: ${(err as Error).message}`);
|
|
159
159
|
}
|
|
160
|
-
}
|
|
160
|
+
}
|