fix(coding-agent): handle missing session cwd

This commit is contained in:
Mario Zechner
2026-04-05 21:24:15 +02:00
parent 127547f2b3
commit 080af6fc06
6 changed files with 259 additions and 12 deletions

View File

@@ -5,6 +5,7 @@ import type { AgentSessionRuntimeDiagnostic, AgentSessionServices } from "./agen
import type { SessionStartEvent } from "./extensions/index.js";
import { emitSessionShutdownEvent } from "./extensions/runner.js";
import type { CreateAgentSessionResult } from "./sdk.js";
import { assertSessionCwdExists } from "./session-cwd.js";
import { SessionManager } from "./session-manager.js";
/**
@@ -124,14 +125,15 @@ export class AgentSessionRuntime {
this._modelFallbackMessage = result.modelFallbackMessage;
}
async switchSession(sessionPath: string): Promise<{ cancelled: boolean }> {
async switchSession(sessionPath: string, cwdOverride?: string): Promise<{ cancelled: boolean }> {
const beforeResult = await this.emitBeforeSwitch("resume", sessionPath);
if (beforeResult.cancelled) {
return beforeResult;
}
const previousSessionFile = this.session.sessionFile;
const sessionManager = SessionManager.open(sessionPath);
const sessionManager = SessionManager.open(sessionPath, undefined, cwdOverride);
assertSessionCwdExists(sessionManager, this.cwd);
await this.teardownCurrent();
this.apply(
await this.createRuntime({
@@ -246,7 +248,7 @@ export class AgentSessionRuntime {
return { cancelled: false, selectedText };
}
async importFromJsonl(inputPath: string): Promise<{ cancelled: boolean }> {
async importFromJsonl(inputPath: string, cwdOverride?: string): Promise<{ cancelled: boolean }> {
const resolvedPath = resolve(inputPath);
if (!existsSync(resolvedPath)) {
throw new Error(`File not found: ${resolvedPath}`);
@@ -268,7 +270,8 @@ export class AgentSessionRuntime {
copyFileSync(resolvedPath, destinationPath);
}
const sessionManager = SessionManager.open(destinationPath, sessionDir);
const sessionManager = SessionManager.open(destinationPath, sessionDir, cwdOverride);
assertSessionCwdExists(sessionManager, this.cwd);
await this.teardownCurrent();
this.apply(
await this.createRuntime({
@@ -302,6 +305,7 @@ export async function createAgentSessionRuntime(
sessionStartEvent?: SessionStartEvent;
},
): Promise<AgentSessionRuntime> {
assertSessionCwdExists(options.sessionManager, options.cwd);
const result = await createRuntime(options);
if (process.cwd() !== result.services.cwd) {
process.chdir(result.services.cwd);

View File

@@ -0,0 +1,59 @@
import { existsSync } from "node:fs";
export interface SessionCwdIssue {
sessionFile?: string;
sessionCwd: string;
fallbackCwd: string;
}
interface SessionCwdSource {
getCwd(): string;
getSessionFile(): string | undefined;
}
export function getMissingSessionCwdIssue(
sessionManager: SessionCwdSource,
fallbackCwd: string,
): SessionCwdIssue | undefined {
const sessionFile = sessionManager.getSessionFile();
if (!sessionFile) {
return undefined;
}
const sessionCwd = sessionManager.getCwd();
if (!sessionCwd || existsSync(sessionCwd)) {
return undefined;
}
return {
sessionFile,
sessionCwd,
fallbackCwd,
};
}
export function formatMissingSessionCwdError(issue: SessionCwdIssue): string {
const sessionFile = issue.sessionFile ? `\nSession file: ${issue.sessionFile}` : "";
return `Stored session working directory does not exist: ${issue.sessionCwd}${sessionFile}\nCurrent working directory: ${issue.fallbackCwd}`;
}
export function formatMissingSessionCwdPrompt(issue: SessionCwdIssue): string {
return `cwd from session file does not exist\n${issue.sessionCwd}\n\ncontinue in current cwd\n${issue.fallbackCwd}`;
}
export class MissingSessionCwdError extends Error {
readonly issue: SessionCwdIssue;
constructor(issue: SessionCwdIssue) {
super(formatMissingSessionCwdError(issue));
this.name = "MissingSessionCwdError";
this.issue = issue;
}
}
export function assertSessionCwdExists(sessionManager: SessionCwdSource, fallbackCwd: string): void {
const issue = getMissingSessionCwdIssue(sessionManager, fallbackCwd);
if (issue) {
throw new MissingSessionCwdError(issue);
}
}

View File

@@ -1270,12 +1270,13 @@ export class SessionManager {
* Open a specific session file.
* @param path Path to session file
* @param sessionDir Optional session directory for /new or /branch. If omitted, derives from file's parent.
* @param cwdOverride Optional cwd override instead of the session header cwd.
*/
static open(path: string, sessionDir?: string): SessionManager {
static open(path: string, sessionDir?: string, cwdOverride?: string): SessionManager {
// Extract cwd from session header if possible, otherwise use process.cwd()
const entries = loadEntriesFromFile(path);
const header = entries.find((e) => e.type === "session") as SessionHeader | undefined;
const cwd = header?.cwd ?? process.cwd();
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);