From 75542c10d8f5685ad0041b1e30d1b812cd6d3db9 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Wed, 8 Apr 2026 22:03:30 +0700 Subject: [PATCH] fix(claude): filter messages from timeline Claude Code writes local CLI command outputs (model switches, /context, /compact, goodbye messages, etc.) as user entries in the JSONL history. These were leaking through as user_message timeline items in Paseo. --- .../agent/providers/claude-agent.test.ts | 73 +++++++++++++++++++ .../server/agent/providers/claude-agent.ts | 13 +++- 2 files changed, 85 insertions(+), 1 deletion(-) diff --git a/packages/server/src/server/agent/providers/claude-agent.test.ts b/packages/server/src/server/agent/providers/claude-agent.test.ts index 8da63128c..35c608c5f 100644 --- a/packages/server/src/server/agent/providers/claude-agent.test.ts +++ b/packages/server/src/server/agent/providers/claude-agent.test.ts @@ -159,6 +159,79 @@ describe("convertClaudeHistoryEntry", () => { expect(convertClaudeHistoryEntry(assistantNoiseEntry, mapBlocks)).toEqual([]); }); + test("skips messages (model switch, /context, etc.)", () => { + // Real entries from Claude Code JSONL history files + const modelSwitch = { + type: "user", + message: { + role: "user", + content: + "Set model to claude-opus-4-6", + }, + userType: "external", + }; + + const modelSwitchWithAnsi = { + type: "user", + message: { + role: "user", + content: + 'Set model to \u001b[1mopus (claude-opus-4-6)\u001b[22m', + }, + }; + + const contextDump = { + type: "user", + message: { + role: "user", + content: + "## Context Usage\n\n**Model:** claude-opus-4-6\n**Tokens:** 19k", + }, + }; + + const planMode = { + type: "user", + message: { + role: "user", + content: "Enabled plan mode", + }, + }; + + const goodbye = { + type: "user", + message: { + role: "user", + content: "Bye!", + }, + }; + + const empty = { + type: "user", + message: { + role: "user", + content: "", + }, + }; + + const mapBlocks = vi.fn().mockReturnValue([]); + + expect(convertClaudeHistoryEntry(modelSwitch, mapBlocks)).toEqual([]); + expect(convertClaudeHistoryEntry(modelSwitchWithAnsi, mapBlocks)).toEqual([]); + expect(convertClaudeHistoryEntry(contextDump, mapBlocks)).toEqual([]); + expect(convertClaudeHistoryEntry(planMode, mapBlocks)).toEqual([]); + expect(convertClaudeHistoryEntry(goodbye, mapBlocks)).toEqual([]); + expect(convertClaudeHistoryEntry(empty, mapBlocks)).toEqual([]); + + // Real user messages must NOT be filtered + const realMessage = { + type: "user", + message: { role: "user", content: "fix the bug in auth.ts" }, + }; + expect(convertClaudeHistoryEntry(realMessage, mapBlocks)).toEqual([ + { type: "user_message", text: "fix the bug in auth.ts" }, + ]); + }); + test("maps task notifications to synthetic tool calls", () => { const entry = { type: "system", diff --git a/packages/server/src/server/agent/providers/claude-agent.ts b/packages/server/src/server/agent/providers/claude-agent.ts index b8c742799..daabaa3cf 100644 --- a/packages/server/src/server/agent/providers/claude-agent.ts +++ b/packages/server/src/server/agent/providers/claude-agent.ts @@ -418,8 +418,19 @@ function isClaudeNoResponsePlaceholderText(value: unknown): boolean { return normalizeClaudeTranscriptText(value) === NO_RESPONSE_REQUESTED_PLACEHOLDER; } +const LOCAL_COMMAND_STDOUT_PATTERN = /^\s*[\s\S]*<\/local-command-stdout>\s*$/; + +function isClaudeLocalCommandStdout(value: unknown): boolean { + const normalized = normalizeClaudeTranscriptText(value); + return normalized !== null && LOCAL_COMMAND_STDOUT_PATTERN.test(normalized); +} + function isClaudeTranscriptNoiseText(value: unknown): boolean { - return isClaudeInterruptPlaceholderText(value) || isClaudeNoResponsePlaceholderText(value); + return ( + isClaudeInterruptPlaceholderText(value) || + isClaudeNoResponsePlaceholderText(value) || + isClaudeLocalCommandStdout(value) + ); } function collectClaudeTextContentParts(content: unknown): string[] {