Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions src/agents/sessions/session-manager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,36 @@ describe("SessionManager.open", () => {
expect(SessionManager.continueRecent(longCwd, dir).getSessionFile()).toBe(sessionFile);
});

it("does not continue a different cwd from a colliding session directory", async () => {
const dir = await makeTempDir();
const cwdA = "/home/alice/dev/client/app";
const cwdB = "/home/alice/dev/client-app";
const sessionA = path.join(dir, "session-a.jsonl");
const sessionB = path.join(dir, "session-b.jsonl");
const headerA = buildSessionHeader(cwdA, "session-a");
const headerB = buildSessionHeader(cwdB, "session-b");

await fs.writeFile(sessionA, `${JSON.stringify(headerA)}\n`, "utf8");
await fs.writeFile(sessionB, `${JSON.stringify(headerB)}\n`, "utf8");
await fs.utimes(
sessionA,
new Date("2026-06-18T00:00:00.000Z"),
new Date("2026-06-18T00:00:00.000Z"),
);
await fs.utimes(
sessionB,
new Date("2026-06-18T00:00:01.000Z"),
new Date("2026-06-18T00:00:01.000Z"),
);

expect(findMostRecentSession(dir)).toBe(sessionB);
expect(findMostRecentSession(dir, cwdA)).toBe(sessionA);
expect(SessionManager.continueRecent(cwdA, dir).getSessionFile()).toBe(sessionA);
await expect(SessionManager.list(cwdA, dir)).resolves.toEqual([
expect.objectContaining({ path: sessionA, cwd: cwdA }),
]);
});

it("skips oversized recent session headers instead of hiding valid sessions", async () => {
const dir = await makeTempDir();
const validSessionFile = path.join(dir, "valid-session.jsonl");
Expand Down
32 changes: 22 additions & 10 deletions src/agents/sessions/session-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1171,27 +1171,34 @@ function readFirstSessionFileLine(filePath: string): string | undefined {
}
}

function isValidSessionFile(filePath: string): boolean {
function readSessionHeaderFromFile(filePath: string): SessionHeader | undefined {
try {
const firstLine = readFirstSessionFileLine(filePath);
if (!firstLine) {
return false;
return undefined;
}
const header = JSON.parse(firstLine);
return header.type === "session" && typeof header.id === "string";
if (header.type !== "session" || typeof header.id !== "string") {
return undefined;
}
return header as SessionHeader;
} catch {
return false;
return undefined;
}
}

/** Exported for testing */
export function findMostRecentSession(sessionDir: string): string | null {
export function findMostRecentSession(sessionDir: string, cwd?: string): string | null {
try {
const files = readdirSync(sessionDir)
.filter((f) => f.endsWith(".jsonl"))
.map((f) => join(sessionDir, f))
.filter(isValidSessionFile)
.map((path) => ({ path, mtime: statSync(path).mtime }))
.map((path) => ({ path, header: readSessionHeaderFromFile(path) }))
.filter(
(candidate): candidate is { path: string; header: SessionHeader } =>
candidate.header !== undefined && (cwd === undefined || candidate.header.cwd === cwd),
)
.map((candidate) => ({ path: candidate.path, mtime: statSync(candidate.path).mtime }))
.toSorted((a, b) => b.mtime.getTime() - a.mtime.getTime());

return files[0]?.path || null;
Expand Down Expand Up @@ -1348,6 +1355,10 @@ async function buildSessionInfo(filePath: string): Promise<SessionInfo | null> {
}
}

function sessionInfoMatchesCwd(info: SessionInfo, cwd: string | undefined): boolean {
return cwd === undefined || info.cwd === cwd;
}

export type SessionListProgress = (loaded: number, total: number) => void;

const MAX_CONCURRENT_SESSION_INFO_LOADS = 10;
Expand Down Expand Up @@ -1397,6 +1408,7 @@ async function listSessionsFromDir(
onProgress?: SessionListProgress,
progressOffset = 0,
progressTotal?: number,
cwd?: string,
): Promise<SessionInfo[]> {
const sessions: SessionInfo[] = [];
if (!existsSync(dir)) {
Expand All @@ -1414,7 +1426,7 @@ async function listSessionsFromDir(
onProgress?.(progressOffset + loaded, total);
});
for (const info of results) {
if (info) {
if (info && sessionInfoMatchesCwd(info, cwd)) {
sessions.push(info);
}
}
Expand Down Expand Up @@ -2921,7 +2933,7 @@ export class SessionManager {
*/
static continueRecent(cwd: string, sessionDir?: string): SessionManager {
const dir = sessionDir ?? getDefaultSessionDir(cwd);
const mostRecent = findMostRecentSession(dir);
const mostRecent = findMostRecentSession(dir, cwd);
if (mostRecent) {
return new SessionManager(cwd, dir, mostRecent, true);
}
Expand Down Expand Up @@ -2995,7 +3007,7 @@ export class SessionManager {
onProgress?: SessionListProgress,
): Promise<SessionInfo[]> {
const dir = sessionDir ?? getDefaultSessionDir(cwd);
const sessions = await listSessionsFromDir(dir, onProgress);
const sessions = await listSessionsFromDir(dir, onProgress, 0, undefined, cwd);
sessions.sort((a, b) => b.modified.getTime() - a.modified.getTime());
return sessions;
}
Expand Down
Loading