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:
@@ -159,6 +159,79 @@ describe("convertClaudeHistoryEntry", () => {
|
|||||||
expect(convertClaudeHistoryEntry(assistantNoiseEntry, mapBlocks)).toEqual([]);
|
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", () => {
|
test("maps task notifications to synthetic tool calls", () => {
|
||||||
const entry = {
|
const entry = {
|
||||||
type: "system",
|
type: "system",
|
||||||
|
|||||||
@@ -418,8 +418,19 @@ function isClaudeNoResponsePlaceholderText(value: unknown): boolean {
|
|||||||
return normalizeClaudeTranscriptText(value) === NO_RESPONSE_REQUESTED_PLACEHOLDER;
|
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 {
|
function isClaudeTranscriptNoiseText(value: unknown): boolean {
|
||||||
return isClaudeInterruptPlaceholderText(value) || isClaudeNoResponsePlaceholderText(value);
|
return (
|
||||||
|
isClaudeInterruptPlaceholderText(value) ||
|
||||||
|
isClaudeNoResponsePlaceholderText(value) ||
|
||||||
|
isClaudeLocalCommandStdout(value)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function collectClaudeTextContentParts(content: unknown): string[] {
|
function collectClaudeTextContentParts(content: unknown): string[] {
|
||||||
|
|||||||
Reference in New Issue
Block a user