More permissive load session (#301)

Adds a lookup that checks the expected cwd path first, then
scans all project directories to locate the session JSONL file.
Ultimately matches the behavior we allow for resume
This commit is contained in:
Ben Brandt
2026-02-10 13:36:02 +01:00
committed by GitHub
parent ebdd115b76
commit ea00a4014a
2 changed files with 71 additions and 5 deletions

View File

@@ -294,10 +294,49 @@ export class ClaudeAcpAgent implements Agent {
return response;
}
async loadSession(params: LoadSessionRequest): Promise<LoadSessionResponse> {
/**
* Find a session file by ID, first checking the given cwd's project directory,
* then falling back to scanning all project directories.
* Returns the absolute file path if found, or null if not found.
*/
private async findSessionFile(sessionId: string, cwd: string): Promise<string | null> {
const fileName = `${sessionId}.jsonl`;
// Fast path: check the expected location based on cwd
const expectedPath = sessionFilePath(cwd, sessionId);
try {
await fs.promises.access(sessionFilePath(params.cwd, params.sessionId));
await fs.promises.access(expectedPath);
return expectedPath;
} catch {
// Not found at expected path, scan all project directories
}
const claudeDir = path.join(CLAUDE_CONFIG_DIR, "projects");
try {
const projectDirs = await fs.promises.readdir(claudeDir);
for (const encodedPath of projectDirs) {
const projectDir = path.join(claudeDir, encodedPath);
const stat = await fs.promises.stat(projectDir);
if (!stat.isDirectory()) continue;
const candidatePath = path.join(projectDir, fileName);
try {
await fs.promises.access(candidatePath);
return candidatePath;
} catch {
continue;
}
}
} catch {
// projects directory doesn't exist or isn't readable
}
return null;
}
async loadSession(params: LoadSessionRequest): Promise<LoadSessionResponse> {
const filePath = await this.findSessionFile(params.sessionId, params.cwd);
if (!filePath) {
throw new Error("Session not found");
}
@@ -312,7 +351,7 @@ export class ClaudeAcpAgent implements Agent {
},
);
await this.replaySessionHistory(params.sessionId, params.cwd);
await this.replaySessionHistory(params.sessionId, filePath);
return {
modes: response.modes,
@@ -710,8 +749,7 @@ export class ClaudeAcpAgent implements Agent {
}
}
private async replaySessionHistory(sessionId: string, cwd: string): Promise<void> {
const filePath = sessionFilePath(cwd, sessionId);
private async replaySessionHistory(sessionId: string, filePath: string): Promise<void> {
const toolUseCache: ToolUseCache = {};
const stream = fs.createReadStream(filePath, { encoding: "utf-8" });
const reader = readline.createInterface({ input: stream, crlfDelay: Infinity });

View File

@@ -230,6 +230,34 @@ describe("loadSession", () => {
});
});
it("finds session file when cwd differs from the original session cwd", async () => {
const originalCwd = "/Users/test/original-project";
const differentCwd = "/Users/test/different-project";
const sessionId = "cross-cwd-session";
// Session was created in originalCwd
writeSessionFile(originalCwd, sessionId, [
{
type: "user",
sessionId,
cwd: originalCwd,
message: { role: "user", content: "Hello from original" },
},
]);
// Load with a different cwd
await agent.loadSession({ cwd: differentCwd, sessionId, mcpServers: [] });
expect(sessionUpdates).toHaveLength(1);
expect(sessionUpdates[0]?.update).toMatchObject({
sessionUpdate: "user_message_chunk",
content: { type: "text", text: "Hello from original" },
});
expect(createSessionSpy).toHaveBeenCalledWith(expect.objectContaining({ cwd: differentCwd }), {
resume: sessionId,
});
});
it("handles Windows-style paths", async () => {
const cwd = "C:\\Users\\test\\project";
const sessionId = "win-session";