chatccc 0.2.242 → 0.2.243

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.
Files changed (56) hide show
  1. package/README.md +5 -5
  2. package/bin/cccagent.mjs +17 -17
  3. package/deepccc-agent/bin/deepccc.mjs +26 -26
  4. package/deepccc-agent/package.json +62 -62
  5. package/deepccc-agent/src/__tests__/chat-session.test.ts +578 -522
  6. package/deepccc-agent/src/__tests__/cli-json.test.ts +49 -49
  7. package/deepccc-agent/src/__tests__/config.test.ts +26 -26
  8. package/deepccc-agent/src/__tests__/context.test.ts +319 -319
  9. package/deepccc-agent/src/__tests__/file-tools.test.ts +240 -240
  10. package/deepccc-agent/src/__tests__/permissions.test.ts +195 -195
  11. package/deepccc-agent/src/__tests__/progress-reducer.test.ts +121 -121
  12. package/deepccc-agent/src/__tests__/session-search.test.ts +262 -262
  13. package/deepccc-agent/src/__tests__/session-select.test.ts +116 -116
  14. package/deepccc-agent/src/__tests__/sigint.test.ts +56 -56
  15. package/deepccc-agent/src/__tests__/skills.test.ts +284 -284
  16. package/deepccc-agent/src/__tests__/terminal-renderer.test.ts +247 -247
  17. package/deepccc-agent/src/__tests__/web-tools.test.ts +220 -220
  18. package/deepccc-agent/src/config.ts +84 -84
  19. package/deepccc-agent/src/context.ts +465 -465
  20. package/deepccc-agent/src/file-log.ts +38 -38
  21. package/deepccc-agent/src/index.ts +22 -0
  22. package/deepccc-agent/src/proc-tree-kill.ts +61 -61
  23. package/deepccc-agent/src/progress/cards-helpers.ts +76 -76
  24. package/deepccc-agent/src/progress/reducer.ts +113 -113
  25. package/deepccc-agent/src/progress/terminal-renderer.ts +294 -294
  26. package/deepccc-agent/src/progress/view.ts +77 -77
  27. package/deepccc-agent/src/raw-stream-log.ts +124 -124
  28. package/deepccc-agent/src/session-search.ts +370 -370
  29. package/deepccc-agent/src/session-select.ts +48 -48
  30. package/deepccc-agent/src/sigint.ts +50 -50
  31. package/deepccc-agent/src/skills.ts +205 -205
  32. package/deepccc-agent/src/web-tools.ts +313 -313
  33. package/deepccc-agent/tsconfig.build.json +13 -13
  34. package/deepccc-agent/tsconfig.json +13 -13
  35. package/deepccc-agent/vitest.config.ts +7 -7
  36. package/package.json +1 -1
  37. package/src/__tests__/builtin-chat-session.test.ts +522 -522
  38. package/src/__tests__/builtin-config.test.ts +26 -26
  39. package/src/__tests__/builtin-context.test.ts +319 -319
  40. package/src/__tests__/builtin-file-tools.test.ts +240 -240
  41. package/src/__tests__/builtin-permissions.test.ts +211 -211
  42. package/src/__tests__/builtin-session-search.test.ts +262 -262
  43. package/src/__tests__/builtin-session-select.test.ts +116 -116
  44. package/src/__tests__/builtin-sigint.test.ts +56 -56
  45. package/src/__tests__/builtin-skills.test.ts +284 -284
  46. package/src/__tests__/builtin-web-tools.test.ts +220 -220
  47. package/src/__tests__/config.test.ts +17 -17
  48. package/src/__tests__/progress-reducer.test.ts +121 -121
  49. package/src/__tests__/session-ccc-config.test.ts +45 -45
  50. package/src/__tests__/session.test.ts +298 -298
  51. package/src/adapters/ccc-adapter.ts +145 -145
  52. package/src/config-utils.ts +13 -13
  53. package/src/config.ts +13 -13
  54. package/src/progress/reducer.ts +113 -113
  55. package/src/session-chat-binding.ts +83 -83
  56. package/src/session.ts +311 -311
@@ -9,7 +9,7 @@ vi.mock("../adapters/proc-tree-kill.ts", () => ({
9
9
  }));
10
10
 
11
11
  // mock stream-state 以支持在测试中控制累积长度
