chatccc 0.2.116 → 0.2.118
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 +22 -14
- package/package.json +1 -1
- package/src/__tests__/orchestrator.test.ts +1 -1
- package/src/adapters/adapter-interface.ts +7 -0
- package/src/adapters/claude-adapter.ts +498 -495
- package/src/adapters/cursor-adapter.ts +11 -1
- package/src/cards.ts +2 -0
- package/src/config.ts +18 -0
- package/src/orchestrator.ts +160 -15
- package/src/session-chat-binding.ts +2 -0
- package/src/session.ts +12 -2
|
@@ -261,8 +261,18 @@ function spawnAgent(
|
|
|
261
261
|
cwd?: string,
|
|
262
262
|
stdinText?: string,
|
|
263
263
|
modelOverride?: string,
|
|
264
|
+
permissionMode?: "plan" | "ask",
|
|
264
265
|
): ChildProcess {
|
|
265
266
|
let allArgs = [...CURSOR_AGENT_ARGS, ...extraArgs];
|
|
267
|
+
if (permissionMode === "ask") {
|
|
268
|
+
// 替换已有的 --mode 或追加
|
|
269
|
+
const modeIdx = allArgs.findIndex((a, i) => a === "--mode" && i + 1 < allArgs.length);
|
|
270
|
+
if (modeIdx >= 0) {
|
|
271
|
+
allArgs[modeIdx + 1] = "ask";
|
|
272
|
+
} else {
|
|
273
|
+
allArgs.push("--mode", "ask");
|
|
274
|
+
}
|
|
275
|
+
}
|
|
266
276
|
if (modelOverride) {
|
|
267
277
|
// 替换全局 --model 为 per-session override
|
|
268
278
|
const modelIdx = allArgs.findIndex((a, i) => a === "--model" && i + 1 < allArgs.length);
|
|
@@ -370,7 +380,7 @@ class CursorAdapter implements ToolAdapter {
|
|
|
370
380
|
options?: ToolPromptOptions,
|
|
371
381
|
): AsyncIterable<UnifiedStreamMessage> {
|
|
372
382
|
console.log(`[Cursor debug] prompt start: sessionId=${sessionId}, cwd=${cwd}, userTextLen=${userText.length}`);
|
|
373
|
-
const proc = spawnAgent(["--resume", sessionId], cwd, userText, this.modelOverride);
|
|
383
|
+
const proc = spawnAgent(["--resume", sessionId], cwd, userText, this.modelOverride, options?.permissionMode);
|
|
374
384
|
this.activeProcs.add(proc);
|
|
375
385
|
if (proc.pid !== undefined) options?.onProcessStart?.({ pid: proc.pid });
|
|
376
386
|
|
package/src/cards.ts
CHANGED
|
@@ -131,6 +131,8 @@ export function buildHelpCard(
|
|
|
131
131
|
"发送 **/new cursor** 创建新 Cursor 会话",
|
|
132
132
|
"发送 **/new codex** 创建新 Codex 会话",
|
|
133
133
|
"发送 **/newh** 重置当前会话(沿用当前工作目录,不切换)",
|
|
134
|
+
"发送 **/plan <消息>** 在 Claude 会话中以只读规划模式提问",
|
|
135
|
+
"发送 **/ask <消息>** 在 Cursor 会话中以只读问答模式提问",
|
|
134
136
|
].join("\n");
|
|
135
137
|
return JSON.stringify({
|
|
136
138
|
config: { wide_screen_mode: true },
|
package/src/config.ts
CHANGED
|
@@ -122,6 +122,24 @@ export interface AppConfig {
|
|
|
122
122
|
export type AgentTool = "claude" | "cursor" | "codex";
|
|
123
123
|
export const AGENT_TOOLS: AgentTool[] = ["claude", "cursor", "codex"];
|
|
124
124
|
|
|
125
|
+
/**
|
|
126
|
+
* Agent 专属模式指令(内部配置,非用户可配)。
|
|
127
|
+
* 映射每个 Agent 支持的特殊指令及其对应的 CLI 权限模式。
|
|
128
|
+
*
|
|
129
|
+
* - claude: /plan → --permission-mode plan(只读/规划)
|
|
130
|
+
* - cursor: /ask → --mode ask(只读/问答)
|
|
131
|
+
* - codex: 暂无原生只读模式,不注册任何模式指令
|
|
132
|
+
*/
|
|
133
|
+
export const AGENT_MODE_COMMANDS: Record<AgentTool, { command: string; mode: "plan" | "ask"; description: string }[]> = {
|
|
134
|
+
claude: [
|
|
135
|
+
{ command: "/plan", mode: "plan", description: "以只读规划模式提问(Claude 专属)" },
|
|
136
|
+
],
|
|
137
|
+
cursor: [
|
|
138
|
+
{ command: "/ask", mode: "ask", description: "以只读问答模式提问(Cursor 专属)" },
|
|
139
|
+
],
|
|
140
|
+
codex: [],
|
|
141
|
+
};
|
|
142
|
+
|
|
125
143
|
/** 获取指定 agent 配置中所有模型相关的值(最多 100 个,去重) */
|
|
126
144
|
export function getAllModelsForTool(tool: AgentTool, cfg: AppConfig = config): string[] {
|
|
127
145
|
const seen = new Set<string>();
|
package/src/orchestrator.ts
CHANGED
|
@@ -19,6 +19,8 @@ import {
|
|
|
19
19
|
CONFIG_FILE,
|
|
20
20
|
GIT_TIMEOUT_MS,
|
|
21
21
|
PROJECT_ROOT,
|
|
22
|
+
AGENT_MODE_COMMANDS,
|
|
23
|
+
AGENT_TOOLS,
|
|
22
24
|
anthropicConfigDisplay,
|
|
23
25
|
config,
|
|
24
26
|
fileLog,
|
|
@@ -125,6 +127,37 @@ function isNonWechatP2p(platform: PlatformAdapter, chatType: string): boolean {
|
|
|
125
127
|
return chatType === "p2p" && platform.kind !== "wechat";
|
|
126
128
|
}
|
|
127
129
|
|
|
130
|
+
/** 根据 Agent 类型构建欢迎卡片中的指令说明 */
|
|
131
|
+
function buildWelcomeCommands(tool: string): string {
|
|
132
|
+
const lines = [
|
|
133
|
+
"发送 **/cd** 切换新建会话的默认目录。",
|
|
134
|
+
"发送 **/model** 查看或切换当前会话的模型。",
|
|
135
|
+
"发送 **/new** 创建新会话,**/newh** 重置当前会话(沿用工作目录)。",
|
|
136
|
+
"发送 **/sessions** 查看所有会话状态。",
|
|
137
|
+
];
|
|
138
|
+
const modeCmds = AGENT_MODE_COMMANDS[tool as AgentTool] ?? [];
|
|
139
|
+
for (const mc of modeCmds) {
|
|
140
|
+
lines.push(`发送 **${mc.command} <消息>** ${mc.description}。`);
|
|
141
|
+
}
|
|
142
|
+
return lines.join("\n");
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** 根据指令名从 AGENT_MODE_COMMANDS 查找对应的 mode 和支持的 agent 列表 */
|
|
146
|
+
function resolveModeCommand(cmd: string): { mode: "plan" | "ask"; supportedTools: string[] } | null {
|
|
147
|
+
const supportedTools: string[] = [];
|
|
148
|
+
let mode: "plan" | "ask" | null = null;
|
|
149
|
+
for (const tool of AGENT_TOOLS) {
|
|
150
|
+
for (const mc of AGENT_MODE_COMMANDS[tool]) {
|
|
151
|
+
if (mc.command === cmd) {
|
|
152
|
+
supportedTools.push(tool);
|
|
153
|
+
mode = mc.mode;
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return mode ? { mode, supportedTools } : null;
|
|
159
|
+
}
|
|
160
|
+
|
|
128
161
|
async function cleanupNonWechatP2pBinding(
|
|
129
162
|
platform: PlatformAdapter,
|
|
130
163
|
chatId: string,
|
|
@@ -429,11 +462,7 @@ export async function handleCommand(
|
|
|
429
462
|
`**Session ID:** ${sessionId}\n` +
|
|
430
463
|
`**工作目录:** \`${cwd}\`\n\n` +
|
|
431
464
|
`直接在这里发消息即可与 ${toolLabel} 对话。\n\n` +
|
|
432
|
-
|
|
433
|
-
`发送 **/model** 查看或切换当前会话的模型。\n` +
|
|
434
|
-
`发送 **/new** 创建新会话,**/newh** 重置当前会话(沿用工作目录)。\n` +
|
|
435
|
-
`发送 **/sessions** 查看所有会话状态。\n` +
|
|
436
|
-
`发送 \`/git <子命令>\` 在本会话工作目录执行 git,例如 \`/git status\`、\`/git log --oneline -n 5\`。`,
|
|
465
|
+
buildWelcomeCommands(tool),
|
|
437
466
|
"green",
|
|
438
467
|
);
|
|
439
468
|
console.log(
|
|
@@ -515,11 +544,7 @@ export async function handleCommand(
|
|
|
515
544
|
`**Session ID:** ${sessionId}\n` +
|
|
516
545
|
`**工作目录:** \`${cwd}\`\n\n` +
|
|
517
546
|
`直接在这里发消息即可与 ${toolLabel} 对话。\n\n` +
|
|
518
|
-
|
|
519
|
-
`发送 **/model** 查看或切换当前会话的模型。\n` +
|
|
520
|
-
`发送 **/new** 创建新会话,**/newh** 重置当前会话(沿用工作目录)。\n` +
|
|
521
|
-
`发送 **/sessions** 查看所有会话状态。\n` +
|
|
522
|
-
`发送 \`/git <子命令>\` 在本会话工作目录执行 git,例如 \`/git status\`、\`/git log --oneline -n 5\`。`,
|
|
547
|
+
buildWelcomeCommands(tool),
|
|
523
548
|
"green",
|
|
524
549
|
);
|
|
525
550
|
|
|
@@ -877,9 +902,8 @@ export async function handleCommand(
|
|
|
877
902
|
`会话已重置为新的 **${toolLabel}** 会话。\n\n` +
|
|
878
903
|
`**Session ID:** ${newSessionId}\n` +
|
|
879
904
|
`**工作目录:** \`${cwd}\`(沿用当前会话目录)\n\n` +
|
|
880
|
-
`直接在这里发消息即可继续对话。\n` +
|
|
881
|
-
|
|
882
|
-
`发送 **/model** 查看或切换当前会话的模型。`,
|
|
905
|
+
`直接在这里发消息即可继续对话。\n\n` +
|
|
906
|
+
buildWelcomeCommands(descriptionTool),
|
|
883
907
|
"green",
|
|
884
908
|
);
|
|
885
909
|
|
|
@@ -1181,6 +1205,128 @@ export async function handleCommand(
|
|
|
1181
1205
|
return;
|
|
1182
1206
|
}
|
|
1183
1207
|
|
|
1208
|
+
// /plan <message> — 只读/规划模式
|
|
1209
|
+
if (textLower.startsWith("/plan ")) {
|
|
1210
|
+
const planText = text.slice(6).trim();
|
|
1211
|
+
const resolved = resolveModeCommand("/plan");
|
|
1212
|
+
if (!planText) {
|
|
1213
|
+
await platform.sendText(chatId, "用法:`/plan <消息>` — 在只读/规划模式下提问。").catch(() => {});
|
|
1214
|
+
logTrace(tid, "DONE", { outcome: "plan_no_message" });
|
|
1215
|
+
return;
|
|
1216
|
+
}
|
|
1217
|
+
if (!resolved || !resolved.supportedTools.includes(descriptionTool)) {
|
|
1218
|
+
const supported = resolved?.supportedTools.map(t => toolDisplayName(t)).join("、") ?? "(无)";
|
|
1219
|
+
await platform.sendText(chatId, `/plan 仅在以下 Agent 会话中可用:${supported}。`).catch(() => {});
|
|
1220
|
+
logTrace(tid, "DONE", { outcome: "plan_wrong_tool", tool: descriptionTool });
|
|
1221
|
+
return;
|
|
1222
|
+
}
|
|
1223
|
+
logTrace(tid, "BRANCH", { cmd: "/plan", sessionId, tool: descriptionTool });
|
|
1224
|
+
|
|
1225
|
+
if (isSessionRunning(sessionId)) {
|
|
1226
|
+
const queued = enqueueMessage(sessionId, {
|
|
1227
|
+
text, chatId, openId, msgTimestamp, chatType, traceId: tid,
|
|
1228
|
+
});
|
|
1229
|
+
if (queued) {
|
|
1230
|
+
logTrace(tid, "QUEUED", { sessionId, mode: "plan" });
|
|
1231
|
+
if (platform.kind === "wechat") {
|
|
1232
|
+
await platform.sendText(chatId, "当前会话正在生成中,你的消息已进入缓存队列,生成完成后会立即处理。发送 /cancel 可取消缓存。").catch(() => {});
|
|
1233
|
+
} else {
|
|
1234
|
+
await platform.sendRawCard(chatId, buildQueuedCard(planText)).catch(() => {});
|
|
1235
|
+
}
|
|
1236
|
+
} else {
|
|
1237
|
+
logTrace(tid, "QUEUE_FULL", { sessionId });
|
|
1238
|
+
if (platform.kind === "wechat") {
|
|
1239
|
+
await platform.sendText(chatId, "当前缓存队列中已有消息等待处理,请等待或发送 /stop(停止生成)或 /cancel(取消缓存)。").catch(() => {});
|
|
1240
|
+
} else {
|
|
1241
|
+
await platform.sendRawCard(chatId, buildQueueFullCard()).catch(() => {});
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
return;
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
if (shouldSendWechatProcessingAck(platform, planText.toLowerCase(), chatType)) {
|
|
1248
|
+
await platform.sendText(chatId, "生成中...").catch(() => {});
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
try {
|
|
1252
|
+
logTrace(tid, "RESUME", { sessionId, tool: descriptionTool, mode: "plan" });
|
|
1253
|
+
await resumeAndPrompt(
|
|
1254
|
+
sessionId, planText, platform, chatId, msgTimestamp,
|
|
1255
|
+
descriptionTool, tid, "plan",
|
|
1256
|
+
);
|
|
1257
|
+
logTrace(tid, "DONE", { outcome: "plan_done", sessionId });
|
|
1258
|
+
} catch (err) {
|
|
1259
|
+
logTrace(tid, "DONE", { outcome: "plan_fail", error: (err as Error).message });
|
|
1260
|
+
await platform.sendCard(
|
|
1261
|
+
chatId, "Error",
|
|
1262
|
+
`Failed to run plan mode:\n${(err as Error).message}`,
|
|
1263
|
+
"red",
|
|
1264
|
+
);
|
|
1265
|
+
}
|
|
1266
|
+
return;
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
// /ask <message> — 只读/问答模式
|
|
1270
|
+
if (textLower.startsWith("/ask ")) {
|
|
1271
|
+
const askText = text.slice(5).trim();
|
|
1272
|
+
const resolved = resolveModeCommand("/ask");
|
|
1273
|
+
if (!askText) {
|
|
1274
|
+
await platform.sendText(chatId, "用法:`/ask <消息>` — 在只读/问答模式下提问。").catch(() => {});
|
|
1275
|
+
logTrace(tid, "DONE", { outcome: "ask_no_message" });
|
|
1276
|
+
return;
|
|
1277
|
+
}
|
|
1278
|
+
if (!resolved || !resolved.supportedTools.includes(descriptionTool)) {
|
|
1279
|
+
const supported = resolved?.supportedTools.map(t => toolDisplayName(t)).join("、") ?? "(无)";
|
|
1280
|
+
await platform.sendText(chatId, `/ask 仅在以下 Agent 会话中可用:${supported}。`).catch(() => {});
|
|
1281
|
+
logTrace(tid, "DONE", { outcome: "ask_wrong_tool", tool: descriptionTool });
|
|
1282
|
+
return;
|
|
1283
|
+
}
|
|
1284
|
+
logTrace(tid, "BRANCH", { cmd: "/ask", sessionId, tool: descriptionTool });
|
|
1285
|
+
|
|
1286
|
+
if (isSessionRunning(sessionId)) {
|
|
1287
|
+
const queued = enqueueMessage(sessionId, {
|
|
1288
|
+
text, chatId, openId, msgTimestamp, chatType, traceId: tid,
|
|
1289
|
+
});
|
|
1290
|
+
if (queued) {
|
|
1291
|
+
logTrace(tid, "QUEUED", { sessionId, mode: "ask" });
|
|
1292
|
+
if (platform.kind === "wechat") {
|
|
1293
|
+
await platform.sendText(chatId, "当前会话正在生成中,你的消息已进入缓存队列,生成完成后会立即处理。发送 /cancel 可取消缓存。").catch(() => {});
|
|
1294
|
+
} else {
|
|
1295
|
+
await platform.sendRawCard(chatId, buildQueuedCard(askText)).catch(() => {});
|
|
1296
|
+
}
|
|
1297
|
+
} else {
|
|
1298
|
+
logTrace(tid, "QUEUE_FULL", { sessionId });
|
|
1299
|
+
if (platform.kind === "wechat") {
|
|
1300
|
+
await platform.sendText(chatId, "当前缓存队列中已有消息等待处理,请等待或发送 /stop(停止生成)或 /cancel(取消缓存)。").catch(() => {});
|
|
1301
|
+
} else {
|
|
1302
|
+
await platform.sendRawCard(chatId, buildQueueFullCard()).catch(() => {});
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
return;
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
if (shouldSendWechatProcessingAck(platform, askText.toLowerCase(), chatType)) {
|
|
1309
|
+
await platform.sendText(chatId, "生成中...").catch(() => {});
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
try {
|
|
1313
|
+
logTrace(tid, "RESUME", { sessionId, tool: descriptionTool, mode: "ask" });
|
|
1314
|
+
await resumeAndPrompt(
|
|
1315
|
+
sessionId, askText, platform, chatId, msgTimestamp,
|
|
1316
|
+
descriptionTool, tid, "ask",
|
|
1317
|
+
);
|
|
1318
|
+
logTrace(tid, "DONE", { outcome: "ask_done", sessionId });
|
|
1319
|
+
} catch (err) {
|
|
1320
|
+
logTrace(tid, "DONE", { outcome: "ask_fail", error: (err as Error).message });
|
|
1321
|
+
await platform.sendCard(
|
|
1322
|
+
chatId, "Error",
|
|
1323
|
+
`Failed to run ask mode:\n${(err as Error).message}`,
|
|
1324
|
+
"red",
|
|
1325
|
+
);
|
|
1326
|
+
}
|
|
1327
|
+
return;
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1184
1330
|
const lastTs = lastMsgTimestamps.get(chatId);
|
|
1185
1331
|
if (lastTs !== undefined && msgTimestamp <= lastTs) {
|
|
1186
1332
|
logTrace(tid, "DONE", {
|
|
@@ -1419,8 +1565,7 @@ export async function handleCommand(
|
|
|
1419
1565
|
`**Session ID:** ${sessionId}\n` +
|
|
1420
1566
|
`**工作目录:** \`${cwd}\`\n\n` +
|
|
1421
1567
|
`下面会自动把你的私聊消息作为第一句话发送给 ${toolLabel}。\n\n` +
|
|
1422
|
-
|
|
1423
|
-
`发送 **/new** 创建新会话,**/newh** 重置当前会话(沿用工作目录)。`,
|
|
1568
|
+
buildWelcomeCommands(tool),
|
|
1424
1569
|
"green",
|
|
1425
1570
|
);
|
|
1426
1571
|
platform.setChatAvatar(newChatId, tool, "new").catch(() => {});
|
|
@@ -151,6 +151,8 @@ export interface DisplayCardState {
|
|
|
151
151
|
lastSentFinalReply?: string;
|
|
152
152
|
/** 点点点动画计数器(统一 display loop 每个卡片独立计数) */
|
|
153
153
|
dotCount: number;
|
|
154
|
+
/** stream-state 连续读取失败次数,超过阈值才删除 display entry */
|
|
155
|
+
readFailCount?: number;
|
|
154
156
|
}
|
|
155
157
|
|
|
156
158
|
export const displayCards = new Map<string, DisplayCardState>();
|
package/src/session.ts
CHANGED
|
@@ -832,8 +832,9 @@ export async function resumeAndPrompt(
|
|
|
832
832
|
msgTimestamp: number,
|
|
833
833
|
tool: string,
|
|
834
834
|
traceId?: string,
|
|
835
|
+
permissionMode?: "plan" | "ask",
|
|
835
836
|
): Promise<void> {
|
|
836
|
-
return runAgentSession(sessionId, userText, platform, chatId, msgTimestamp, tool, traceId);
|
|
837
|
+
return runAgentSession(sessionId, userText, platform, chatId, msgTimestamp, tool, traceId, permissionMode);
|
|
837
838
|
}
|
|
838
839
|
|
|
839
840
|
// ---------------------------------------------------------------------------
|
|
@@ -848,6 +849,7 @@ export async function runAgentSession(
|
|
|
848
849
|
msgTimestamp: number,
|
|
849
850
|
tool: string,
|
|
850
851
|
traceId?: string,
|
|
852
|
+
permissionMode?: "plan" | "ask",
|
|
851
853
|
): Promise<void> {
|
|
852
854
|
const tid = traceId ?? "";
|
|
853
855
|
|
|
@@ -1106,6 +1108,7 @@ export async function runAgentSession(
|
|
|
1106
1108
|
clearPromptProcessMonitor(sessionId);
|
|
1107
1109
|
if (exitInfo.pid !== undefined) unregisterProcess(exitInfo.pid);
|
|
1108
1110
|
},
|
|
1111
|
+
permissionMode,
|
|
1109
1112
|
})) {
|
|
1110
1113
|
for (const block of unifiedMsg.blocks) {
|
|
1111
1114
|
accumulateBlockContent(block, state, toolCallMap);
|
|
@@ -1279,15 +1282,21 @@ export function startUnifiedDisplayLoop(): void {
|
|
|
1279
1282
|
const sessionId = display.sessionId;
|
|
1280
1283
|
const state = await readStreamState(sessionId);
|
|
1281
1284
|
if (!state) {
|
|
1282
|
-
|
|
1285
|
+
display.readFailCount = (display.readFailCount ?? 0) + 1;
|
|
1286
|
+
if (display.readFailCount >= 3) {
|
|
1287
|
+
console.log(`[${ts()}] [DISPLAY] readStreamState ${display.readFailCount} consecutive failures for ${sessionId}, removing display entry for chat ${chatId}`);
|
|
1288
|
+
displayCards.delete(chatId);
|
|
1289
|
+
}
|
|
1283
1290
|
continue;
|
|
1284
1291
|
}
|
|
1292
|
+
display.readFailCount = 0;
|
|
1285
1293
|
|
|
1286
1294
|
// 交叉验证:chat 当前绑定的 session 是否仍是 display 记录的 session。
|
|
1287
1295
|
// 若 chat 已被切换到其他 session(如 /newh),旧 display 必须停推。
|
|
1288
1296
|
const currentSessionForChat = sessionInfoMap.get(chatId)?.sessionId;
|
|
1289
1297
|
if (currentSessionForChat && currentSessionForChat !== sessionId) {
|
|
1290
1298
|
if (state.status !== "running") {
|
|
1299
|
+
console.log(`[${ts()}] [DISPLAY] chat ${chatId} now bound to ${currentSessionForChat}, removing stale display for ${sessionId} (terminal: ${state.status})`);
|
|
1291
1300
|
displayCards.delete(chatId);
|
|
1292
1301
|
}
|
|
1293
1302
|
continue;
|
|
@@ -1297,6 +1306,7 @@ export function startUnifiedDisplayLoop(): void {
|
|
|
1297
1306
|
const lastActive = getLastActiveChat(sessionId);
|
|
1298
1307
|
if (lastActive !== chatId) {
|
|
1299
1308
|
if (state.status !== "running") {
|
|
1309
|
+
console.log(`[${ts()}] [DISPLAY] lastActive mismatch for ${sessionId}: display chat=${chatId} lastActive=${lastActive ?? "undefined"}, removing display entry (terminal: ${state.status})`);
|
|
1300
1310
|
displayCards.delete(chatId);
|
|
1301
1311
|
}
|
|
1302
1312
|
continue;
|