fix(agent-claude-code): treat pr-link as re-snapshot noise too

Empirical evidence from production: ao-160's JSONL had the same PR (#1911) written as a `pr-link` entry 33 times in the last 200 lines, alternating with permission-mode/ai-title in the same trio pattern. Internal timestamps showed re-emissions ~1 minute apart (15:24:40, 15:26:27, 15:27:42 — same PR, three writes within ~3 minutes).

User-visible bug: ao-139 (last real work 11h ago) and ao-160 (last real work over a day ago) were both showing as `ready` on the dashboard because the file mtime kept moving forward via these pr-link re-snapshots, and pr-link was in the explicit ready/idle bookkeeping case.

Fix: move `pr-link` from the bookkeeping case into NOISE_JSONL_TYPES so the cascade skips it and falls through to the AO JSONL pipeline (and ultimately to the stale-native idle fallback for genuinely dormant sessions). Real PR creation is still observable via the assistant message that asked for it and via the gh-tracker.

Verified: 200/200 plugin tests pass. The existing pr-link test updated to expect idle (matches new behavior); added comment with the empirical evidence.
This commit is contained in:
harshitsinghbhandari 2026-05-19 21:00:35 +05:30
parent 8aeffdcd86
commit 4e0e64ab06
2 changed files with 27 additions and 9 deletions

View File

@ -407,9 +407,19 @@ describe("Claude Code Activity Detection", () => {
expect((await agent.getActivityState(makeSession()))?.state).toBe("ready");
});
it("returns 'ready' for recent 'pr-link' (bookkeeping)", async () => {
writeJsonl([{ type: "pr-link" }]);
expect((await agent.getActivityState(makeSession()))?.state).toBe("ready");
it("returns 'idle' (not 'ready') for recent 'pr-link' — re-snapshot noise", async () => {
// Empirical evidence (ao-160): the SAME PR (#1911) was written as
// pr-link 33 times in the last 200 lines, with new timestamps each
// time. It's a periodic state snapshot, not a one-shot event.
// Treat as noise so dormant sessions don't show as "ready" forever.
writeJsonl([
{
type: "pr-link",
prNumber: 1911,
prUrl: "https://github.com/owner/repo/pull/1911",
},
]);
expect((await agent.getActivityState(makeSession()))?.state).toBe("idle");
});
it("returns 'ready' for recent 'attachment' (bookkeeping)", async () => {

View File

@ -352,6 +352,14 @@ const NOISE_JSONL_TYPES: ReadonlySet<string> = new Set([
"agent-color",
"agent-name",
"custom-title",
// pr-link is also re-snapshot noise — verified on ao-160's JSONL where the
// SAME PR (#1911) was written as a `pr-link` entry three times within
// minutes (count: 33 pr-link vs 21 user messages in the last 200 lines).
// The first emission is real; subsequent re-emissions are state snapshots.
// We can't distinguish first vs Nth from the last line alone, so treat
// all pr-link as noise. Real PR creation is still observable via the
// assistant message and the gh-tracker side.
"pr-link",
]);
/**
@ -443,14 +451,14 @@ export async function getClaudeActivityState(
case "summary":
return { state: ageMs > threshold ? "idle" : "ready", timestamp };
// Bookkeeping types Claude writes AFTER finishing a turn (file
// edits, PR creation, attachment context). Map to ready/idle by
// age, same as assistant/summary. Pure UI-noise types
// (permission-mode, ai-title, agent-*, custom-title) are handled
// separately by the NOISE_JSONL_TYPES check above the switch.
// Bookkeeping types Claude writes AFTER a real event (file edits,
// attachment context, queue housekeeping, prompt submit). Map to
// ready/idle by age, same as assistant/summary. The pure re-snapshot
// types (permission-mode, ai-title, agent-*, custom-title, pr-link)
// are filtered out earlier by NOISE_JSONL_TYPES — they get written
// continuously without indicating activity.
case "file-history-snapshot":
case "attachment":
case "pr-link":
case "queue-operation":
case "last-prompt":
return { state: ageMs > threshold ? "idle" : "ready", timestamp };