12
- const mockStreamStates = new Map<string, {
12
+ const mockStreamStates = new Map<string, {
13
13
  accumulatedContent: string;
14
14
  finalReply: string;
15
15
  activity?: {
@@ -22,14 +22,14 @@ const mockStreamStates = new Map<string, {
22
22
  autoEndedAt?: number;
23
23
  turnCount?: number;
24
24
  finalReplySentTurn?: number;
25
- finalReplySentAt?: number;
26
- terminalError?: {
27
- kind: "network_timeout" | "network" | "authentication" | "rate_limit" | "provider" | "process" | "resource" | "unknown";
28
- title: string;
29
- message: string;
30
- occurredAt: number;
31
- };
32
- }>();
25
+ finalReplySentAt?: number;
26
+ terminalError?: {
27
+ kind: "network_timeout" | "network" | "authentication" | "rate_limit" | "provider" | "process" | "resource" | "unknown";
28
+ title: string;
29
+ message: string;
30
+ occurredAt: number;
31
+ };
32
+ }>();
33
33
  vi.mock("../stream-state.ts", () => ({
34
34
  readStreamState: async (sid: string) => {
35
35
  const state = mockStreamStates.get(sid);
@@ -40,8 +40,8 @@ vi.mock("../stream-state.ts", () => ({
40
40
  finalReply: state.finalReply,
41
41
  activity: state.activity,
42
42
  finalReplySentTurn: state.finalReplySentTurn,
43
- finalReplySentAt: state.finalReplySentAt,
44
- terminalError: state.terminalError,
43
+ finalReplySentAt: state.finalReplySentAt,
44
+ terminalError: state.terminalError,
45
45
  autoEndedAt: state.autoEndedAt,
46
46
  status: state.status ?? "running",
47
47
  chunkCount: 0,
@@ -66,14 +66,14 @@ vi.mock("../stream-state.ts", () => ({
66
66
  autoEndedAt?: number;
67
67
  turnCount?: number;
68
68
  finalReplySentTurn?: number;
69
- finalReplySentAt?: number;
70
- terminalError?: {
71
- kind: "network_timeout" | "network" | "authentication" | "rate_limit" | "provider" | "process" | "resource" | "unknown";
72
- title: string;
73
- message: string;
74
- occurredAt: number;
75
- };
76
- }) => {
69
+ finalReplySentAt?: number;
70
+ terminalError?: {
71
+ kind: "network_timeout" | "network" | "authentication" | "rate_limit" | "provider" | "process" | "resource" | "unknown";
72
+ title: string;
73
+ message: string;
74
+ occurredAt: number;
75
+ };
76
+ }) => {
77
77
  mockStreamStates.set(state.sessionId, {
78
78
  accumulatedContent: state.accumulatedContent,
79
79
  finalReply: state.finalReply,
@@ -82,8 +82,8 @@ vi.mock("../stream-state.ts", () => ({
82
82
  autoEndedAt: state.autoEndedAt,
83
83
  turnCount: state.turnCount,
84
84
  finalReplySentTurn: state.finalReplySentTurn,
85
- finalReplySentAt: state.finalReplySentAt,
86
- terminalError: state.terminalError,
85
+ finalReplySentAt: state.finalReplySentAt,
86
+ terminalError: state.terminalError,
87
87
  });
88
88
  },
89
89
  createEmptyStreamState: (sid: string, cwd: string, tool: string, turnCount: number) => ({
@@ -131,11 +131,11 @@ import {
131
131
  stopUnifiedDisplayLoop,
132
132
  _setProcessAliveForTest,
133
133
  _resetProcessAliveForTest,
134
- _setProcessMonitorIntervalForTest,
135
- _resetProcessMonitorIntervalForTest,
136
- _setAvatarRefreshIntervalForTest,
137
- _resetAvatarRefreshIntervalForTest,
138
- _setResponseStallTimeoutForTest,
134
+ _setProcessMonitorIntervalForTest,
135
+ _resetProcessMonitorIntervalForTest,
136
+ _setAvatarRefreshIntervalForTest,
137
+ _resetAvatarRefreshIntervalForTest,
138
+ _setResponseStallTimeoutForTest,
139
139
  _resetResponseStallTimeoutForTest,
140
140
  _setResponseStallCheckIntervalForTest,
141
141
  _resetResponseStallCheckIntervalForTest,
@@ -438,7 +438,7 @@ describe("runAgentSession previous final delivery guard", () => {
438
438
  });
439
439
  });
440
440
 
441
- describe("runAgentSession process monitor", () => {
441
+ describe("runAgentSession process monitor", () => {
442
442
  let registryFile = "";
443
443
  let toolsFile = "";
444
444
 
@@ -468,7 +468,7 @@ describe("runAgentSession process monitor", () => {
468
468
  vi.useRealTimers();
469
469
  });
470
470
 
471
- it("marks the turn as error and sends a separate notice when the CLI process disappears", async () => {
471
+ it("marks the turn as error and sends a separate notice when the CLI process disappears", async () => {
472
472
  const platform = mockPlatform("feishu");
473
473
  setSessionPlatform(platform);
474
474
  bindChatToSession("sid-process", "chat-process");
@@ -524,52 +524,52 @@ describe("runAgentSession process monitor", () => {
524
524
  "chat-process",
525
525
  expect.stringContaining("进程异常结束"),
526
526
  );
527
- });
528
-
529
- it("persists and sends the root cause when the agent stream fails before producing output", async () => {
530
- vi.spyOn(console, "error").mockImplementation(() => {});
531
- const platform = mockPlatform("feishu");
532
- platform.cardCreate = vi.fn(async () => {
533
- throw new Error("CardKit unavailable");
534
- });
535
- setSessionPlatform(platform);
536
- bindChatToSession("sid-stream-error", "chat-stream-error");
537
- recordLastActiveChat("sid-stream-error", "chat-stream-error");
538
-
539
- const adapter: ToolAdapter = {
540
- displayName: "CCC Agent",
541
- sessionDescPrefix: "CCC Session:",
542
- createSession: async () => ({ sessionId: "sid-stream-error" }),
543
- getSessionInfo: async (sid) => ({ sessionId: sid, cwd: "/tmp" }),
544
- closeSession: async () => {},
545
- prompt: async function* () {
546
- throw new Error(
547
- "Failed after 3 attempts. Last error: Cannot connect to API: " +
548
- "Connect Timeout Error (timeout: 10000ms)",
549
- );
550
- },
551
- };
552
- _setAdapterForToolForTest("ccc", adapter);
553
-
554
- const outcome = await runAgentSession(
555
- "sid-stream-error",
556
- "prompt",
557
- platform,
558
- "chat-stream-error",
559
- Date.now(),
560
- "ccc",
561
- );
562
-
563
- const state = mockStreamStates.get("sid-stream-error");
564
- expect(outcome).toBe("error");
565
- expect(state?.status).toBe("error");
566
- expect(state?.terminalError?.kind).toBe("network_timeout");
567
- expect(state?.terminalError?.message).toContain("已重试 3 次");
568
- expect(platform.sendText).toHaveBeenCalledWith(
569
- "chat-stream-error",
570
- expect.stringContaining("网络连接超时"),
571
- );
572
- });
527
+ });
528
+
529
+ it("persists and sends the root cause when the agent stream fails before producing output", async () => {
530
+ vi.spyOn(console, "error").mockImplementation(() => {});
531
+ const platform = mockPlatform("feishu");
532
+ platform.cardCreate = vi.fn(async () => {
533
+ throw new Error("CardKit unavailable");
534
+ });
535
+ setSessionPlatform(platform);
536
+ bindChatToSession("sid-stream-error", "chat-stream-error");
537
+ recordLastActiveChat("sid-stream-error", "chat-stream-error");
538
+
539
+ const adapter: ToolAdapter = {
540
+ displayName: "CCC Agent",
541
+ sessionDescPrefix: "CCC Session:",
542
+ createSession: async () => ({ sessionId: "sid-stream-error" }),
543
+ getSessionInfo: async (sid) => ({ sessionId: sid, cwd: "/tmp" }),
544
+ closeSession: async () => {},
545
+ prompt: async function* () {
546
+ throw new Error(
547
+ "Failed after 3 attempts. Last error: Cannot connect to API: " +
548
+ "Connect Timeout Error (timeout: 10000ms)",
549
+ );
550
+ },
551
+ };
552
+ _setAdapterForToolForTest("ccc", adapter);
553
+
554
+ const outcome = await runAgentSession(
555
+ "sid-stream-error",
556
+ "prompt",
557
+ platform,
558
+ "chat-stream-error",
559
+ Date.now(),
560
+ "ccc",
561
+ );
562
+
563
+ const state = mockStreamStates.get("sid-stream-error");
564
+ expect(outcome).toBe("error");
565
+ expect(state?.status).toBe("error");
566
+ expect(state?.terminalError?.kind).toBe("network_timeout");
567
+ expect(state?.terminalError?.message).toContain("已重试 3 次");
568
+ expect(platform.sendText).toHaveBeenCalledWith(
569
+ "chat-stream-error",
570
+ expect.stringContaining("网络连接超时"),
571
+ );
572
+ });
573
573
 
574
574
  it("recreates the progress card when the initial CardKit send fails", async () => {
575
575
  vi.spyOn(console, "error").mockImplementation(() => {});
@@ -806,99 +806,99 @@ describe("runAgentSession process monitor", () => {
806
806
  expect(sentTexts[0]).toContain("first prompt");
807
807
  expect(sentTexts[1]).toContain("second prompt");
808
808
  });
809
- });
810
-
811
- describe("runAgentSession periodic avatar refresh", () => {
812
- let registryFile = "";
813
- let toolsFile = "";
814
-
815
- beforeEach(async () => {
816
- vi.useFakeTimers();
817
- resetState();
818
- resetBindingState();
819
- mockStreamStates.clear();
820
- const dir = await mkdtemp(join(tmpdir(), "chatccc-avatar-refresh-"));
821
- registryFile = join(dir, "session-registry.json");
822
- toolsFile = join(dir, "session-tools.json");
823
- _setSessionRegistryFileForTest(registryFile);
824
- _setSessionToolsFileForTest(toolsFile);
825
- _setAvatarRefreshIntervalForTest(50);
826
- });
827
-
828
- afterEach(async () => {
829
- _resetSessionRegistryFileForTest();
830
- _resetSessionToolsFileForTest();
831
- _resetAvatarRefreshIntervalForTest();
832
- _clearAdapterCacheForTest();
833
- resetState();
834
- resetBindingState();
835
- vi.useRealTimers();
836
- if (registryFile) await rm(dirname(registryFile), { recursive: true, force: true });
837
- });
838
-
839
- it.each(["codex", "cursor", "claude", "ccc"])(
840
- "refreshes the busy avatar for %s every interval and stops after completion",
841
- async (tool) => {
842
- const sessionId = `sid-avatar-${tool}`;
843
- const chatId = `chat-avatar-${tool}`;
844
- const platform = mockPlatform("feishu");
845
- setSessionPlatform(platform);
846
- bindChatToSession(sessionId, chatId);
847
- recordLastActiveChat(sessionId, chatId);
848
-
849
- let finishPrompt: (() => void) | undefined;
850
- const adapter: ToolAdapter = {
851
- displayName: tool,
852
- sessionDescPrefix: `${tool} Session:`,
853
- createSession: async () => ({ sessionId }),
854
- getSessionInfo: async (sid) => ({ sessionId: sid, cwd: "/tmp" }),
855
- closeSession: async () => {},
856
- prompt: async function* () {
857
- yield { type: "assistant", blocks: [{ type: "text", text: "working" }] };
858
- await new Promise<void>((resolve) => {
859
- finishPrompt = resolve;
860
- });
861
- yield { type: "assistant", blocks: [{ type: "text", text: "done" }] };
862
- },
863
- };
864
- _setAdapterForToolForTest(tool, adapter);
865
-
866
- const runPromise = runAgentSession(
867
- sessionId,
868
- "prompt",
869
- platform,
870
- chatId,
871
- Date.now(),
872
- tool,
873
- );
874
-
875
- await vi.waitFor(() => {
876
- expect(vi.mocked(platform.setChatAvatar).mock.calls.some(
877
- ([calledChatId, calledTool, status]) =>
878
- calledChatId === chatId && calledTool === tool && status === "busy",
879
- )).toBe(true);
880
- expect(finishPrompt).toBeTypeOf("function");
881
- });
882
- const initialBusyCalls = vi.mocked(platform.setChatAvatar).mock.calls.filter(
883
- ([calledChatId, calledTool, status]) =>
884
- calledChatId === chatId && calledTool === tool && status === "busy",
885
- ).length;
886
-
887
- await vi.advanceTimersByTimeAsync(51);
888
- expect(vi.mocked(platform.setChatAvatar).mock.calls.filter(
889
- ([calledChatId, calledTool, status]) =>
890
- calledChatId === chatId && calledTool === tool && status === "busy",
891
- )).toHaveLength(initialBusyCalls + 1);
892
-
893
- finishPrompt?.();
894
- await runPromise;
895
- const callsAfterCompletion = vi.mocked(platform.setChatAvatar).mock.calls.length;
896
-
897
- await vi.advanceTimersByTimeAsync(100);
898
- expect(platform.setChatAvatar).toHaveBeenCalledTimes(callsAfterCompletion);
899
- },
900
- );
901
- });
809
+ });
810
+
811
+ describe("runAgentSession periodic avatar refresh", () => {
812
+ let registryFile = "";
813
+ let toolsFile = "";
814
+
815
+ beforeEach(async () => {
816
+ vi.useFakeTimers();
817
+ resetState();
818
+ resetBindingState();
819
+ mockStreamStates.clear();
820
+ const dir = await mkdtemp(join(tmpdir(), "chatccc-avatar-refresh-"));
821
+ registryFile = join(dir, "session-registry.json");
822
+ toolsFile = join(dir, "session-tools.json");
823
+ _setSessionRegistryFileForTest(registryFile);
824
+ _setSessionToolsFileForTest(toolsFile);
825
+ _setAvatarRefreshIntervalForTest(50);
826
+ });
827
+
828
+ afterEach(async () => {
829
+ _resetSessionRegistryFileForTest();
830
+ _resetSessionToolsFileForTest();
831
+ _resetAvatarRefreshIntervalForTest();
832
+ _clearAdapterCacheForTest();
833
+ resetState();
834
+ resetBindingState();
835
+ vi.useRealTimers();
836
+ if (registryFile) await rm(dirname(registryFile), { recursive: true, force: true });
837
+ });
838
+
839
+ it.each(["codex", "cursor", "claude", "ccc"])(
840
+ "refreshes the busy avatar for %s every interval and stops after completion",
841
+ async (tool) => {
842
+ const sessionId = `sid-avatar-${tool}`;
843
+ const chatId = `chat-avatar-${tool}`;
844
+ const platform = mockPlatform("feishu");
845
+ setSessionPlatform(platform);
846
+ bindChatToSession(sessionId, chatId);
847
+ recordLastActiveChat(sessionId, chatId);
848
+
849
+ let finishPrompt: (() => void) | undefined;
850
+ const adapter: ToolAdapter = {
851
+ displayName: tool,
852
+ sessionDescPrefix: `${tool} Session:`,
853
+ createSession: async () => ({ sessionId }),
854
+ getSessionInfo: async (sid) => ({ sessionId: sid, cwd: "/tmp" }),
855
+ closeSession: async () => {},
856
+ prompt: async function* () {
857
+ yield { type: "assistant", blocks: [{ type: "text", text: "working" }] };
858
+ await new Promise<void>((resolve) => {
859
+ finishPrompt = resolve;
860
+ });
861
+ yield { type: "assistant", blocks: [{ type: "text", text: "done" }] };
862
+ },
863
+ };
864
+ _setAdapterForToolForTest(tool, adapter);
865
+
866
+ const runPromise = runAgentSession(
867
+ sessionId,
868
+ "prompt",
869
+ platform,
870
+ chatId,
871
+ Date.now(),
872
+ tool,
873
+ );
874
+
875
+ await vi.waitFor(() => {
876
+ expect(vi.mocked(platform.setChatAvatar).mock.calls.some(
877
+ ([calledChatId, calledTool, status]) =>
878
+ calledChatId === chatId && calledTool === tool && status === "busy",
879
+ )).toBe(true);
880
+ expect(finishPrompt).toBeTypeOf("function");
881
+ });
882
+ const initialBusyCalls = vi.mocked(platform.setChatAvatar).mock.calls.filter(
883
+ ([calledChatId, calledTool, status]) =>
884
+ calledChatId === chatId && calledTool === tool && status === "busy",
885
+ ).length;
886
+
887
+ await vi.advanceTimersByTimeAsync(51);
888
+ expect(vi.mocked(platform.setChatAvatar).mock.calls.filter(
889
+ ([calledChatId, calledTool, status]) =>
890
+ calledChatId === chatId && calledTool === tool && status === "busy",
891
+ )).toHaveLength(initialBusyCalls + 1);
892
+
893
+ finishPrompt?.();
894
+ await runPromise;
895
+ const callsAfterCompletion = vi.mocked(platform.setChatAvatar).mock.calls.length;
896
+
897
+ await vi.advanceTimersByTimeAsync(100);
898
+ expect(platform.setChatAvatar).toHaveBeenCalledTimes(callsAfterCompletion);
899
+ },
900
+ );
901
+ });
902
902
 
903
903
  describe("runAgentSession response stall watchdog", () => {
904
904
  let tempDir = "";
@@ -1001,7 +1001,7 @@ describe("runAgentSession response stall watchdog", () => {
1001
1001
  });
1002
1002
  });
1003
1003
 
1004
- it("does not apply the reply-stall timeout while an Agent is compacting context", async () => {
1004
+ it("does not apply the reply-stall timeout while an Agent is compacting context", async () => {
1005
1005
  vi.setSystemTime(0);
1006
1006
  _setResponseStallTimeoutForTest(180_000);
1007
1007
  _setResponseStallCheckIntervalForTest(1_000);
@@ -1026,10 +1026,10 @@ describe("runAgentSession response stall watchdog", () => {
1026
1026
  signal?: AbortSignal,
1027
1027
  options?: ToolPromptOptions,
1028
1028
  ) {
1029
- options?.onSessionCreated?.(closeSession);
1030
- options?.onProcessStart?.({ pid: 4343 });
1031
- yield { type: "assistant", blocks: [{ type: "agent_status", status: "compacting" }] };
1032
- await new Promise<void>((resolve) => {
1029
+ options?.onSessionCreated?.(closeSession);
1030
+ options?.onProcessStart?.({ pid: 4343 });
1031
+ yield { type: "assistant", blocks: [{ type: "agent_status", status: "compacting" }] };
1032
+ await new Promise<void>((resolve) => {
1033
1033
  if (signal?.aborted) {
1034
1034
  resolve();
1035
1035
  return;
@@ -1051,11 +1051,11 @@ describe("runAgentSession response stall watchdog", () => {
1051
1051
  );
1052
1052
 
1053
1053
  await vi.waitFor(() => {
1054
- expect(activePrompts.get("sid-starting-stall")).toMatchObject({
1055
- processPid: 4343,
1056
- });
1057
- expect(activePrompts.get("sid-starting-stall")?.responseProgress).toBeUndefined();
1058
- expect(closeSession).toHaveBeenCalledTimes(0);
1054
+ expect(activePrompts.get("sid-starting-stall")).toMatchObject({
1055
+ processPid: 4343,
1056
+ });
1057
+ expect(activePrompts.get("sid-starting-stall")?.responseProgress).toBeUndefined();
1058
+ expect(closeSession).toHaveBeenCalledTimes(0);
1059
1059
  });
1060
1060
 
1061
1061
  await vi.advanceTimersByTimeAsync(181_001);
@@ -1066,17 +1066,17 @@ describe("runAgentSession response stall watchdog", () => {
1066
1066
  .length;
1067
1067
 
1068
1068
  // 旧实现不会结束真正的零事件启动;先清理测试会话,避免失败用例悬挂。
1069
- stopSession("sid-starting-stall");
1070
- await vi.advanceTimersByTimeAsync(1_000);
1071
- await runPromise;
1072
-
1073
- expect(stateAfterDeadline).toMatchObject({
1074
- status: "running",
1075
- activity: { kind: "compacting" },
1076
- });
1077
- expect(closeCountAfterDeadline).toBe(0);
1078
- expect(treeKillCountAfterDeadline).toBe(0);
1079
- });
1069
+ stopSession("sid-starting-stall");
1070
+ await vi.advanceTimersByTimeAsync(1_000);
1071
+ await runPromise;
1072
+
1073
+ expect(stateAfterDeadline).toMatchObject({
1074
+ status: "running",
1075
+ activity: { kind: "compacting" },
1076
+ });
1077
+ expect(closeCountAfterDeadline).toBe(0);
1078
+ expect(treeKillCountAfterDeadline).toBe(0);
1079
+ });
1080
1080
 
1081
1081
  it("self-heals a missing trigger-chat binding and gives automatic recovery the normal card lifecycle", async () => {
1082
1082
  vi.setSystemTime(0);
@@ -1519,7 +1519,7 @@ describe("runAgentSession response stall watchdog", () => {
1519
1519
  expect(receivedPrompts[1]).toContain(recoveryPrompt);
1520
1520
  expect(platform.sendText).toHaveBeenCalledWith(
1521
1521
  "chat-recovery-limit",
1522
- "⚠️ 自动续跑仍连续 3 分钟没有生成新回复,本次不再自动继续。",
1522
+ "⚠️ 自动续跑仍连续 3 分钟没有生成新回复,本次不再自动继续。",
1523
1523
  );
1524
1524
  });
1525
1525
 
@@ -1721,7 +1721,7 @@ describe("unified display loop activity status", () => {
1721
1721
  });
1722
1722
  });
1723
1723
 
1724
- describe("unified display loop terminal card update", () => {
1724
+ describe("unified display loop terminal card update", () => {
1725
1725
  beforeEach(() => {
1726
1726
  vi.useFakeTimers();
1727
1727
  resetState();
@@ -1735,7 +1735,7 @@ describe("unified display loop terminal card update", () => {
1735
1735
  vi.useRealTimers();
1736
1736
  });
1737
1737
 
1738
- it("shows a distinct auto-ended state and sends a warning even with an empty reply", async () => {
1738
+ it("shows a distinct auto-ended state and sends a warning even with an empty reply", async () => {
1739
1739
  const platform = mockPlatform("feishu");
1740
1740
  setSessionPlatform(platform);
1741
1741
 
@@ -1777,115 +1777,115 @@ describe("unified display loop terminal card update", () => {
1777
1777
  expect(card.header.title.content).toBe("已自动结束 · 3分钟无新内容");
1778
1778
  expect(card.header.template).toBe("orange");
1779
1779
  expect(platform.sendText).toHaveBeenCalledWith(
1780
- "chat-auto-ended",
1781
- "⚠️ 已自动结束:生成回复阶段连续 3 分钟没有字符变化。本轮没有可发送的回复内容。",
1780
+ "chat-auto-ended",
1781
+ "⚠️ 已自动结束:生成回复阶段连续 3 分钟没有字符变化。本轮没有可发送的回复内容。",
1782
1782
  );
1783
1783
  expect(displayCards.has("chat-auto-ended")).toBe(false);
1784
- });
1785
-
1786
- it("shows the stream root cause in the error card without a duplicate text notice", async () => {
1787
- const platform = mockPlatform("feishu");
1788
- setSessionPlatform(platform);
1789
-
1790
- bindChatToSession("sid-network-error", "chat-network-error");
1791
- recordLastActiveChat("sid-network-error", "chat-network-error");
1792
- sessionInfoMap.set("chat-network-error", {
1793
- sessionId: "sid-network-error",
1794
- turnCount: 1,
1795
- lastContextTokens: 0,
1796
- startTime: 0,
1797
- tool: "ccc",
1798
- });
1799
- displayCards.set("chat-network-error", {
1800
- cardId: "card-network-error",
1801
- sequence: 1,
1802
- cardBusy: false,
1803
- cardCreatedAt: Date.now(),
1804
- lastSentContent: "",
1805
- streamErrorNotified: false,
1806
- sessionId: "sid-network-error",
1807
- turnCount: 1,
1808
- dotCount: 0,
1809
- });
1810
- mockStreamStates.set("sid-network-error", {
1811
- accumulatedContent: "",
1812
- finalReply: "",
1813
- status: "error",
1814
- turnCount: 1,
1815
- terminalError: {
1816
- kind: "network_timeout",
1817
- title: "网络连接超时",
1818
- message: "连接模型服务失败,已重试 3 次,单次连接等待 10 秒。请检查网络、VPN或模型服务状态后重试。",
1819
- occurredAt: Date.now(),
1820
- },
1821
- });
1822
-
1823
- startUnifiedDisplayLoop();
1824
- await vi.advanceTimersByTimeAsync(3_000);
1825
-
1826
- const payload = vi.mocked(platform.cardUpdate).mock.calls[0]?.[1];
1827
- const card = JSON.parse(payload as string) as {
1828
- header: { title: { content: string }; template?: string };
1829
- body: { elements: Array<{ content?: string }> };
1830
- };
1831
- expect(card.header.title.content).toBe("异常结束 · 网络连接超时");
1832
- expect(card.header.template).toBe("red");
1833
- expect(card.body.elements[0]?.content).toContain("已重试 3 次");
1834
- expect(platform.sendText).not.toHaveBeenCalled();
1835
- expect(mockStreamStates.get("sid-network-error")?.finalReplySentTurn).toBe(1);
1836
- expect(displayCards.has("chat-network-error")).toBe(false);
1837
- });
1838
-
1839
- it("falls back to a root-cause text when the error card cannot be updated", async () => {
1840
- vi.spyOn(console, "error").mockImplementation(() => {});
1841
- const platform = mockPlatform("feishu");
1842
- platform.cardUpdate = vi.fn(async () => {
1843
- throw new Error("CardKit unavailable");
1844
- });
1845
- setSessionPlatform(platform);
1846
-
1847
- bindChatToSession("sid-error-fallback", "chat-error-fallback");
1848
- recordLastActiveChat("sid-error-fallback", "chat-error-fallback");
1849
- sessionInfoMap.set("chat-error-fallback", {
1850
- sessionId: "sid-error-fallback",
1851
- turnCount: 1,
1852
- lastContextTokens: 0,
1853
- startTime: 0,
1854
- tool: "ccc",
1855
- });
1856
- displayCards.set("chat-error-fallback", {
1857
- cardId: "card-error-fallback",
1858
- sequence: 1,
1859
- cardBusy: false,
1860
- cardCreatedAt: Date.now(),
1861
- lastSentContent: "",
1862
- streamErrorNotified: false,
1863
- sessionId: "sid-error-fallback",
1864
- turnCount: 1,
1865
- dotCount: 0,
1866
- });
1867
- mockStreamStates.set("sid-error-fallback", {
1868
- accumulatedContent: "",
1869
- finalReply: "",
1870
- status: "error",
1871
- turnCount: 1,
1872
- terminalError: {
1873
- kind: "network_timeout",
1874
- title: "网络连接超时",
1875
- message: "连接模型服务失败,已重试 3 次。",
1876
- occurredAt: Date.now(),
1877
- },
1878
- });
1879
-
1880
- startUnifiedDisplayLoop();
1881
- await vi.advanceTimersByTimeAsync(3_000);
1882
-
1883
- expect(platform.sendText).toHaveBeenCalledWith(
1884
- "chat-error-fallback",
1885
- expect.stringContaining("异常结束:网络连接超时"),
1886
- );
1887
- expect(displayCards.has("chat-error-fallback")).toBe(false);
1888
- });
1784
+ });
1785
+
1786
+ it("shows the stream root cause in the error card without a duplicate text notice", async () => {
1787
+ const platform = mockPlatform("feishu");
1788
+ setSessionPlatform(platform);
1789
+
1790
+ bindChatToSession("sid-network-error", "chat-network-error");
1791
+ recordLastActiveChat("sid-network-error", "chat-network-error");
1792
+ sessionInfoMap.set("chat-network-error", {
1793
+ sessionId: "sid-network-error",
1794
+ turnCount: 1,
1795
+ lastContextTokens: 0,
1796
+ startTime: 0,
1797
+ tool: "ccc",
1798
+ });
1799
+ displayCards.set("chat-network-error", {
1800
+ cardId: "card-network-error",
1801
+ sequence: 1,
1802
+ cardBusy: false,
1803
+ cardCreatedAt: Date.now(),
1804
+ lastSentContent: "",
1805
+ streamErrorNotified: false,
1806
+ sessionId: "sid-network-error",
1807
+ turnCount: 1,
1808
+ dotCount: 0,
1809
+ });
1810
+ mockStreamStates.set("sid-network-error", {
1811
+ accumulatedContent: "",
1812
+ finalReply: "",
1813
+ status: "error",
1814
+ turnCount: 1,
1815
+ terminalError: {
1816
+ kind: "network_timeout",
1817
+ title: "网络连接超时",
1818
+ message: "连接模型服务失败,已重试 3 次,单次连接等待 10 秒。请检查网络、VPN或模型服务状态后重试。",
1819
+ occurredAt: Date.now(),
1820
+ },
1821
+ });
1822
+
1823
+ startUnifiedDisplayLoop();
1824
+ await vi.advanceTimersByTimeAsync(3_000);
1825
+
1826
+ const payload = vi.mocked(platform.cardUpdate).mock.calls[0]?.[1];
1827
+ const card = JSON.parse(payload as string) as {
1828
+ header: { title: { content: string }; template?: string };
1829
+ body: { elements: Array<{ content?: string }> };
1830
+ };
1831
+ expect(card.header.title.content).toBe("异常结束 · 网络连接超时");
1832
+ expect(card.header.template).toBe("red");
1833
+ expect(card.body.elements[0]?.content).toContain("已重试 3 次");
1834
+ expect(platform.sendText).not.toHaveBeenCalled();
1835
+ expect(mockStreamStates.get("sid-network-error")?.finalReplySentTurn).toBe(1);
1836
+ expect(displayCards.has("chat-network-error")).toBe(false);
1837
+ });
1838
+
1839
+ it("falls back to a root-cause text when the error card cannot be updated", async () => {
1840
+ vi.spyOn(console, "error").mockImplementation(() => {});
1841
+ const platform = mockPlatform("feishu");
1842
+ platform.cardUpdate = vi.fn(async () => {
1843
+ throw new Error("CardKit unavailable");
1844
+ });
1845
+ setSessionPlatform(platform);
1846
+
1847
+ bindChatToSession("sid-error-fallback", "chat-error-fallback");
1848
+ recordLastActiveChat("sid-error-fallback", "chat-error-fallback");
1849
+ sessionInfoMap.set("chat-error-fallback", {
1850
+ sessionId: "sid-error-fallback",
1851
+ turnCount: 1,
1852
+ lastContextTokens: 0,
1853
+ startTime: 0,
1854
+ tool: "ccc",
1855
+ });
1856
+ displayCards.set("chat-error-fallback", {
1857
+ cardId: "card-error-fallback",
1858
+ sequence: 1,
1859
+ cardBusy: false,
1860
+ cardCreatedAt: Date.now(),
1861
+ lastSentContent: "",
1862
+ streamErrorNotified: false,
1863
+ sessionId: "sid-error-fallback",
1864
+ turnCount: 1,
1865
+ dotCount: 0,
1866
+ });
1867
+ mockStreamStates.set("sid-error-fallback", {
1868
+ accumulatedContent: "",
1869
+ finalReply: "",
1870
+ status: "error",
1871
+ turnCount: 1,
1872
+ terminalError: {
1873
+ kind: "network_timeout",
1874
+ title: "网络连接超时",
1875
+ message: "连接模型服务失败,已重试 3 次。",
1876
+ occurredAt: Date.now(),
1877
+ },
1878
+ });
1879
+
1880
+ startUnifiedDisplayLoop();
1881
+ await vi.advanceTimersByTimeAsync(3_000);
1882
+
1883
+ expect(platform.sendText).toHaveBeenCalledWith(
1884
+ "chat-error-fallback",
1885
+ expect.stringContaining("异常结束:网络连接超时"),
1886
+ );
1887
+ expect(displayCards.has("chat-error-fallback")).toBe(false);
1888
+ });
1889
1889
 
1890
1890
  it("does not repeat the same terminal CardKit sequence while the prompt is still active", async () => {
1891
1891
  const platform = mockPlatform("feishu");