fix(coding-agent): scope custom session dir lookups

This commit is contained in:
Armin Ronacher
2026-05-28 23:24:00 +02:00
parent b64f3f5eae
commit 4b4641c6b0
5 changed files with 146 additions and 17 deletions

View File

@@ -433,11 +433,15 @@ export function buildSessionContext(
* Compute the default session directory for a cwd.
* Encodes cwd into a safe directory name under ~/.pi/agent/sessions/.
*/
export function getDefaultSessionDir(cwd: string, agentDir: string = getDefaultAgentDir()): string {
function getDefaultSessionDirPath(cwd: string, agentDir: string = getDefaultAgentDir()): string {
const resolvedCwd = resolvePath(cwd);
const resolvedAgentDir = resolvePath(agentDir);
const safePath = `--${resolvedCwd.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-")}--`;
const sessionDir = join(resolvedAgentDir, "sessions", safePath);
return join(resolvedAgentDir, "sessions", safePath);
}
export function getDefaultSessionDir(cwd: string, agentDir: string = getDefaultAgentDir()): string {
const sessionDir = getDefaultSessionDirPath(cwd, agentDir);
if (!existsSync(sessionDir)) {
mkdirSync(sessionDir, { recursive: true });
}
@@ -473,30 +477,48 @@ export function loadEntriesFromFile(filePath: string): FileEntry[] {
return entries;
}
function isValidSessionFile(filePath: string): boolean {
function readSessionHeader(filePath: string): SessionHeader | null {
try {
const fd = openSync(filePath, "r");
const buffer = Buffer.alloc(512);
const bytesRead = readSync(fd, buffer, 0, 512, 0);
closeSync(fd);
const firstLine = buffer.toString("utf8", 0, bytesRead).split("\n")[0];
if (!firstLine) return false;
const header = JSON.parse(firstLine);
return header.type === "session" && typeof header.id === "string";
if (!firstLine) return null;
const header = JSON.parse(firstLine) as Record<string, unknown>;
if (header.type !== "session" || typeof header.id !== "string") {
return null;
}
return header as unknown as SessionHeader;
} catch {
return false;
return null;
}
}
function getSessionHeaderCwd(header: SessionHeader): string | undefined {
const cwd = (header as { cwd?: unknown }).cwd;
return typeof cwd === "string" ? cwd : undefined;
}
function sessionCwdMatches(cwd: string | undefined, resolvedCwd: string): boolean {
return cwd !== undefined && cwd !== "" && resolvePath(cwd) === resolvedCwd;
}
/** Exported for testing */
export function findMostRecentSession(sessionDir: string): string | null {
export function findMostRecentSession(sessionDir: string, cwd?: string): string | null {
const resolvedSessionDir = normalizePath(sessionDir);
const resolvedCwd = cwd ? resolvePath(cwd) : undefined;
try {
const files = readdirSync(resolvedSessionDir)
.filter((f) => f.endsWith(".jsonl"))
.map((f) => join(resolvedSessionDir, f))
.filter(isValidSessionFile)
.map((path) => ({ path, mtime: statSync(path).mtime }))
.map((path) => ({ path, header: readSessionHeader(path) }))
.filter(
(file): file is { path: string; header: SessionHeader } =>
file.header !== null &&
(!resolvedCwd || sessionCwdMatches(getSessionHeaderCwd(file.header), resolvedCwd)),
)
.map(({ path }) => ({ path, mtime: statSync(path).mtime }))
.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
return files[0]?.path || null;
@@ -849,6 +871,10 @@ export class SessionManager {
return this.sessionDir;
}
usesDefaultSessionDir(): boolean {
return this.sessionDir === getDefaultSessionDirPath(this.cwd);
}
getSessionId(): string {
return this.sessionId;
}
@@ -1363,7 +1389,8 @@ export class SessionManager {
*/
static continueRecent(cwd: string, sessionDir?: string): SessionManager {
const dir = sessionDir ? normalizePath(sessionDir) : getDefaultSessionDir(cwd);
const mostRecent = findMostRecentSession(dir);
const filterCwd = sessionDir !== undefined && dir !== getDefaultSessionDirPath(cwd);
const mostRecent = findMostRecentSession(dir, filterCwd ? cwd : undefined);
if (mostRecent) {
return new SessionManager(cwd, dir, mostRecent, true);
}
@@ -1443,7 +1470,11 @@ export class SessionManager {
*/
static async list(cwd: string, sessionDir?: string, onProgress?: SessionListProgress): Promise<SessionInfo[]> {
const dir = sessionDir ? normalizePath(sessionDir) : getDefaultSessionDir(cwd);
const sessions = await listSessionsFromDir(dir, onProgress);
const filterCwd = sessionDir !== undefined && dir !== getDefaultSessionDirPath(cwd);
const resolvedCwd = resolvePath(cwd);
const sessions = (await listSessionsFromDir(dir, onProgress)).filter(
(session) => !filterCwd || sessionCwdMatches(session.cwd, resolvedCwd),
);
sessions.sort((a, b) => b.modified.getTime() - a.modified.getTime());
return sessions;
}
@@ -1452,7 +1483,21 @@ export class SessionManager {
* List all sessions across all project directories.
* @param onProgress Optional callback for progress updates (loaded, total)
*/
static async listAll(onProgress?: SessionListProgress): Promise<SessionInfo[]> {
static async listAll(onProgress?: SessionListProgress): Promise<SessionInfo[]>;
static async listAll(sessionDir?: string, onProgress?: SessionListProgress): Promise<SessionInfo[]>;
static async listAll(
sessionDirOrOnProgress?: string | SessionListProgress,
onProgress?: SessionListProgress,
): Promise<SessionInfo[]> {
const customSessionDir =
typeof sessionDirOrOnProgress === "string" ? normalizePath(sessionDirOrOnProgress) : undefined;
const progress = typeof sessionDirOrOnProgress === "function" ? sessionDirOrOnProgress : onProgress;
if (customSessionDir) {
const sessions = await listSessionsFromDir(customSessionDir, progress);
sessions.sort((a, b) => b.modified.getTime() - a.modified.getTime());
return sessions;
}
const sessionsDir = getSessionsDir();
try {
@@ -1482,7 +1527,7 @@ export class SessionManager {
const results = await buildSessionInfosWithConcurrency(allFiles, () => {
loaded++;
onProgress?.(loaded, totalFiles);
progress?.(loaded, totalFiles);
});
for (const info of results) {

View File

@@ -171,7 +171,7 @@ async function resolveSessionPath(sessionArg: string, cwd: string, sessionDir?:
}
// Try global search across all projects
const allSessions = await SessionManager.listAll();
const allSessions = await SessionManager.listAll(sessionDir);
const globalMatch =
allSessions.find((s) => s.id === sessionArg) ?? allSessions.find((s) => s.id.startsWith(sessionArg));
@@ -309,7 +309,7 @@ async function createSessionManager(
try {
const selectedPath = await selectSession(
(onProgress) => SessionManager.list(cwd, sessionDir, onProgress),
SessionManager.listAll,
(onProgress) => SessionManager.listAll(sessionDir, onProgress),
);
if (!selectedPath) {
console.log(chalk.dim("No session selected"));

View File

@@ -4401,7 +4401,10 @@ export class InteractiveMode {
const selector = new SessionSelectorComponent(
(onProgress) =>
SessionManager.list(this.sessionManager.getCwd(), this.sessionManager.getSessionDir(), onProgress),
SessionManager.listAll,
(onProgress) =>
this.sessionManager.usesDefaultSessionDir()
? SessionManager.listAll(onProgress)
: SessionManager.listAll(this.sessionManager.getSessionDir(), onProgress),
async (sessionPath) => {
done();
await this.handleResumeSession(sessionPath);