Skip to content

Commit 1e886d4

Browse files
maweibinclaude
andcommitted
fix(sessions): make session path encoding injective and verify cwd on resume (#96542)
getDefaultSessionDir replaces path separators with '-' which means "client/app" and "client-app" produce identical encoded paths, causing session cross-contamination. Fix: use percent-encoding (%2F/%5C/%3A) to distinguish separators from literal hyphens. Also add cwd verification in continueRecent so that if a session file from a colliding directory is found, the code scans remaining files for one matching the requested cwd before falling back to a fresh session. Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
1 parent 56d95b1 commit 1e886d4

1 file changed

Lines changed: 41 additions & 2 deletions

File tree

src/agents/sessions/session-manager.ts

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -437,7 +437,10 @@ export function buildSessionContext(
437437
* Encodes cwd into a safe directory name under ~/.openclaw/agent/sessions/.
438438
*/
439439
export function getDefaultSessionDir(cwd: string, agentDir: string = getDefaultAgentDir()): string {
440-
const safePath = `--${cwd.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-")}--`;
440+
const safePath = `--${cwd
441+
.replace(/^[/\\]/, "")
442+
.replace(/%/g, "%25")
443+
.replace(/[/\\:]/g, (c) => (c === "/" ? "%2F" : c === "\\" ? "%5C" : "%3A"))}--`;
441444
const sessionDir = join(agentDir, "sessions", safePath);
442445
if (!existsSync(sessionDir)) {
443446
mkdirSync(sessionDir, { recursive: true });
@@ -1200,6 +1203,35 @@ export function findMostRecentSession(sessionDir: string): string | null {
12001203
}
12011204
}
12021205

1206+
function readSessionCwd(filePath: string): string | undefined {
1207+
try {
1208+
const firstLine = readFirstSessionFileLine(filePath);
1209+
if (!firstLine) {
1210+
return undefined;
1211+
}
1212+
const parsed = JSON.parse(firstLine);
1213+
return parsed.type === "session" ? parsed.cwd : undefined;
1214+
} catch {
1215+
return undefined;
1216+
}
1217+
}
1218+
1219+
function findMostRecentSessionByCwd(sessionDir: string, cwd: string): string | null {
1220+
try {
1221+
const normalizedCwd = resolve(cwd);
1222+
const files = readdirSync(sessionDir)
1223+
.filter((f) => f.endsWith(".jsonl"))
1224+
.map((f) => join(sessionDir, f))
1225+
.filter(isValidSessionFile)
1226+
.map((path) => ({ path, mtime: statSync(path).mtime, sessionCwd: readSessionCwd(path) }))
1227+
.filter((f) => f.sessionCwd !== undefined && resolve(f.sessionCwd) === normalizedCwd)
1228+
.toSorted((a, b) => b.mtime.getTime() - a.mtime.getTime());
1229+
return files[0]?.path || null;
1230+
} catch {
1231+
return null;
1232+
}
1233+
}
1234+
12031235
function isMessageWithContent(message: AgentMessage): message is Message {
12041236
return typeof (message as Message).role === "string" && "content" in message;
12051237
}
@@ -2923,7 +2955,14 @@ export class SessionManager {
29232955
const dir = sessionDir ?? getDefaultSessionDir(cwd);
29242956
const mostRecent = findMostRecentSession(dir);
29252957
if (mostRecent) {
2926-
return new SessionManager(cwd, dir, mostRecent, true);
2958+
const sessionCwd = readSessionCwd(mostRecent);
2959+
if (sessionCwd !== undefined && resolve(sessionCwd) === resolve(cwd)) {
2960+
return new SessionManager(cwd, dir, mostRecent, true);
2961+
}
2962+
const cwdMatch = findMostRecentSessionByCwd(dir, cwd);
2963+
if (cwdMatch) {
2964+
return new SessionManager(cwd, dir, cwdMatch, true);
2965+
}
29272966
}
29282967
return new SessionManager(cwd, dir, undefined, true);
29292968
}

0 commit comments

Comments
 (0)