From 39c2b1ddea28302ec571e94730c0ea925a4080da Mon Sep 17 00:00:00 2001 From: harshitsinghbhandari <24b4506@iitb.ac.in> Date: Tue, 19 May 2026 18:11:38 +0530 Subject: [PATCH] fix(agent-claude-code): map bookkeeping JSONL types to ready, not active MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Claude writes several types AFTER finishing a turn — `file-history-snapshot`, `attachment`, `pr-link`, `queue-operation`, `permission-mode`, `last-prompt`, `ai-title`, `agent-color`, `agent-name`, `custom-title`. Until now they fell through to the `default` switch branch and looked `active` for 30s, making finished sessions appear busy. This was almost certainly the root cause of "Claude looks like it's still working when it's done" reports (the #1908 family). Add explicit cases mapping each to `ready`/`idle` by age, same as `assistant`/`summary`. Three existing tests that asserted these returned `active` were testing the buggy behavior — updated to the correct behavior, plus four new tests covering `attachment`, `permission-mode`, and `ai-title`. --- .../src/__tests__/activity-detection.test.ts | 31 +++++++++++++++---- .../src/activity-detection.ts | 19 ++++++++++++ 2 files changed, 44 insertions(+), 6 deletions(-) diff --git a/packages/plugins/agent-claude-code/src/__tests__/activity-detection.test.ts b/packages/plugins/agent-claude-code/src/__tests__/activity-detection.test.ts index bc2735913..e38de91a7 100644 --- a/packages/plugins/agent-claude-code/src/__tests__/activity-detection.test.ts +++ b/packages/plugins/agent-claude-code/src/__tests__/activity-detection.test.ts @@ -276,19 +276,38 @@ describe("Claude Code Activity Detection", () => { expect((await agent.getActivityState(makeSession()))?.state).toBe("ready"); }); - it("returns 'active' for recent 'file-history-snapshot' (bookkeeping)", async () => { + // Bookkeeping types Claude writes AFTER finishing a turn — these are + // turn-end markers, not "Claude is working" signals. They must map to + // ready/idle by age, NOT active. Previously they fell through to the + // default branch and looked "active" for 30s; fixed in this PR. + it("returns 'ready' for recent 'file-history-snapshot' (bookkeeping)", async () => { writeJsonl([{ type: "file-history-snapshot" }]); - expect((await agent.getActivityState(makeSession()))?.state).toBe("active"); + expect((await agent.getActivityState(makeSession()))?.state).toBe("ready"); }); - it("returns 'active' for recent 'queue-operation' (bookkeeping)", async () => { + it("returns 'ready' for recent 'queue-operation' (bookkeeping)", async () => { writeJsonl([{ type: "queue-operation" }]); - expect((await agent.getActivityState(makeSession()))?.state).toBe("active"); + expect((await agent.getActivityState(makeSession()))?.state).toBe("ready"); }); - it("returns 'active' for recent 'pr-link' (bookkeeping)", async () => { + it("returns 'ready' for recent 'pr-link' (bookkeeping)", async () => { writeJsonl([{ type: "pr-link" }]); - expect((await agent.getActivityState(makeSession()))?.state).toBe("active"); + expect((await agent.getActivityState(makeSession()))?.state).toBe("ready"); + }); + + it("returns 'ready' for recent 'attachment' (bookkeeping)", async () => { + writeJsonl([{ type: "attachment", attachment: { type: "skill_listing" } }]); + expect((await agent.getActivityState(makeSession()))?.state).toBe("ready"); + }); + + it("returns 'ready' for recent 'permission-mode' (bookkeeping)", async () => { + writeJsonl([{ type: "permission-mode", permissionMode: "bypassPermissions" }]); + expect((await agent.getActivityState(makeSession()))?.state).toBe("ready"); + }); + + it("returns 'ready' for recent UI-metadata bookkeeping (ai-title)", async () => { + writeJsonl([{ type: "ai-title", title: "Fix login bug" }]); + expect((await agent.getActivityState(makeSession()))?.state).toBe("ready"); }); }); diff --git a/packages/plugins/agent-claude-code/src/activity-detection.ts b/packages/plugins/agent-claude-code/src/activity-detection.ts index 3022c4011..ba6a96d11 100644 --- a/packages/plugins/agent-claude-code/src/activity-detection.ts +++ b/packages/plugins/agent-claude-code/src/activity-detection.ts @@ -304,6 +304,25 @@ export async function getClaudeActivityState( case "result": return { state: ageMs > threshold ? "idle" : "ready", timestamp }; + // Bookkeeping types Claude writes AFTER finishing a turn (UI metadata, + // file snapshots, queue housekeeping). When one of these is the LAST + // entry, Claude is done — not active. Without these explicit cases + // they'd fall through to `default` and look `active` for 30s, which + // is the root cause of "Claude looks busy when it's done" reports. + // Survey of types observed in real ~/.claude/projects/ JSONL: see + // PR #1927 description. + case "file-history-snapshot": + case "attachment": + case "pr-link": + case "queue-operation": + case "permission-mode": + case "last-prompt": + case "ai-title": + case "agent-color": + case "agent-name": + case "custom-title": + return { state: ageMs > threshold ? "idle" : "ready", timestamp }; + default: if (ageMs <= activeWindowMs) return { state: "active", timestamp }; return { state: ageMs > threshold ? "idle" : "ready", timestamp };