diff --git a/src/config/sessions/session-accessor.test.ts b/src/config/sessions/session-accessor.test.ts index 57592e21b326..9f8512968e4c 100644 --- a/src/config/sessions/session-accessor.test.ts +++ b/src/config/sessions/session-accessor.test.ts @@ -276,6 +276,9 @@ describe("session accessor file-backed seam", () => { const freshTranscriptPath = path.join(tempDir, "fresh-lifecycle.jsonl"); const referencedTranscriptPath = path.join(tempDir, "referenced.jsonl"); const orphanTranscriptPath = path.join(tempDir, "orphan-lifecycle.jsonl"); + const siblingDir = `${tempDir}-sibling-sessions`; + const siblingTranscriptPath = path.join(siblingDir, "sibling-lifecycle.jsonl"); + fs.mkdirSync(siblingDir, { recursive: true }); fs.writeFileSync( storePath, @@ -290,6 +293,10 @@ describe("session accessor file-backed seam", () => { sessionFile: "custom-lifecycle-old.jsonl", sessionId: "custom-lifecycle", }, + "agent:main:lifecycle-cleanup-sibling": { + sessionFile: siblingTranscriptPath, + sessionId: "sibling-lifecycle", + }, "agent:main:telegram:group:lifecycle-cleanup-room": { sessionId: "kept-by-segment", }, @@ -303,6 +310,7 @@ describe("session accessor file-backed seam", () => { fs.writeFileSync(customTranscriptPath, '{"runId":"lifecycle-marker-custom"}\n', "utf-8"); fs.writeFileSync(freshDefaultTranscriptPath, '{"runId":"lifecycle-marker-default"}\n', "utf-8"); fs.writeFileSync(freshTranscriptPath, '{"runId":"lifecycle-marker-fresh"}\n', "utf-8"); + fs.writeFileSync(siblingTranscriptPath, '{"runId":"lifecycle-marker-sibling"}\n', "utf-8"); fs.writeFileSync( referencedTranscriptPath, '{"runId":"lifecycle-marker-referenced"}\n', @@ -311,6 +319,7 @@ describe("session accessor file-backed seam", () => { fs.writeFileSync(orphanTranscriptPath, '{"runId":"lifecycle-marker-orphan"}\n', "utf-8"); fs.utimesSync(removedTranscriptPath, oldDate, oldDate); fs.utimesSync(customTranscriptPath, oldDate, oldDate); + fs.utimesSync(siblingTranscriptPath, oldDate, oldDate); fs.utimesSync(referencedTranscriptPath, oldDate, oldDate); fs.utimesSync(orphanTranscriptPath, oldDate, oldDate); @@ -322,10 +331,11 @@ describe("session accessor file-backed seam", () => { nowMs, }); - expect(result).toEqual({ removedEntries: 2, archivedTranscriptArtifacts: 3 }); + expect(result).toEqual({ removedEntries: 3, archivedTranscriptArtifacts: 3 }); const loaded = loadSessionStore(storePath, { skipCache: true }); expect(loaded).not.toHaveProperty("agent:main:lifecycle-cleanup-removed"); expect(loaded).not.toHaveProperty("agent:main:lifecycle-cleanup-custom"); + expect(loaded).not.toHaveProperty("agent:main:lifecycle-cleanup-sibling"); expect(loaded).toHaveProperty("agent:main:lifecycle-cleanup-fresh"); expect(loaded).toHaveProperty("agent:main:telegram:group:lifecycle-cleanup-room"); expect(loaded).toHaveProperty("agent:main:regular"); @@ -342,6 +352,8 @@ describe("session accessor file-backed seam", () => { expect(files).toContain("custom-lifecycle.jsonl"); expect(files).toContain("fresh-lifecycle.jsonl"); expect(files).toContain("referenced.jsonl"); + expect(fs.existsSync(siblingTranscriptPath)).toBe(true); + expect(fs.readdirSync(siblingDir)).toEqual(["sibling-lifecycle.jsonl"]); }); it("loads and appends transcript events through a session scope", async () => { diff --git a/src/config/sessions/store.ts b/src/config/sessions/store.ts index eb2938760162..02e23ce3cd65 100644 --- a/src/config/sessions/store.ts +++ b/src/config/sessions/store.ts @@ -572,10 +572,19 @@ function lifecycleTranscriptIsReclaimable(params: { } } -function archiveExactLifecycleTranscriptPath(transcriptPath: string): number { - const archivedPath = `${transcriptPath}.deleted.${formatSessionArchiveTimestamp()}`; +function archiveExactLifecycleTranscriptPath(params: { + sessionsDir: string; + transcriptPath: string; +}): number { + const resolvedSessionsDir = normalizePathForLifecycleComparison(params.sessionsDir); + const resolvedTranscriptPath = normalizePathForLifecycleComparison(params.transcriptPath); + const relative = path.relative(resolvedSessionsDir, resolvedTranscriptPath); + if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) { + return 0; + } + const archivedPath = `${resolvedTranscriptPath}.deleted.${formatSessionArchiveTimestamp()}`; try { - fs.renameSync(transcriptPath, archivedPath); + fs.renameSync(resolvedTranscriptPath, archivedPath); emitSessionTranscriptUpdate({ sessionFile: archivedPath }); return 1; } catch { @@ -1025,7 +1034,10 @@ export async function cleanupSessionLifecycleArtifacts( if (referencedSessionIds.has(removedSessionId)) { continue; } - archivedTranscriptArtifacts += archiveExactLifecycleTranscriptPath(transcriptPath); + archivedTranscriptArtifacts += archiveExactLifecycleTranscriptPath({ + sessionsDir, + transcriptPath, + }); } const { removeRemovedSessionTrajectoryArtifacts } = await loadTrajectoryCleanupRuntime(); await removeRemovedSessionTrajectoryArtifacts({