chatccc 0.2.204 → 0.2.206

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chatccc",
3
- "version": "0.2.204",
3
+ "version": "0.2.206",
4
4
  "description": "Feishu bot bridge for Claude Code",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -0,0 +1,76 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import {
4
+ createAgentActivityTracker,
5
+ formatAgentActivityTitle,
6
+ updateAgentActivity,
7
+ } from "../agent-activity.ts";
8
+
9
+ describe("agent activity", () => {
10
+ it("starts with an explicit startup status", () => {
11
+ const tracker = createAgentActivityTracker(1_000);
12
+
13
+ expect(formatAgentActivityTitle(tracker.activity, 4_000)).toBe("正在启动 Agent · 3秒");
14
+ });
15
+
16
+ it("keeps the thinking timer stable across repeated thinking blocks", () => {
17
+ const tracker = createAgentActivityTracker(1_000);
18
+
19
+ expect(updateAgentActivity(tracker, { type: "thinking", thinking: "first" }, 2_000)).toBe(true);
20
+ expect(updateAgentActivity(tracker, { type: "thinking", thinking: "second" }, 5_000)).toBe(false);
21
+ expect(formatAgentActivityTitle(tracker.activity, 8_000)).toBe("思考中 · 6秒");
22
+ });
23
+
24
+ it("shows the active tool name and elapsed time", () => {
25
+ const tracker = createAgentActivityTracker(1_000);
26
+
27
+ updateAgentActivity(tracker, {
28
+ type: "tool_use",
29
+ id: "tool-1",
30
+ name: "Bash",
31
+ input: { command: "npm test" },
32
+ }, 3_000);
33
+
34
+ expect(formatAgentActivityTitle(tracker.activity, 38_000)).toBe("正在执行 Bash · 35秒");
35
+ });
36
+
37
+ it("tracks parallel tools and keeps the remaining tool after one result", () => {
38
+ const tracker = createAgentActivityTracker(1_000);
39
+
40
+ updateAgentActivity(tracker, { type: "tool_use", id: "read", name: "Read", input: {} }, 2_000);
41
+ updateAgentActivity(tracker, { type: "tool_use", id: "grep", name: "Grep", input: {} }, 3_000);
42
+ expect(formatAgentActivityTitle(tracker.activity, 4_000)).toBe("正在执行 Read 等 2 项 · 2秒");
43
+
44
+ updateAgentActivity(tracker, {
45
+ type: "tool_result",
46
+ tool_use_id: "read",
47
+ content: "done",
48
+ }, 5_000);
49
+ expect(formatAgentActivityTitle(tracker.activity, 7_000)).toBe("正在执行 Grep · 4秒");
50
+
51
+ updateAgentActivity(tracker, {
52
+ type: "tool_result",
53
+ tool_use_id: "grep",
54
+ content: "done",
55
+ }, 8_000);
56
+ expect(formatAgentActivityTitle(tracker.activity, 10_000)).toBe("正在处理工具结果 · 2秒");
57
+ });
58
+
59
+ it("switches to response and context-compaction statuses", () => {
60
+ const tracker = createAgentActivityTracker(1_000);
61
+
62
+ updateAgentActivity(tracker, { type: "text", text: "answer" }, 2_000);
63
+ expect(formatAgentActivityTitle(tracker.activity, 3_000)).toBe("正在生成回复 · 1秒");
64
+
65
+ updateAgentActivity(tracker, {
66
+ type: "compact_boundary",
67
+ trigger: "auto",
68
+ pre_tokens: 100_000,
69
+ }, 4_000);
70
+ expect(formatAgentActivityTitle(tracker.activity, 5_000)).toBe("正在整理上下文 · 1秒");
71
+ });
72
+
73
+ it("formats longer elapsed time without decorative animation", () => {
74
+ expect(formatAgentActivityTitle({ kind: "thinking", startedAt: 1_000 }, 74_000)).toBe("思考中 · 1分13秒");
75
+ });
76
+ });
@@ -76,12 +76,12 @@ describe("normalizeCodexMessage", () => {
76
76
  status: "in_progress",
77
77
  },
78
78
  });
