fix(coding-agent): Clean up Path Handling (#4873)

This commit is contained in:
Armin Ronacher
2026-05-22 11:25:15 +02:00
committed by GitHub
parent bf56a86e1e
commit c100620bf4
23 changed files with 363 additions and 214 deletions

View File

@@ -16,6 +16,7 @@ import {
import { readdir, readFile, stat } from "fs/promises";
import { join, resolve } from "path";
import { getAgentDir as getDefaultAgentDir, getSessionsDir } from "../config.ts";
import { normalizePath, resolvePath } from "../utils/paths.ts";
import {
type BashExecutionMessage,
type CustomMessage,
@@ -425,8 +426,10 @@ export function buildSessionContext(
* Encodes cwd into a safe directory name under ~/.pi/agent/sessions/.
*/
export function getDefaultSessionDir(cwd: string, agentDir: string = getDefaultAgentDir()): string {
const safePath = `--${cwd.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-")}--`;
const sessionDir = join(agentDir, "sessions", safePath);
const resolvedCwd = resolvePath(cwd);
const resolvedAgentDir = resolvePath(agentDir);
const safePath = `--${resolvedCwd.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-")}--`;
const sessionDir = join(resolvedAgentDir, "sessions", safePath);
if (!existsSync(sessionDir)) {
mkdirSync(sessionDir, { recursive: true });
}
@@ -435,9 +438,10 @@ export function getDefaultSessionDir(cwd: string, agentDir: string = getDefaultA
/** Exported for testing */
export function loadEntriesFromFile(filePath: string): FileEntry[] {
if (!existsSync(filePath)) return [];
const resolvedFilePath = normalizePath(filePath);
if (!existsSync(resolvedFilePath)) return [];
const content = readFileSync(filePath, "utf8");
const content = readFileSync(resolvedFilePath, "utf8");
const entries: FileEntry[] = [];
const lines = content.trim().split("\n");
@@ -478,10 +482,11 @@ function isValidSessionFile(filePath: string): boolean {
/** Exported for testing */
export function findMostRecentSession(sessionDir: string): string | null {
const resolvedSessionDir = normalizePath(sessionDir);
try {
const files = readdirSync(sessionDir)
const files = readdirSync(resolvedSessionDir)
.filter((f) => f.endsWith(".jsonl"))
.map((f) => join(sessionDir, f))
.map((f) => join(resolvedSessionDir, f))
.filter(isValidSessionFile)
.map((path) => ({ path, mtime: statSync(path).mtime }))
.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
@@ -717,11 +722,11 @@ export class SessionManager {
private leafId: string | null = null;
private constructor(cwd: string, sessionDir: string, sessionFile: string | undefined, persist: boolean) {
this.cwd = cwd;
this.sessionDir = sessionDir;
this.cwd = resolvePath(cwd);
this.sessionDir = normalizePath(sessionDir);
this.persist = persist;
if (persist && sessionDir && !existsSync(sessionDir)) {
mkdirSync(sessionDir, { recursive: true });
if (persist && this.sessionDir && !existsSync(this.sessionDir)) {
mkdirSync(this.sessionDir, { recursive: true });
}
if (sessionFile) {
@@ -733,7 +738,7 @@ export class SessionManager {
/** Switch to a different session file (used for resume and branching) */
setSessionFile(sessionFile: string): void {
this.sessionFile = resolve(sessionFile);
this.sessionFile = resolvePath(sessionFile);
if (existsSync(this.sessionFile)) {
this.fileEntries = loadEntriesFromFile(this.sessionFile);
@@ -1304,7 +1309,7 @@ export class SessionManager {
* @param sessionDir Optional session directory. If omitted, uses default (~/.pi/agent/sessions/<encoded-cwd>/).
*/
static create(cwd: string, sessionDir?: string): SessionManager {
const dir = sessionDir ?? getDefaultSessionDir(cwd);
const dir = sessionDir ? normalizePath(sessionDir) : getDefaultSessionDir(cwd);
return new SessionManager(cwd, dir, undefined, true);
}
@@ -1315,13 +1320,14 @@ export class SessionManager {
* @param cwdOverride Optional cwd override instead of the session header cwd.
*/
static open(path: string, sessionDir?: string, cwdOverride?: string): SessionManager {
const resolvedPath = resolvePath(path);
// Extract cwd from session header if possible, otherwise use process.cwd()
const entries = loadEntriesFromFile(path);
const entries = loadEntriesFromFile(resolvedPath);
const header = entries.find((e) => e.type === "session") as SessionHeader | undefined;
const cwd = cwdOverride ?? header?.cwd ?? process.cwd();
// If no sessionDir provided, derive from file's parent directory
const dir = sessionDir ?? resolve(path, "..");
return new SessionManager(cwd, dir, path, true);
const dir = sessionDir ? normalizePath(sessionDir) : resolve(resolvedPath, "..");
return new SessionManager(cwd, dir, resolvedPath, true);
}
/**
@@ -1330,7 +1336,7 @@ export class SessionManager {
* @param sessionDir Optional session directory. If omitted, uses default (~/.pi/agent/sessions/<encoded-cwd>/).
*/
static continueRecent(cwd: string, sessionDir?: string): SessionManager {
const dir = sessionDir ?? getDefaultSessionDir(cwd);
const dir = sessionDir ? normalizePath(sessionDir) : getDefaultSessionDir(cwd);
const mostRecent = findMostRecentSession(dir);
if (mostRecent) {
return new SessionManager(cwd, dir, mostRecent, true);
@@ -1351,17 +1357,19 @@ export class SessionManager {
* @param sessionDir Optional session directory. If omitted, uses default for targetCwd.
*/
static forkFrom(sourcePath: string, targetCwd: string, sessionDir?: string): SessionManager {
const sourceEntries = loadEntriesFromFile(sourcePath);
const resolvedSourcePath = resolvePath(sourcePath);
const resolvedTargetCwd = resolvePath(targetCwd);
const sourceEntries = loadEntriesFromFile(resolvedSourcePath);
if (sourceEntries.length === 0) {
throw new Error(`Cannot fork: source session file is empty or invalid: ${sourcePath}`);
throw new Error(`Cannot fork: source session file is empty or invalid: ${resolvedSourcePath}`);
}
const sourceHeader = sourceEntries.find((e) => e.type === "session") as SessionHeader | undefined;
if (!sourceHeader) {
throw new Error(`Cannot fork: source session has no header: ${sourcePath}`);
throw new Error(`Cannot fork: source session has no header: ${resolvedSourcePath}`);
}
const dir = sessionDir ?? getDefaultSessionDir(targetCwd);
const dir = sessionDir ? normalizePath(sessionDir) : getDefaultSessionDir(resolvedTargetCwd);
if (!existsSync(dir)) {
mkdirSync(dir, { recursive: true });
}
@@ -1378,8 +1386,8 @@ export class SessionManager {
version: CURRENT_SESSION_VERSION,
id: newSessionId,
timestamp,
cwd: targetCwd,
parentSession: sourcePath,
cwd: resolvedTargetCwd,
parentSession: resolvedSourcePath,
};
appendFileSync(newSessionFile, `${JSON.stringify(newHeader)}\n`);
@@ -1390,7 +1398,7 @@ export class SessionManager {
}
}
return new SessionManager(targetCwd, dir, newSessionFile, true);
return new SessionManager(resolvedTargetCwd, dir, newSessionFile, true);
}
/**
@@ -1400,7 +1408,7 @@ export class SessionManager {
* @param onProgress Optional callback for progress updates (loaded, total)
*/
static async list(cwd: string, sessionDir?: string, onProgress?: SessionListProgress): Promise<SessionInfo[]> {
const dir = sessionDir ?? getDefaultSessionDir(cwd);
const dir = sessionDir ? normalizePath(sessionDir) : getDefaultSessionDir(cwd);
const sessions = await listSessionsFromDir(dir, onProgress);
sessions.sort((a, b) => b.modified.getTime() - a.modified.getTime());
return sessions;