clawdbot-9c3: constrain lifecycle transcript archive

This commit is contained in:
Josh Lehman
2026-06-02 16:07:13 -07:00
parent dcbac222d8
commit 09252bbb0c
2 changed files with 29 additions and 5 deletions

View File

@@ -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 () => {

View File

@@ -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({