79
- expect(result).not.toBeNull();
80
- expect(result!.type).toBe("assistant");
81
- expect(result!.blocks).toEqual([
82
- { type: "tool_use", name: "Bash", input: { command: "powershell.exe -Command ls" } },
83
- ]);
84
- });
79
+ expect(result).not.toBeNull();
80
+ expect(result!.type).toBe("assistant");
81
+ expect(result!.blocks).toEqual([
82
+ { type: "tool_use", id: "item_0", name: "Bash", input: { command: "powershell.exe -Command ls" } },
83
+ ]);
84
+ });
85
85
 
86
86
  it("normalizes command_execution completion as tool_result (success)", () => {
87
87
  const result = normalizeCodexMessage({
@@ -302,4 +302,4 @@ describe("createCodexAdapter", () => {
302
302
  });
303
303
  expect(await adapter.getSessionInfo("sid-C")).toBeUndefined();
304
304
  });
305
- });
305
+ });
@@ -660,11 +660,11 @@ describe("Cursor stream fixture - 端到端不重复", () => {
660
660
  session_id: "sid",
661
661
  timestamp_ms: 1000,
662
662
  } as Parameters<typeof normalizeCursorMessage>[0]);
663
- expect(result).not.toBeNull();
664
- expect(result!.blocks).toEqual([
665
- { type: "tool_use", name: "Bash", input: { command: "ls" } },
666
- ]);
667
- });
663
+ expect(result).not.toBeNull();
664
+ expect(result!.blocks).toEqual([
665
+ { type: "tool_use", id: "toolu_abc", name: "Bash", input: { command: "ls" } },
666
+ ]);
667
+ });
668
668
 
