diff --git a/packages/server/src/server/agent/providers/claude/agent.request-journal.test.ts b/packages/server/src/server/agent/providers/claude/agent.request-journal.test.ts new file mode 100644 index 0000000000..9be65da964 --- /dev/null +++ b/packages/server/src/server/agent/providers/claude/agent.request-journal.test.ts @@ -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 { + 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\n1000 tokens left", + }, + { + 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", + }), + ]); + }); +}); diff --git a/packages/server/src/server/agent/providers/claude/agent.ts b/packages/server/src/server/agent/providers/claude/agent.ts index 6a70d28d6a..120bda11c9 100644 --- a/packages/server/src/server/agent/providers/claude/agent.ts +++ b/packages/server/src/server/agent/providers/claude/agent.ts @@ -6397,6 +6397,11 @@ function convertClaudeHistoryEntryPreamble( return { shortCircuit: [taskNotificationItem] }; } + // Request-journal records carry message-shaped context, not conversation turns. + if (entry.type !== "user" && entry.type !== "assistant") { + return { shortCircuit: [] }; + } + if (entry.isCompactSummary) { return { shortCircuit: [] }; }