mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-23 09:42:53 +00:00
Keep Claude request context out of restored chats (#7590)
* 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> * fix(claude): keep request context out of restored chats Claude request-journal records contain message-shaped tool context. Only conversation records should enter the ordinary history mapper, after compaction and background task notifications are handled. Co-authored-by: Kheva Mann <khevamann@users.noreply.github.com> Co-authored-by: rogerluan <rogerluan@users.noreply.github.com> --------- Co-authored-by: Roger Oba <rogerluan.oba@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com> Co-authored-by: Kheva Mann <khevamann@users.noreply.github.com> Co-authored-by: rogerluan <rogerluan@users.noreply.github.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
Kheva Mann
rogerluan
Roger Oba
parent
e522e6ea8e
commit
ce67efa9e7
@@ -0,0 +1,140 @@
|
||||
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, test } from "vitest";
|
||||
|
||||
import type { AgentTimelineItem } from "../../agent-sdk-types.js";
|
||||
import { createTestLogger } from "../../../../test-utils/test-logger.js";
|
||||
import { ClaudeAgentClient } from "./agent.js";
|
||||
import { claudeProjectDirSync } from "./project-dir.js";
|
||||
|
||||
describe("Claude session request journal replay", () => {
|
||||
let root: string;
|
||||
let cwd: string;
|
||||
let configDir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
root = mkdtempSync(path.join(os.tmpdir(), "claude-request-journal-"));
|
||||
cwd = path.join(root, "repo");
|
||||
configDir = path.join(root, "claude");
|
||||
mkdirSync(cwd);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
async function replay(records: unknown[]): Promise<AgentTimelineItem[]> {
|
||||
const historyDir = claudeProjectDirSync(cwd, { configDir });
|
||||
mkdirSync(historyDir, { recursive: true });
|
||||
writeFileSync(
|
||||
path.join(historyDir, "journal-session.jsonl"),
|
||||
records.map((record) => JSON.stringify(record)).join("\n"),
|
||||
);
|
||||
const client = new ClaudeAgentClient({ logger: createTestLogger() });
|
||||
const session = await client.resumeSession(
|
||||
{ provider: "claude", sessionId: "journal-session" },
|
||||
{ cwd },
|
||||
{ env: { CLAUDE_CONFIG_DIR: configDir } },
|
||||
);
|
||||
try {
|
||||
const items: AgentTimelineItem[] = [];
|
||||
for await (const event of session.streamHistory()) {
|
||||
if (event.type === "timeline") items.push(event.item);
|
||||
}
|
||||
return items;
|
||||
} finally {
|
||||
await session.close();
|
||||
}
|
||||
}
|
||||
|
||||
test.each(["api-request-shape", "api-request", "api-request-blob", "future-journal-record"])(
|
||||
"ignores %s content while preserving the conversation and tool calls",
|
||||
async (type) => {
|
||||
const items = await replay([
|
||||
{ type: "user", message: { role: "user", content: "Read package.json" } },
|
||||
{
|
||||
type: "assistant",
|
||||
message: {
|
||||
role: "assistant",
|
||||
content: [
|
||||
{
|
||||
type: "tool_use",
|
||||
id: "read-1",
|
||||
name: "Read",
|
||||
input: { file_path: "package.json" },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
...["user", "system", "assistant"].map((role) => ({
|
||||
type,
|
||||
message: {
|
||||
role,
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "SessionStart hook output\n# MCP Server Instructions\n<total_tokens>1000 tokens left</total_tokens>",
|
||||
},
|
||||
{
|
||||
type: "tool_addition",
|
||||
tool: {
|
||||
name: "deferred_tool",
|
||||
description: "Deferred tool",
|
||||
input_schema: { type: "object", properties: {} },
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "tool_use",
|
||||
id: "journal-only",
|
||||
name: "Read",
|
||||
input: { file_path: "old.json" },
|
||||
},
|
||||
],
|
||||
},
|
||||
})),
|
||||
{
|
||||
type: "assistant",
|
||||
message: { role: "assistant", content: [{ type: "text", text: "paseo" }] },
|
||||
},
|
||||
]);
|
||||
|
||||
expect(items).toEqual([
|
||||
expect.objectContaining({ type: "user_message", text: "Read package.json" }),
|
||||
expect.objectContaining({ type: "tool_call", callId: "read-1", name: "Read" }),
|
||||
expect.objectContaining({ type: "assistant_message", text: "paseo" }),
|
||||
]);
|
||||
},
|
||||
);
|
||||
|
||||
test("preserves system compaction and background task notifications", async () => {
|
||||
const items = await replay([
|
||||
{
|
||||
type: "system",
|
||||
subtype: "compact_boundary",
|
||||
compactMetadata: { trigger: "manual", preTokens: 1000 },
|
||||
},
|
||||
{
|
||||
type: "system",
|
||||
subtype: "task_notification",
|
||||
uuid: "note-1",
|
||||
task_id: "task-1",
|
||||
status: "completed",
|
||||
summary: "Background work finished",
|
||||
},
|
||||
]);
|
||||
expect(items).toEqual([
|
||||
expect.objectContaining({
|
||||
type: "compaction",
|
||||
status: "completed",
|
||||
trigger: "manual",
|
||||
preTokens: 1000,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
type: "tool_call",
|
||||
name: "task_notification",
|
||||
status: "completed",
|
||||
}),
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -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)) {
|
||||
|
||||
Reference in New Issue
Block a user