fix(server): skip Claude request-journal records when loading history

Claude Code 2.1.304+ journals each API request into the transcript as
api-request-blob records. They carry a message with a role and content,
so the history loader rendered them as assistant messages: restored
timelines gained the session's system context, including SessionStart
hook output and skill and tool listings.

Only user and assistant records are conversation turns once the system
record handling has run, so short-circuit every other type.

Closes #7102

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Roger Oba
2026-10-21 12:06:03 -03:00
co-authored by Claude Opus 5.5
parent e522e6ea8e
commit 2425282e6d
2 changed files with 38 additions and 0 deletions
@@ -141,6 +141,39 @@ describe("convertClaudeHistoryEntry", () => {
]);
});
test("ignores Claude Code request-journal records that carry a message", () => {
// Claude Code 2.1.x journals each API request into the transcript. These records carry a
// message with a role and content, but they are copies of the request, not conversation turns.
const entries = [
{
type: "api-request-blob",
message: {
role: "system",
content: [
{ type: "text", text: "SessionStart:startup hook success: hook output" },
{ type: "tool_addition", name: "Read" },
],
},
},
{
type: "api-request-blob",
message: {
role: "user",
content: [{ type: "text", text: "<system-reminder>skills list</system-reminder>" }],
},
},
];
const leakedTimeline: AgentTimelineItem[] = [
{ type: "assistant_message", text: "SessionStart:startup hook success: hook output" },
];
const mapBlocks = vi.fn().mockReturnValue(leakedTimeline);
for (const entry of entries) {
expect(convertClaudeHistoryEntry(entry, mapBlocks)).toEqual([]);
}
expect(mapBlocks).not.toHaveBeenCalled();
});
test("converts compact boundary metadata variants", () => {
const fixtures = [
{
@@ -6403,6 +6403,11 @@ function convertClaudeHistoryEntryPreamble(
if (entry.type === "user" && isSyntheticHistoryUserEntry(entry)) {
return { shortCircuit: [] };
}
// Only user and assistant records are conversation turns. Claude Code 2.1.x also journals each
// API request as `api-request-blob` records whose message replays the system context.
if (entry.type !== "user" && entry.type !== "assistant") {
return { shortCircuit: [] };
}
const message = entry?.message;
if (!message || !("content" in message)) {