fix(claude): filter <local-command-stdout> 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.
This commit is contained in:
Mohamed Boudra
2026-04-08 22:03:30 +07:00
parent aae4d9f8dd
commit fc667d6312
2 changed files with 85 additions and 1 deletions
@@ -159,6 +159,79 @@ describe("convertClaudeHistoryEntry", () => {
expect(convertClaudeHistoryEntry(assistantNoiseEntry, mapBlocks)).toEqual([]);
});
test("skips <local-command-stdout> messages (model switch, /context, etc.)", () => {
// Real entries from Claude Code JSONL history files
const modelSwitch = {
type: "user",
message: {
role: "user",
content:
"<local-command-stdout>Set model to claude-opus-4-6</local-command-stdout>",
},
userType: "external",
};
const modelSwitchWithAnsi = {
type: "user",
message: {
role: "user",
content:
'<local-command-stdout>Set model to \u001b[1mopus (claude-opus-4-6)\u001b[22m</local-command-stdout>',
},
};
const contextDump = {
type: "user",
message: {
role: "user",
content:
"<local-command-stdout>## Context Usage\n\n**Model:** claude-opus-4-6\n**Tokens:** 19k</local-command-stdout>",
},
};
const planMode = {
type: "user",
message: {
role: "user",
content: "<local-command-stdout>Enabled plan mode</local-command-stdout>",
},
};
const goodbye = {
type: "user",
message: {
role: "user",
content: "<local-command-stdout>Bye!</local-command-stdout>",
},
};
const empty = {
type: "user",
message: {
role: "user",
content: "<local-command-stdout></local-command-stdout>",
},
};
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",
@@ -418,8 +418,19 @@ function isClaudeNoResponsePlaceholderText(value: unknown): boolean {
return normalizeClaudeTranscriptText(value) === NO_RESPONSE_REQUESTED_PLACEHOLDER;
}
const LOCAL_COMMAND_STDOUT_PATTERN = /^\s*<local-command-stdout>[\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[] {