669
669
  it("normalizes tool_call completed → tool_result block (success)", () => {
670
670
  const result = normalizeCursorMessage({
@@ -0,0 +1,49 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import {
4
+ hasResponseStalled,
5
+ observeResponseProgress,
6
+ } from "../response-stall.ts";
7
+
8
+ describe("response stall detection", () => {
9
+ it("starts tracking while responding even when the total character count is zero", () => {
10
+ const observation = observeResponseProgress(undefined, true, 0, 1_000);
11
+
12
+ expect(observation).toEqual({
13
+ totalChars: 0,
14
+ unchangedSince: 1_000,
15
+ });
16
+ });
17
+
18
+ it("auto-ends only after the same character count has lasted three minutes", () => {
19
+ const first = observeResponseProgress(undefined, true, 12, 1_000);
20
+ const unchanged = observeResponseProgress(first, true, 12, 90_000);
21
+
22
+ expect(unchanged).toBe(first);
23
+ expect(hasResponseStalled(unchanged, 180_999, 180_000)).toBe(false);
24
+ expect(hasResponseStalled(unchanged, 181_000, 180_000)).toBe(true);
25
+ });
26
+
27
+ it("restarts the timer whenever the total character count changes", () => {
28
+ const first = observeResponseProgress(undefined, true, 12, 1_000);
29
+ const changed = observeResponseProgress(first, true, 13, 150_000);
30
+
31
+ expect(changed).toEqual({
32
+ totalChars: 13,
33
+ unchangedSince: 150_000,
34
+ });
35
+ expect(hasResponseStalled(changed, 181_000, 180_000)).toBe(false);
36
+ });
37
+
38
+ it("clears tracking outside responding and starts a fresh window after returning", () => {
39
+ const first = observeResponseProgress(undefined, true, 0, 1_000);
40
+ const cleared = observeResponseProgress(first, false, 0, 100_000);
41
+ const resumed = observeResponseProgress(cleared, true, 0, 200_000);
42
+
43
+ expect(cleared).toBeUndefined();
44
+ expect(resumed).toEqual({
45
+ totalChars: 0,
46
+ unchangedSince: 200_000,
47
+ });
48
+ });
49
+ });
@@ -1,13 +1,25 @@
1
1
  import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
2
2
  import { mkdtemp, rm, readFile } from "node:fs/promises";
3
3
  import { tmpdir } from "node:os";
4
- import { dirname, join } from "node:path";
5
-
6
- // mock stream-state 以支持在测试中控制累积长度
7
- const mockStreamStates = new Map<string, {
8
- accumulatedContent: string;
9
- finalReply: string;
10
- status?: "running" | "done" | "stopped" | "error";
4
+ import { dirname, join } from "node:path";
5
+
6
+ const killProcessTreeMock = vi.hoisted(() => vi.fn(async () => {}));
7
+ vi.mock("../adapters/proc-tree-kill.ts", () => ({
8
+ killProcessTree: killProcessTreeMock,
9
+ }));
10
+
11
+ // mock stream-state 以支持在测试中控制累积长度
12
+ const mockStreamStates = new Map<string, {
13
+ accumulatedContent: string;
14
+ finalReply: string;
15
+ activity?: {
16
+ kind: "starting" | "thinking" | "tool" | "processing" | "responding" | "searching" | "compacting";
17
+ startedAt: number;
18
+ toolName?: string;
19
+ toolCount?: number;
20
+ };
21
+ status?: "running" | "done" | "stopped" | "error" | "auto_ended";
22
+ autoEndedAt?: number;
11
23
  turnCount?: number;
12
24
  finalReplySentTurn?: number;
13
25
  finalReplySentAt?: number;
@@ -18,10 +30,12 @@ vi.mock("../stream-state.ts", () => ({
18
30
  if (!state) return null;
19
31
  return {
20
32
  sessionId: sid,
21
- accumulatedContent: state.accumulatedContent,
22
- finalReply: state.finalReply,
23
- finalReplySentTurn: state.finalReplySentTurn,
24
- finalReplySentAt: state.finalReplySentAt,
33
+ accumulatedContent: state.accumulatedContent,
34
+ finalReply: state.finalReply,
35
+ activity: state.activity,
36
+ finalReplySentTurn: state.finalReplySentTurn,
37
+ finalReplySentAt: state.finalReplySentAt,
38
+ autoEndedAt: state.autoEndedAt,
25
39
  status: state.status ?? "running",
26
40
  chunkCount: 0,
27
41
  turnCount: state.turnCount ?? 0,
@@ -34,24 +48,33 @@ vi.mock("../stream-state.ts", () => ({
34
48
  writeStreamState: async (state: {
35
49
  sessionId: string;
36
50
  accumulatedContent: string;
37
- finalReply: string;
38
- status?: "running" | "done" | "stopped" | "error";
51
+ finalReply: string;
52
+ activity?: {
53
+ kind: "starting" | "thinking" | "tool" | "processing" | "responding" | "searching" | "compacting";
54
+ startedAt: number;
55
+ toolName?: string;
56
+ toolCount?: number;
57
+ };
58
+ status?: "running" | "done" | "stopped" | "error" | "auto_ended";
59
+ autoEndedAt?: number;
39
60
  turnCount?: number;
40
61
  finalReplySentTurn?: number;
41
62
  finalReplySentAt?: number;
42
63
  }) => {
43
64
  mockStreamStates.set(state.sessionId, {
44
- accumulatedContent: state.accumulatedContent,
45
- finalReply: state.finalReply,
46
- status: state.status,
65
+ accumulatedContent: state.accumulatedContent,
66
+ finalReply: state.finalReply,
67
+ activity: state.activity,
68
+ status: state.status,
69
+ autoEndedAt: state.autoEndedAt,
47
70
  turnCount: state.turnCount,
48
71
  finalReplySentTurn: state.finalReplySentTurn,
49
72
  finalReplySentAt: state.finalReplySentAt,
50
73
  });
51
74
  },
52
- createEmptyStreamState: (sid: string, cwd: string, tool: string, turnCount: number) => ({
53
- sessionId: sid, status: "running" as const, accumulatedContent: "", finalReply: "", chunkCount: 0, turnCount, contextTokens: 0, updatedAt: Date.now(), cwd, tool,
54
- }),
75
+ createEmptyStreamState: (sid: string, cwd: string, tool: string, turnCount: number) => ({
76
+ sessionId: sid, status: "running" as const, accumulatedContent: "", finalReply: "", activity: { kind: "starting" as const, startedAt: Date.now() }, chunkCount: 0, turnCount, contextTokens: 0, updatedAt: Date.now(), cwd, tool,
77
+ }),
55
78
  isFinalReplySentForTurn: (state: { turnCount: number; finalReplySentTurn?: number }) => state.finalReplySentTurn === state.turnCount,
56
79
  markFinalReplySent: async (sid: string, turnCount: number, sentAt = Date.now()) => {
57
80
  const state = mockStreamStates.get(sid);
@@ -95,6 +118,10 @@ import {
95
118
  _resetProcessAliveForTest,
96
119
  _setProcessMonitorIntervalForTest,
97
120
  _resetProcessMonitorIntervalForTest,
121
+ _setResponseStallTimeoutForTest,
122
+ _resetResponseStallTimeoutForTest,
123
+ _setResponseStallCheckIntervalForTest,
124
+ _resetResponseStallCheckIntervalForTest,
98
125
  setSessionEffortOverride,
99
126
  } from "../session.ts";
100
127
  import {
@@ -335,13 +362,15 @@ describe("runAgentSession process monitor", () => {
335
362
  _resetSessionRegistryFileForTest();
336
363
  _resetSessionToolsFileForTest();
337
364
  _clearAdapterCacheForTest();
338
- _resetProcessAliveForTest();
339
- _resetProcessMonitorIntervalForTest();
365
+ _resetProcessAliveForTest();
366
+ _resetProcessMonitorIntervalForTest();
367
+ _resetResponseStallTimeoutForTest();
368
+ _resetResponseStallCheckIntervalForTest();
340
369
  resetBindingState();
341
370
  vi.useRealTimers();
342
371
  });
343
372
 
344
- it("marks the turn as error and sends a separate notice when the CLI process disappears", async () => {
373
+ it("marks the turn as error and sends a separate notice when the CLI process disappears", async () => {
345
374
  const platform = mockPlatform("feishu");
346
375
  setSessionPlatform(platform);
347
376
  bindChatToSession("sid-process", "chat-process");
@@ -569,8 +598,108 @@ describe("runAgentSession process monitor", () => {
569
598
  });
570
599
  });
571
600
 
572
- describe("unified display loop WeChat delta", () => {
573
- beforeEach(() => {
601
+ describe("runAgentSession response stall watchdog", () => {
602
+ let tempDir = "";
603
+
604
+ beforeEach(async () => {
605
+ vi.useFakeTimers();
606
+ resetState();
607
+ resetBindingState();
608
+ mockStreamStates.clear();
609
+ killProcessTreeMock.mockClear();
610
+ tempDir = await mkdtemp(join(tmpdir(), "chatccc-response-stall-"));
611
+ _setSessionRegistryFileForTest(join(tempDir, "session-registry.json"));
612
+ _setSessionToolsFileForTest(join(tempDir, "session-tools.json"));
613
+ });
614
+
615
+ afterEach(async () => {
616
+ _resetSessionRegistryFileForTest();
617
+ _resetSessionToolsFileForTest();
618
+ _clearAdapterCacheForTest();
619
+ _resetProcessAliveForTest();
620
+ _resetResponseStallTimeoutForTest();
621
+ _resetResponseStallCheckIntervalForTest();
622
+ resetBindingState();
623
+ vi.useRealTimers();
624
+ if (tempDir) await rm(tempDir, { recursive: true, force: true });
625
+ });
626
+
627
+ it("auto-ends every Agent after three minutes of unchanged reply characters, including zero", async () => {
628
+ vi.setSystemTime(0);
629
+ _setResponseStallTimeoutForTest(180_000);
630
+ _setResponseStallCheckIntervalForTest(1_000);
631
+ _setProcessAliveForTest(() => true);
632
+
633
+ const platform = mockPlatform("feishu");
634
+ setSessionPlatform(platform);
635
+ bindChatToSession("sid-response-stall", "chat-response-stall");
636
+ recordLastActiveChat("sid-response-stall", "chat-response-stall");
637
+
638
+ const closeSession = vi.fn();
639
+ const adapter: ToolAdapter = {
640
+ displayName: "Any Agent",
641
+ sessionDescPrefix: "Agent Session:",
642
+ createSession: async () => ({ sessionId: "sid-response-stall" }),
643
+ getSessionInfo: async (sid) => ({ sessionId: sid, cwd: "F:\\repo" }),
644
+ closeSession: async () => {},
645
+ prompt: async function* (
646
+ _sid: string,
647
+ _text: string,
648
+ _cwd: string,
649
+ signal?: AbortSignal,
650
+ options?: ToolPromptOptions,
651
+ ) {
652
+ options?.onSessionCreated?.(closeSession);
653
+ options?.onProcessStart?.({ pid: 4242 });
654
+ yield { type: "assistant", blocks: [{ type: "text", text: "" }] };
655
+ await new Promise<void>((resolve) => {
656
+ if (signal?.aborted) {
657
+ resolve();
658
+ return;
659
+ }
660
+ signal?.addEventListener("abort", () => resolve(), { once: true });
661
+ });
662
+ },
663
+ };
664
+ _setAdapterForToolForTest("claude", adapter);
665
+
666
+ const runPromise = runAgentSession(
667
+ "sid-response-stall",
668
+ "prompt",
669
+ platform,
670
+ "chat-response-stall",
671
+ Date.now(),
672
+ "claude",
673
+ );
674
+
675
+ await vi.waitFor(() => {
676
+ expect(activePrompts.get("sid-response-stall")?.responseProgress).toEqual({
677
+ totalChars: 0,
678
+ unchangedSince: expect.any(Number),
679
+ });
680
+ });
681
+
682
+ const progress = activePrompts.get("sid-response-stall")!.responseProgress!;
683
+ const remainingBeforeBoundary = progress.unchangedSince + 180_000 - Date.now() - 1;
684
+ await vi.advanceTimersByTimeAsync(remainingBeforeBoundary);
685
+ expect(activePrompts.has("sid-response-stall")).toBe(true);
686
+
687
+ await vi.advanceTimersByTimeAsync(1_001);
688
+ await runPromise;
689
+
690
+ expect(closeSession).toHaveBeenCalledTimes(1);
691
+ expect(killProcessTreeMock).toHaveBeenCalledWith(4242);
692
+ expect(activePrompts.has("sid-response-stall")).toBe(false);
693
+ expect(mockStreamStates.get("sid-response-stall")).toMatchObject({
694
+ status: "auto_ended",
695
+ finalReply: "",
696
+ autoEndedAt: expect.any(Number),
697
+ });
698
+ });
699
+ });
700
+
701
+ describe("unified display loop WeChat delta", () => {
702
+ beforeEach(() => {
574
703
  vi.useFakeTimers();
575
704
  resetState();
576
705
  resetBindingState();
@@ -636,10 +765,77 @@ describe("unified display loop WeChat delta", () => {
636
765
  "chat-wechat",
637
766
  "tool output",
638
767
  );
639
- });
640
- });
641
-
642
- describe("unified display loop terminal card update", () => {
768
+ });
769
+
770
+ });
771
+
772
+ describe("unified display loop activity status", () => {
773
+ beforeEach(() => {
774
+ vi.useFakeTimers();
775
+ vi.setSystemTime(new Date("2026-07-16T08:00:00.000Z"));
776
+ resetState();
777
+ resetBindingState();
778
+ mockStreamStates.clear();
779
+ });
780
+
781
+ afterEach(() => {
782
+ stopUnifiedDisplayLoop();
783
+ resetBindingState();
784
+ vi.useRealTimers();
785
+ });
786
+
787
+ it("renders explicit activity and elapsed time alongside the liveness dots", async () => {
788
+ const platform = mockPlatform("feishu");
789
+ setSessionPlatform(platform);
790
+
791
+ bindChatToSession("sid-activity", "chat-activity");
792
+ recordLastActiveChat("sid-activity", "chat-activity");
793
+ sessionInfoMap.set("chat-activity", {
794
+ sessionId: "sid-activity",
795
+ turnCount: 1,
796
+ lastContextTokens: 0,
797
+ startTime: Date.now() - 12_000,
798
+ tool: "codex",
799
+ });
800
+ displayCards.set("chat-activity", {
801
+ cardId: "card-activity",
802
+ sequence: 1,
803
+ cardBusy: false,
804
+ cardCreatedAt: Date.now(),
805
+ lastSentContent: "",
806
+ streamErrorNotified: false,
807
+ sessionId: "sid-activity",
808
+ turnCount: 1,
809
+ dotCount: 0,
810
+ });
811
+ mockStreamStates.set("sid-activity", {
812
+ accumulatedContent: "正在检查日志",
813
+ finalReply: "",
814
+ activity: {
815
+ kind: "tool",
816
+ startedAt: Date.now() - 12_000,
817
+ toolName: "Shell",
818
+ toolCount: 1,
819
+ },
820
+ status: "running",
821
+ turnCount: 1,
822
+ });
823
+
824
+ startUnifiedDisplayLoop();
825
+ await vi.advanceTimersByTimeAsync(3_000);
826
+
827
+ const payload = vi.mocked(platform.cardUpdate).mock.calls[0]?.[1];
828
+ expect(payload).toBeTypeOf("string");
829
+ const card = JSON.parse(payload as string) as {
830
+ header: { title: { content: string } };
831
+ body: { elements: Array<{ tag: string; content?: string }> };
832
+ };
833
+ expect(card.header.title.content).toBe("正在执行 Shell · 15秒");
834
+ expect(card.body.elements[0]?.content).toBe("正在检查日志\n。");
835
+ });
836
+ });
837
+
838
+ describe("unified display loop terminal card update", () => {
643
839
  beforeEach(() => {
644
840
  vi.useFakeTimers();
645
841
  resetState();
@@ -647,13 +843,61 @@ describe("unified display loop terminal card update", () => {
647
843
  mockStreamStates.clear();
648
844
  });
649
845
 
650
- afterEach(() => {
846
+ afterEach(() => {
651
847
  stopUnifiedDisplayLoop();
652
848
  resetBindingState();
653
- vi.useRealTimers();
654
- });
655
-
656
- it("does not repeat the same terminal CardKit sequence while the prompt is still active", async () => {
849
+ vi.useRealTimers();
850
+ });
851
+
852
+ it("shows a distinct auto-ended state and sends a warning even with an empty reply", async () => {
853
+ const platform = mockPlatform("feishu");
854
+ setSessionPlatform(platform);
855
+
856
+ bindChatToSession("sid-auto-ended", "chat-auto-ended");
857
+ recordLastActiveChat("sid-auto-ended", "chat-auto-ended");
858
+ sessionInfoMap.set("chat-auto-ended", {
859
+ sessionId: "sid-auto-ended",
860
+ turnCount: 1,
861
+ lastContextTokens: 0,
862
+ startTime: 0,
863
+ tool: "cursor",
864
+ });
865
+ displayCards.set("chat-auto-ended", {
866
+ cardId: "card-auto-ended",
867
+ sequence: 1,
868
+ cardBusy: false,
869
+ cardCreatedAt: Date.now(),
870
+ lastSentContent: "",
871
+ streamErrorNotified: false,
872
+ sessionId: "sid-auto-ended",
873
+ turnCount: 1,
874
+ dotCount: 0,
875
+ });
876
+ mockStreamStates.set("sid-auto-ended", {
877
+ accumulatedContent: "",
878
+ finalReply: "",
879
+ status: "auto_ended",
880
+ turnCount: 1,
881
+ autoEndedAt: Date.now(),
882
+ });
883
+
884
+ startUnifiedDisplayLoop();
885
+ await vi.advanceTimersByTimeAsync(3_000);
886
+
887
+ const payload = vi.mocked(platform.cardUpdate).mock.calls[0]?.[1];
888
+ const card = JSON.parse(payload as string) as {
889
+ header: { title: { content: string }; template?: string };
890
+ };
891
+ expect(card.header.title.content).toBe("已自动结束 · 3分钟无新内容");
892
+ expect(card.header.template).toBe("orange");
893
+ expect(platform.sendText).toHaveBeenCalledWith(
894
+ "chat-auto-ended",
895
+ "⚠️ 已自动结束:连续 3 分钟处于“正在生成回复”且回复字符总数没有变化。本轮没有可发送的回复内容。",
896
+ );
897
+ expect(displayCards.has("chat-auto-ended")).toBe(false);
898
+ });
899
+
900
+ it("does not repeat the same terminal CardKit sequence while the prompt is still active", async () => {
657
901
  const platform = mockPlatform("feishu");
658
902
  platform.cardUpdate = vi.fn(async () => {
659
903
  throw new Error("CardKit update: [300317] ErrMsg: sequence number compare failed; ");
@@ -153,6 +153,7 @@ export function normalizeCodexMessage(
153
153
  blocks: [
154
154
  {
155
155
  type: "tool_use",
156
+ id: msg.item.id,
156
157
  name: "Bash",
157
158
  input: { command: msg.item.command },
158
159
  },
@@ -297,6 +297,7 @@ export function normalizeCursorMessage(
297
297
  blocks: [
298
298
  {
299
299
  type: "tool_use",
300
+ id: msg.call_id,
300
301
  name: mapToolCallKey(toolKey),
301
302
  input: toolData.args ?? {},
302
303
  },