Skip to content

Commit 539152e

Browse files
committed
fix(session): filter by header cwd instead of changing directory encoding
Revert the ~D escape scheme in getDefaultSessionDir and instead fix cross-project session collision by filtering on the session header's cwd field in continueRecent and list. This preserves existing session directory paths during upgrade while preventing sessions from one project from leaking into another when the directory encoding collides. - Revert getDefaultSessionDir to original encoding (preserves existing dirs) - Keep cwd verification in continueRecent (skips mismatched sessions) - Add cwd filtering in list (excludes sessions from other projects) - Add regression tests for list cwd filtering and legacy empty-cwd sessions - Remove injective encoding tests (encoding unchanged from shipped behavior)
1 parent 2252c57 commit 539152e

2 files changed

Lines changed: 51 additions & 36 deletions

File tree

src/agents/sessions/session-manager.test.ts

Lines changed: 43 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import { repairSessionFileIfNeeded } from "../session-file-repair.js";
1212
import {
1313
CURRENT_SESSION_VERSION,
1414
findMostRecentSession,
15-
getDefaultSessionDir,
1615
loadEntriesFromFile,
1716
SessionManager,
1817
type SessionEntry,
@@ -2518,30 +2517,6 @@ function buildMessageEntry(index: number, parentId: string | null): SessionEntry
25182517
};
25192518
}
25202519

2521-
describe("getDefaultSessionDir encoding", () => {
2522-
it("produces distinct dirs for paths differing only by separator", async () => {
2523-
const agentDir = await makeTempDir();
2524-
const dirA = getDefaultSessionDir("/home/alice/dev/client/app", agentDir);
2525-
const dirB = getDefaultSessionDir("/home/alice/dev/client-app", agentDir);
2526-
expect(dirA).not.toBe(dirB);
2527-
});
2528-
2529-
it("produces distinct dirs for paths differing by trailing separator", async () => {
2530-
const agentDir = await makeTempDir();
2531-
const dirA = getDefaultSessionDir("/home/alice/dev", agentDir);
2532-
const dirB = getDefaultSessionDir("/home/alice/dev/", agentDir);
2533-
// Trailing separator is stripped, so these should be the same
2534-
expect(dirA).toBe(dirB);
2535-
});
2536-
2537-
it("is deterministic for the same input", async () => {
2538-
const agentDir = await makeTempDir();
2539-
const dir1 = getDefaultSessionDir("/home/alice/dev/client/app", agentDir);
2540-
const dir2 = getDefaultSessionDir("/home/alice/dev/client/app", agentDir);
2541-
expect(dir1).toBe(dir2);
2542-
});
2543-
});
2544-
25452520
describe("SessionManager.continueRecent cwd verification", () => {
25462521
it("skips session whose header cwd does not match requested cwd", async () => {
25472522
const agentDir = await makeTempDir();
@@ -2575,3 +2550,46 @@ describe("SessionManager.continueRecent cwd verification", () => {
25752550
expect(sm.getSessionId()).toBe("sess-a");
25762551
});
25772552
});
2553+
2554+
describe("SessionManager.list cwd filtering", () => {
2555+
it("filters out sessions whose header cwd does not match requested cwd", async () => {
2556+
const agentDir = await makeTempDir();
2557+
const sessionDir = path.join(agentDir, "sessions", "test-list-filter");
2558+
await fs.mkdir(sessionDir, { recursive: true });
2559+
2560+
// Write two sessions in the same directory with different cwds
2561+
const headerA = buildSessionHeader("/project-a", "sess-a");
2562+
const fileA = path.join(sessionDir, "2026-06-24T10-00-00_A.jsonl");
2563+
writeFileSync(fileA, JSON.stringify(headerA) + "\n");
2564+
2565+
const headerB = buildSessionHeader("/project-b", "sess-b");
2566+
const fileB = path.join(sessionDir, "2026-06-24T11-00-00_B.jsonl");
2567+
writeFileSync(fileB, JSON.stringify(headerB) + "\n");
2568+
2569+
// List with cwd = "/project-a" — should only see sess-a
2570+
const sessions = await SessionManager.list("/project-a", sessionDir);
2571+
expect(sessions).toHaveLength(1);
2572+
expect(sessions[0].id).toBe("sess-a");
2573+
});
2574+
2575+
it("includes sessions with empty cwd for backward compatibility", async () => {
2576+
const agentDir = await makeTempDir();
2577+
const sessionDir = path.join(agentDir, "sessions", "test-list-legacy");
2578+
await fs.mkdir(sessionDir, { recursive: true });
2579+
2580+
// Write a session without a cwd field (legacy session)
2581+
const header = {
2582+
type: "session",
2583+
version: CURRENT_SESSION_VERSION,
2584+
id: "legacy-sess",
2585+
timestamp: "2026-06-24T10:00:00.000Z",
2586+
};
2587+
const file = path.join(sessionDir, "2026-06-24T10-00-00_L.jsonl");
2588+
writeFileSync(file, JSON.stringify(header) + "\n");
2589+
2590+
// List should include legacy sessions (empty cwd is accepted)
2591+
const sessions = await SessionManager.list("/any-cwd", sessionDir);
2592+
expect(sessions).toHaveLength(1);
2593+
expect(sessions[0].id).toBe("legacy-sess");
2594+
});
2595+
});

src/agents/sessions/session-manager.ts

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -437,15 +437,7 @@ 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-
// Use an injective encoding so that paths differing only by separator
441-
// (e.g. client/app vs client-app) produce distinct session directories.
442-
// Strip leading/trailing separators, escape literal "--", then replace
443-
// internal separators with "--".
444-
const safePath = `--${cwd
445-
.replace(/^[/\\\\]/, "")
446-
.replace(/[/\\\\]$/, "")
447-
.replace(/--/g, "~D")
448-
.replace(/[/\\\\:]/g, "--")}--`;
440+
const safePath = `--${cwd.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-")}--`;
449441
const sessionDir = join(agentDir, "sessions", safePath);
450442
if (!existsSync(sessionDir)) {
451443
mkdirSync(sessionDir, { recursive: true });
@@ -3013,8 +3005,13 @@ export class SessionManager {
30133005
): Promise<SessionInfo[]> {
30143006
const dir = sessionDir ?? getDefaultSessionDir(cwd);
30153007
const sessions = await listSessionsFromDir(dir, onProgress);
3016-
sessions.sort((a, b) => b.modified.getTime() - a.modified.getTime());
3017-
return sessions;
3008+
// Filter out sessions whose header cwd does not match the requested cwd.
3009+
// The default session directory encoding is not injective (e.g. client/app
3010+
// and client-app share the same directory), so cwd-aware filtering prevents
3011+
// cross-project session leakage.
3012+
const filtered = sessions.filter((s) => !s.cwd || s.cwd === cwd);
3013+
filtered.sort((a, b) => b.modified.getTime() - a.modified.getTime());
3014+
return filtered;
30183015
}
30193016

30203017
/**

0 commit comments

Comments
 (0)