chatccc 0.2.154 → 0.2.155
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__/session.test.ts +77 -10
- package/src/agent-stop-stuck.ts +1 -0
- package/src/orchestrator.ts +36 -12
- package/src/session.ts +37 -15
- package/src/stream-state.ts +3 -0
package/package.json
CHANGED
|
@@ -398,10 +398,10 @@ describe("runAgentSession process monitor", () => {
|
|
|
398
398
|
});
|
|
399
399
|
});
|
|
400
400
|
|
|
401
|
-
describe("unified display loop WeChat delta", () => {
|
|
402
|
-
beforeEach(() => {
|
|
403
|
-
vi.useFakeTimers();
|
|
404
|
-
resetState();
|
|
401
|
+
describe("unified display loop WeChat delta", () => {
|
|
402
|
+
beforeEach(() => {
|
|
403
|
+
vi.useFakeTimers();
|
|
404
|
+
resetState();
|
|
405
405
|
resetBindingState();
|
|
406
406
|
mockStreamStates.clear();
|
|
407
407
|
});
|
|
@@ -464,12 +464,79 @@ describe("unified display loop WeChat delta", () => {
|
|
|
464
464
|
2,
|
|
465
465
|
"chat-wechat",
|
|
466
466
|
"tool output",
|
|
467
|
-
);
|
|
468
|
-
});
|
|
469
|
-
});
|
|
470
|
-
|
|
471
|
-
describe("
|
|
472
|
-
|
|
467
|
+
);
|
|
468
|
+
});
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
describe("unified display loop terminal card update", () => {
|
|
472
|
+
beforeEach(() => {
|
|
473
|
+
vi.useFakeTimers();
|
|
474
|
+
resetState();
|
|
475
|
+
resetBindingState();
|
|
476
|
+
mockStreamStates.clear();
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
afterEach(() => {
|
|
480
|
+
stopUnifiedDisplayLoop();
|
|
481
|
+
resetBindingState();
|
|
482
|
+
vi.useRealTimers();
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
it("does not repeat the same terminal CardKit sequence while the prompt is still active", async () => {
|
|
486
|
+
const platform = mockPlatform("feishu");
|
|
487
|
+
platform.cardUpdate = vi.fn(async () => {
|
|
488
|
+
throw new Error("CardKit update: [300317] ErrMsg: sequence number compare failed; ");
|
|
489
|
+
});
|
|
490
|
+
setSessionPlatform(platform);
|
|
491
|
+
|
|
492
|
+
bindChatToSession("sid-terminal", "chat-terminal");
|
|
493
|
+
recordLastActiveChat("sid-terminal", "chat-terminal");
|
|
494
|
+
sessionInfoMap.set("chat-terminal", {
|
|
495
|
+
sessionId: "sid-terminal",
|
|
496
|
+
turnCount: 1,
|
|
497
|
+
lastContextTokens: 0,
|
|
498
|
+
startTime: 0,
|
|
499
|
+
tool: "claude",
|
|
500
|
+
});
|
|
501
|
+
activePrompts.set("sid-terminal", {
|
|
502
|
+
controller: new AbortController(),
|
|
503
|
+
stopped: false,
|
|
504
|
+
startTime: Date.now(),
|
|
505
|
+
});
|
|
506
|
+
displayCards.set("chat-terminal", {
|
|
507
|
+
cardId: "card-terminal",
|
|
508
|
+
sequence: 109,
|
|
509
|
+
cardBusy: false,
|
|
510
|
+
cardCreatedAt: Date.now(),
|
|
511
|
+
lastSentContent: "",
|
|
512
|
+
streamErrorNotified: false,
|
|
513
|
+
sessionId: "sid-terminal",
|
|
514
|
+
turnCount: 1,
|
|
515
|
+
dotCount: 0,
|
|
516
|
+
});
|
|
517
|
+
mockStreamStates.set("sid-terminal", {
|
|
518
|
+
accumulatedContent: "partial tool output",
|
|
519
|
+
finalReply: "",
|
|
520
|
+
status: "stopped",
|
|
521
|
+
turnCount: 1,
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
startUnifiedDisplayLoop();
|
|
525
|
+
await vi.advanceTimersByTimeAsync(3000);
|
|
526
|
+
await vi.advanceTimersByTimeAsync(3000);
|
|
527
|
+
|
|
528
|
+
expect(platform.cardUpdate).toHaveBeenCalledTimes(1);
|
|
529
|
+
expect(platform.cardUpdate).toHaveBeenCalledWith(
|
|
530
|
+
"card-terminal",
|
|
531
|
+
expect.any(String),
|
|
532
|
+
110,
|
|
533
|
+
);
|
|
534
|
+
expect(displayCards.get("chat-terminal")?.sequence).toBe(110);
|
|
535
|
+
});
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
describe("rebuildBindingsFromRegistry", () => {
|
|
539
|
+
let registryFile = "";
|
|
473
540
|
|
|
474
541
|
beforeEach(async () => {
|
|
475
542
|
chatSessionMap.clear();
|
package/src/agent-stop-stuck.ts
CHANGED
package/src/orchestrator.ts
CHANGED
|
@@ -78,6 +78,7 @@ import {
|
|
|
78
78
|
cancelQueuedMessage,
|
|
79
79
|
} from "./session-chat-binding.ts";
|
|
80
80
|
import { getTenantAccessToken, sendPostMessage } from "./feishu-platform.ts";
|
|
81
|
+
import { readStreamState } from "./stream-state.ts";
|
|
81
82
|
export { type PlatformAdapter } from "./platform-adapter.ts";
|
|
82
83
|
import type { PlatformAdapter } from "./platform-adapter.ts";
|
|
83
84
|
|
|
@@ -593,10 +594,12 @@ export async function handleCommand(
|
|
|
593
594
|
|
|
594
595
|
if (sessionId && descriptionTool && toolLabel) {
|
|
595
596
|
// 有会话上下文 — 路由到命令处理或 prompt
|
|
596
|
-
logTrace(tid, "BRANCH", { sessionId, tool: descriptionTool });
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
597
|
+
logTrace(tid, "BRANCH", { sessionId, tool: descriptionTool });
|
|
598
|
+
const routeKind = textLower.startsWith("/") ? "command" : "prompt";
|
|
599
|
+
const chatKind = chatType === "p2p" ? "p2p chat" : "session group";
|
|
600
|
+
console.log(
|
|
601
|
+
`[${ts()}] [ROUTE] ${toolLabel} ${chatKind} ${routeKind} detected, session=${sessionId} tool=${descriptionTool}`,
|
|
602
|
+
);
|
|
600
603
|
|
|
601
604
|
if (
|
|
602
605
|
chatType !== "p2p" &&
|
|
@@ -1228,6 +1231,27 @@ export async function handleCommand(
|
|
|
1228
1231
|
await platform.sendText(chatId, "生成中...").catch(() => {});
|
|
1229
1232
|
}
|
|
1230
1233
|
|
|
1234
|
+
// 检查 session 是否被 stop-stuck-loop 标记为卡住,是则创建新 session
|
|
1235
|
+
const stuckState = await readStreamState(sessionId);
|
|
1236
|
+
if (stuckState?.stuckAt) {
|
|
1237
|
+
try {
|
|
1238
|
+
const init = await initClaudeSession(descriptionTool, undefined, chatId);
|
|
1239
|
+
const newSessionId = init.sessionId;
|
|
1240
|
+
unbindChatFromSession(sessionId, chatId);
|
|
1241
|
+
bindChatToSession(newSessionId, chatId);
|
|
1242
|
+
await recordSessionRegistry({
|
|
1243
|
+
chatId,
|
|
1244
|
+
sessionId: newSessionId,
|
|
1245
|
+
tool: descriptionTool,
|
|
1246
|
+
chatName: sessionChatName(text.slice(0, 10), init.cwd),
|
|
1247
|
+
}).catch(() => {});
|
|
1248
|
+
console.log(`[${ts()}] [STUCK-REDIRECT] Session ${sessionId} was stuck, created new session ${newSessionId}`);
|
|
1249
|
+
sessionId = newSessionId;
|
|
1250
|
+
} catch (err) {
|
|
1251
|
+
console.warn(`[${ts()}] [STUCK-REDIRECT] Failed to create new session: ${(err as Error).message}`);
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1231
1255
|
try {
|
|
1232
1256
|
logTrace(tid, "RESUME", { sessionId, tool: descriptionTool });
|
|
1233
1257
|
await resumeAndPrompt(
|
|
@@ -1235,10 +1259,10 @@ export async function handleCommand(
|
|
|
1235
1259
|
text,
|
|
1236
1260
|
platform,
|
|
1237
1261
|
chatId,
|
|
1238
|
-
msgTimestamp,
|
|
1239
|
-
descriptionTool,
|
|
1240
|
-
tid,
|
|
1241
|
-
);
|
|
1262
|
+
msgTimestamp,
|
|
1263
|
+
descriptionTool,
|
|
1264
|
+
tid,
|
|
1265
|
+
);
|
|
1242
1266
|
logTrace(tid, "DONE", { outcome: "resume_done", sessionId });
|
|
1243
1267
|
console.log(`[${ts()}] [RESUME] Session ${sessionId} done`);
|
|
1244
1268
|
} catch (err) {
|
|
@@ -1434,10 +1458,10 @@ export async function handleCommand(
|
|
|
1434
1458
|
text,
|
|
1435
1459
|
platform,
|
|
1436
1460
|
newChatId,
|
|
1437
|
-
msgTimestamp,
|
|
1438
|
-
tool,
|
|
1439
|
-
tid,
|
|
1440
|
-
);
|
|
1461
|
+
msgTimestamp,
|
|
1462
|
+
tool,
|
|
1463
|
+
tid,
|
|
1464
|
+
);
|
|
1441
1465
|
console.log(`[${ts()}] [AUTO-P2P 5/5] First prompt sent → OK`);
|
|
1442
1466
|
logTrace(tid, "DONE", { outcome: "auto_new_p2p_prompt_done", newChatId, sessionId, tool });
|
|
1443
1467
|
} catch (err) {
|
package/src/session.ts
CHANGED
|
@@ -167,11 +167,15 @@ function formatTerminalHeader(status: "running" | "done" | "stopped" | "error"):
|
|
|
167
167
|
return { title: "完成" };
|
|
168
168
|
}
|
|
169
169
|
|
|
170
|
-
function turnFinalStatus(status: "running" | "done" | "stopped" | "error"): "done" | "stopped" {
|
|
171
|
-
return status === "stopped" || status === "error" ? "stopped" : "done";
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
function
|
|
170
|
+
function turnFinalStatus(status: "running" | "done" | "stopped" | "error"): "done" | "stopped" {
|
|
171
|
+
return status === "stopped" || status === "error" ? "stopped" : "done";
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function isCardKitSequenceConflict(err: unknown): boolean {
|
|
175
|
+
return err instanceof Error && err.message.includes("300317");
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function startPromptProcessMonitor(sessionId: string, info: ToolProcessInfo): void {
|
|
175
179
|
const prompt = activePrompts.get(sessionId);
|
|
176
180
|
if (!prompt) return;
|
|
177
181
|
prompt.processPid = info.pid;
|
|
@@ -1356,24 +1360,42 @@ export function startUnifiedDisplayLoop(): void {
|
|
|
1356
1360
|
displayCards.delete(chatId);
|
|
1357
1361
|
} else {
|
|
1358
1362
|
// 发送最终结果(卡片平台)
|
|
1359
|
-
while (display.cardBusy) await new Promise(r => setTimeout(r, 20));
|
|
1360
|
-
const
|
|
1363
|
+
while (display.cardBusy) await new Promise(r => setTimeout(r, 20));
|
|
1364
|
+
const promptStillActive = activePrompts.has(sessionId);
|
|
1365
|
+
if (
|
|
1366
|
+
promptStillActive &&
|
|
1367
|
+
display.lastSentAccLen === state.accumulatedContent.length &&
|
|
1368
|
+
display.lastSentFinalReply === state.finalReply
|
|
1369
|
+
) {
|
|
1370
|
+
continue;
|
|
1371
|
+
}
|
|
1372
|
+
const nextSeq = display.sequence + 1;
|
|
1361
1373
|
const { title: headerTitle, template: headerTemplate } = formatTerminalHeader(state.status);
|
|
1362
1374
|
const cardContent = truncateContent(state.accumulatedContent + state.finalReply) || " ";
|
|
1363
1375
|
const doneCard = buildProgressCard(cardContent, { showStop: false, headerTitle, headerTemplate });
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1376
|
+
let terminalCardUpdateAccepted = false;
|
|
1377
|
+
await p.cardUpdate(display.cardId, doneCard, nextSeq).then(() => {
|
|
1378
|
+
display.sequence = nextSeq;
|
|
1379
|
+
terminalCardUpdateAccepted = true;
|
|
1380
|
+
}).catch(err => {
|
|
1381
|
+
console.error(`[${ts()}] [DISPLAY] terminal cardUpdate failed: ${(err as Error).message}`);
|
|
1382
|
+
if (isCardKitSequenceConflict(err)) {
|
|
1383
|
+
display.sequence = nextSeq;
|
|
1384
|
+
terminalCardUpdateAccepted = true;
|
|
1385
|
+
}
|
|
1386
|
+
});
|
|
1367
1387
|
|
|
1368
1388
|
// 若 session 仍在 activePrompts 中,说明 runAgentSession 的 finally
|
|
1369
1389
|
// 还没执行,当前 stream state 可能是 stopSession fire-and-forget
|
|
1370
1390
|
// 写入的,finalReply 滞后于内存态。卡片已更新为终态外观,但不发送
|
|
1371
1391
|
// 文本、不删除 display 条目,留给 finally 落盘后的下一次 tick 处理。
|
|
1372
|
-
if (
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1392
|
+
if (promptStillActive) {
|
|
1393
|
+
if (terminalCardUpdateAccepted) {
|
|
1394
|
+
display.lastSentAccLen = state.accumulatedContent.length;
|
|
1395
|
+
display.lastSentFinalReply = state.finalReply;
|
|
1396
|
+
}
|
|
1397
|
+
continue;
|
|
1398
|
+
}
|
|
1377
1399
|
|
|
1378
1400
|
const finalSt = turnFinalStatus(state.status);
|
|
1379
1401
|
finalizeTurnCards(sessionId, state.turnCount, finalSt).catch(() => {});
|
package/src/stream-state.ts
CHANGED
|
@@ -26,6 +26,9 @@ export interface StreamState {
|
|
|
26
26
|
updatedAt: number;
|
|
27
27
|
cwd: string;
|
|
28
28
|
tool: string;
|
|
29
|
+
/** Set by stop-stuck-loop to prevent the session from being resumed.
|
|
30
|
+
* The orchestrator checks this before resuming and creates a new session instead. */
|
|
31
|
+
stuckAt?: number;
|
|
29
32
|
}
|
|
30
33
|
|
|
31
34
|
function getStreamStatePath(sessionId: string): string {
|