diff --git a/packages/server/src/server/agent/providers/claude/agent.history-load.test.ts b/packages/server/src/server/agent/providers/claude/agent.history-load.test.ts new file mode 100644 index 0000000000..223e2001f5 --- /dev/null +++ b/packages/server/src/server/agent/providers/claude/agent.history-load.test.ts @@ -0,0 +1,93 @@ +import { mkdirSync, mkdtempSync, rmSync } from "node:fs"; +import * as os from "node:os"; +import * as path from "node:path"; +import pino from "pino"; +import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; + +import { ClaudeAgentClient } from "./agent.js"; +import { claudeProjectDirSync } from "./project-dir.js"; + +/** + * A resumed Claude agent whose transcript is missing or unreadable opens with an empty timeline. + * The daemon log at its default level has to say which, and where it looked. + */ + +const SESSION_ID = "history-load-session"; + +interface LogRecord { + level: number; + msg: string; + sessionId?: string; + historyPath?: string; + err?: { message?: string }; +} + +describe("ClaudeAgentSession persisted history load", () => { + let tempRoot: string; + let cwd: string; + let configDir: string; + let records: LogRecord[]; + + async function resume(): Promise { + const client = new ClaudeAgentClient({ + logger: pino({ level: "info" }, { write: (line: string) => records.push(JSON.parse(line)) }), + queryFactory: vi.fn(() => { + throw new Error("history load must not start a query"); + }), + resolveVersion: async () => "2.1.220", + }); + const session = await client.resumeSession( + { provider: "claude", sessionId: SESSION_ID }, + { cwd }, + ); + await session.close(); + } + + function transcriptPath(): string { + return path.join(claudeProjectDirSync(cwd, { configDir }), `${SESSION_ID}.jsonl`); + } + + beforeEach(() => { + tempRoot = mkdtempSync(path.join(os.tmpdir(), "claude-history-load-")); + cwd = path.join(tempRoot, "repo"); + configDir = path.join(tempRoot, "claude-config"); + mkdirSync(cwd, { recursive: true }); + vi.stubEnv("CLAUDE_CONFIG_DIR", configDir); + records = []; + }); + + afterEach(() => { + vi.unstubAllEnvs(); + rmSync(tempRoot, { recursive: true, force: true }); + }); + + test("logs the resolved transcript path when the transcript does not exist", async () => { + await resume(); + + expect(records).toContainEqual( + expect.objectContaining({ + level: pino.levels.values.info, + msg: "No Claude transcript to load history from", + sessionId: SESSION_ID, + historyPath: transcriptPath(), + }), + ); + }); + + test("logs a warning with the error when the transcript cannot be read", async () => { + // A directory at the transcript path exists but cannot be read as a file. + mkdirSync(transcriptPath(), { recursive: true }); + + await resume(); + + expect(records).toContainEqual( + expect.objectContaining({ + level: pino.levels.values.warn, + msg: "Failed to load Claude history from transcript", + sessionId: SESSION_ID, + historyPath: transcriptPath(), + err: expect.objectContaining({ message: expect.stringContaining("EISDIR") }), + }), + ); + }); +}); diff --git a/packages/server/src/server/agent/providers/claude/agent.ts b/packages/server/src/server/agent/providers/claude/agent.ts index b6a366370b..e5153e2102 100644 --- a/packages/server/src/server/agent/providers/claude/agent.ts +++ b/packages/server/src/server/agent/providers/claude/agent.ts @@ -4881,10 +4881,15 @@ class ClaudeAgentSession implements AgentSession { } private loadPersistedHistory(sessionId: string): void { + let historyPath: string | null = null; try { this.taskState.reset(); - const historyPath = this.resolveHistoryPath(sessionId); + historyPath = this.resolveHistoryPath(sessionId); if (!historyPath || !fs.existsSync(historyPath)) { + this.logger.info( + { sessionId, cwd: this.config.cwd, historyPath }, + "No Claude transcript to load history from", + ); return; } const content = fs.readFileSync(historyPath, "utf8"); @@ -4893,8 +4898,11 @@ class ClaudeAgentSession implements AgentSession { readClaudeSidechainHistory(historyPath), ); this.ingestPersistedHistory(content, replay); - } catch { - // ignore history load failures + } catch (error) { + this.logger.warn( + { err: error, sessionId, historyPath }, + "Failed to load Claude history from transcript", + ); } }