fix(coding-agent): excluded cwd-less legacy sessions from relocation guard
- Guard now skips sessions with empty cwd, not just undefined.
- Prevents path.resolve("") collision from hijacking a moved session.
This commit is contained in:
@@ -3518,7 +3518,7 @@ export class SessionManager {
|
||||
// current-directory session. If that shared dir also contains an older session
|
||||
// that already belongs to the current cwd, prefer that local session instead
|
||||
// of re-rooting the stale breadcrumb over it.
|
||||
const resolvedBreadcrumbCwd = breadcrumbCwd ?? path.resolve(breadcrumb.cwd);
|
||||
const resolvedBreadcrumbCwd = path.resolve(breadcrumb.cwd);
|
||||
mostRecent = await findMostRecentSession(dir, storage);
|
||||
const sourceCwdGone = !fs.existsSync(resolvedBreadcrumbCwd);
|
||||
const breadcrumbSessionFile = path.resolve(breadcrumb.sessionFile);
|
||||
@@ -3529,7 +3529,7 @@ export class SessionManager {
|
||||
const currentCwdSession = (await SessionManager.list(cwd, dir, storage)).find(
|
||||
session =>
|
||||
path.resolve(session.path) !== breadcrumbSessionFile &&
|
||||
session.cwd !== undefined &&
|
||||
session.cwd &&
|
||||
path.resolve(session.cwd) === resolvedCwd,
|
||||
);
|
||||
if (currentCwdSession) {
|
||||
|
||||
@@ -30,6 +30,17 @@ function writeBreadcrumb(cwd: string, sessionFile: string): string {
|
||||
return file;
|
||||
}
|
||||
|
||||
function stripHeaderCwd(file: string): void {
|
||||
const lines = fs.readFileSync(file, "utf8").split("\n");
|
||||
const rewritten = lines.map(line => {
|
||||
if (!line.trim()) return line;
|
||||
const obj = JSON.parse(line) as { type?: string; cwd?: unknown };
|
||||
if (obj.type === "session") delete obj.cwd;
|
||||
return JSON.stringify(obj);
|
||||
});
|
||||
fs.writeFileSync(file, rewritten.join("\n"));
|
||||
}
|
||||
|
||||
describe("SessionManager.continueRecent relocation", () => {
|
||||
let testAgentDir: string;
|
||||
let cwdA: string;
|
||||
@@ -230,4 +241,47 @@ describe("SessionManager.continueRecent relocation", () => {
|
||||
await resumed.close();
|
||||
}
|
||||
});
|
||||
|
||||
it("re-roots past a cwd-less legacy session in a shared explicit sessionDir", async () => {
|
||||
// Regression: SessionInfo.cwd is "" for sessions whose header has no cwd, and
|
||||
// path.resolve("") === process.cwd(). A guard that only excluded `undefined`
|
||||
// treated such a legacy session as "belongs to the current cwd" whenever
|
||||
// --continue ran from process.cwd(), hijacking the moved session. Resume must
|
||||
// be invoked with process.cwd() to reproduce the path.resolve("") collision.
|
||||
const explicitSessionDir = path.join(testAgentDir, "shared-legacy-sessions");
|
||||
const currentCwd = process.cwd();
|
||||
|
||||
// Older session with no recorded cwd (header cwd stripped → "" on load).
|
||||
const legacy = SessionManager.create(cwdB, explicitSessionDir);
|
||||
legacy.appendMessage({ role: "user", content: "legacy cwd-less", timestamp: 1 });
|
||||
legacy.appendMessage(makeAssistantMessage());
|
||||
await legacy.flush();
|
||||
const legacyFile = legacy.getSessionFile();
|
||||
if (!legacyFile) throw new Error("Expected persisted legacy session file");
|
||||
await legacy.close();
|
||||
stripHeaderCwd(legacyFile);
|
||||
|
||||
// Newer moved session, recorded under the now-missing worktree cwd.
|
||||
await new Promise(resolve => setTimeout(resolve, 20));
|
||||
const moved = SessionManager.create(cwdA, explicitSessionDir);
|
||||
moved.appendMessage({ role: "user", content: "newer moved cwd", timestamp: 2 });
|
||||
moved.appendMessage(makeAssistantMessage());
|
||||
await moved.flush();
|
||||
const movedFile = moved.getSessionFile();
|
||||
if (!movedFile) throw new Error("Expected persisted moved session file");
|
||||
await moved.close();
|
||||
|
||||
writeBreadcrumb(cwdA, movedFile);
|
||||
await fsp.rm(cwdA, { recursive: true, force: true });
|
||||
|
||||
const resumed = await SessionManager.continueRecent(currentCwd, explicitSessionDir);
|
||||
try {
|
||||
// The moved session is re-rooted; the cwd-less legacy session is not hijacked.
|
||||
expect(resumed.getSessionFile()).toBe(movedFile);
|
||||
expect(resumed.getCwd()).toBe(path.resolve(currentCwd));
|
||||
expect(fs.existsSync(legacyFile)).toBe(true);
|
||||
} finally {
|
||||
await resumed.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user