fix(coding-agent): scope custom session dir lookups
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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"));
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user