From 01ac8db8ce49d904ee4415329b57121a7785eed2 Mon Sep 17 00:00:00 2001 From: sunlit-deng Date: Wed, 26 Aug 2026 18:16:26 +0800 Subject: [PATCH] fix(sessions): exclude archived trajectory artifacts (#126912) * fix(sessions): exclude archived trajectory artifacts * fix(sessions): classify archived transcripts through canonical parser Reject archived trajectory and compaction checkpoint artifacts while preserving retained conversation transcripts and existing Windows test cleanup. Co-authored-by: sunlit-deng * test(memory): compact session archive listener expectations Preserve every archive-listener invariant while keeping the regression suite below its enforced line budget. Co-authored-by: sunlit-deng --------- Co-authored-by: Peter Steinberger --- .../manager-sync-ops.startup-catchup.test.ts | 13 ++-- .../src/host/session-files.test.ts | 10 ++- src/config/sessions/artifacts.test.ts | 67 +++++++++++++++++++ src/config/sessions/artifacts.ts | 8 +-- 4 files changed, 86 insertions(+), 12 deletions(-) diff --git a/extensions/memory-core/src/memory/manager-sync-ops.startup-catchup.test.ts b/extensions/memory-core/src/memory/manager-sync-ops.startup-catchup.test.ts index cb4c7bfdbeb4..50fb62d10132 100644 --- a/extensions/memory-core/src/memory/manager-sync-ops.startup-catchup.test.ts +++ b/extensions/memory-core/src/memory/manager-sync-ops.startup-catchup.test.ts @@ -1083,6 +1083,10 @@ describe("session startup catch-up", () => { it.each([ "thread.jsonl.bak.2026-06-23T10-00-00.000Z", "thread.trajectory.jsonl", + "thread.trajectory.jsonl.deleted.2026-06-23T10-00-00.000Z", + "thread.trajectory.jsonl.reset.2026-06-23T10-00-00.000Z.zst", + "thread.checkpoint.11111111-1111-4111-8111-111111111111.jsonl.deleted.2026-06-23T10-00-00.000Z", + "thread.checkpoint.11111111-1111-4111-8111-111111111111.jsonl.reset.2026-06-23T10-00-00.000Z.zst", "sessions.json", ])("ignores non-corpus session artifact updates for %s", async (fileName) => { vi.useFakeTimers(); @@ -1096,10 +1100,8 @@ describe("session startup catch-up", () => { await vi.advanceTimersByTimeAsync(6000); await harness.waitForSessionSync(); - expect(harness.getPendingArchiveFiles()).toEqual([]); - expect(harness.getDirtyArchiveFiles()).toEqual([]); - expect(harness.syncCalls).toEqual([]); - expect(harness.indexedPaths).toEqual([]); + expect([harness.getPendingArchiveFiles(), harness.getDirtyArchiveFiles()]).toEqual([[], []]); + expect([harness.syncCalls, harness.indexedPaths]).toEqual([[], []]); } finally { harness.stopTranscriptListener(); } @@ -1119,8 +1121,7 @@ describe("session startup catch-up", () => { await harness.waitForSessionSync(); expect(harness.getDirtyArchiveFiles()).toEqual([]); - expect(harness.syncCalls).toEqual([]); - expect(harness.indexedPaths).toEqual([]); + expect([harness.syncCalls, harness.indexedPaths]).toEqual([[], []]); } finally { harness.stopTranscriptListener(); } diff --git a/packages/memory-host-sdk/src/host/session-files.test.ts b/packages/memory-host-sdk/src/host/session-files.test.ts index b192fcd88bc8..23e20bffcedf 100644 --- a/packages/memory-host-sdk/src/host/session-files.test.ts +++ b/packages/memory-host-sdk/src/host/session-files.test.ts @@ -110,7 +110,15 @@ describe("listSessionFilesForAgent", () => { "active.jsonl.reset.2026-02-16T22-26-33.000Z", "active.jsonl.deleted.2026-02-16T22-27-33.000Z", ]; - const excluded = ["active.jsonl.bak.2026-02-16T22-28-33.000Z", "sessions.json", "notes.md"]; + const excluded = [ + "active.jsonl.bak.2026-02-16T22-28-33.000Z", + "active.trajectory.jsonl.deleted.2026-02-16T22-30-33.000Z", + "active.trajectory.jsonl.reset.2026-02-16T22-31-33.000Z.zst", + "active.checkpoint.11111111-1111-4111-8111-111111111111.jsonl.deleted.2026-02-16T22-32-33.000Z", + "active.checkpoint.11111111-1111-4111-8111-111111111111.jsonl.reset.2026-02-16T22-33-33.000Z.zst", + "sessions.json", + "notes.md", + ]; excluded.push("active.checkpoint.11111111-1111-4111-8111-111111111111.jsonl"); for (const fileName of [...included, ...excluded]) { diff --git a/src/config/sessions/artifacts.test.ts b/src/config/sessions/artifacts.test.ts index f4fab67802d2..52c9b858572d 100644 --- a/src/config/sessions/artifacts.test.ts +++ b/src/config/sessions/artifacts.test.ts @@ -90,6 +90,8 @@ describe("session artifact helpers", () => { it("classifies usage-counted transcript files", () => { expect(isUsageCountedSessionTranscriptFileName("abc.jsonl")).toBe(true); + expect(isUsageCountedSessionTranscriptFileName(".jsonl")).toBe(true); + expect(isUsageCountedSessionTranscriptFileName("keep.deleted.keep.jsonl")).toBe(true); expect( isUsageCountedSessionTranscriptFileName("abc.jsonl.reset.2026-01-01T00-00-00.000Z"), ).toBe(true); @@ -101,6 +103,39 @@ describe("session artifact helpers", () => { `abc.jsonl.deleted.2026-01-01T00-00-00.000Z.${"a".repeat(32)}`, ), ).toBe(true); + expect( + isUsageCountedSessionTranscriptFileName("abc.jsonl.reset.2026-01-01T00-00-00.000Z.zst"), + ).toBe(true); + expect( + isUsageCountedSessionTranscriptFileName( + "abc.checkpoint.not-a-uuid.jsonl.deleted.2026-01-01T00-00-00.000Z", + ), + ).toBe(true); + expect( + isUsageCountedSessionTranscriptFileName( + "abc.trajectory.jsonl.deleted.2026-01-01T00-00-00.000Z", + ), + ).toBe(false); + expect( + isUsageCountedSessionTranscriptFileName( + "abc.trajectory.jsonl.reset.2026-01-01T00-00-00.000Z.zst", + ), + ).toBe(false); + for (const reason of ["reset", "deleted"] as const) { + expect( + isUsageCountedSessionTranscriptFileName( + `abc.checkpoint.11111111-1111-4111-8111-111111111111.jsonl.${reason}.2026-01-01T00-00-00.000Z.zst`, + ), + ).toBe(false); + } + expect(isUsageCountedSessionTranscriptFileName(".jsonl.reset.2026-01-01T00-00-00.000Z")).toBe( + false, + ); + expect( + isUsageCountedSessionTranscriptFileName( + `abc.jsonl.deleted.2026-01-01T00-00-00.000Z.${"A".repeat(32)}`, + ), + ).toBe(false); expect(isUsageCountedSessionTranscriptFileName("abc.jsonl.bak.2026-01-01T00-00-00.000Z")).toBe( false, ); @@ -114,6 +149,10 @@ describe("session artifact helpers", () => { it("parses usage-counted session ids from file names", () => { expect(parseUsageCountedSessionIdFromFileName("abc.jsonl")).toBe("abc"); + expect(parseUsageCountedSessionIdFromFileName(".jsonl")).toBe(""); + expect(parseUsageCountedSessionIdFromFileName("keep.deleted.keep.jsonl")).toBe( + "keep.deleted.keep", + ); expect(parseUsageCountedSessionIdFromFileName("abc.jsonl.reset.2026-01-01T00-00-00.000Z")).toBe( "abc", ); @@ -125,6 +164,14 @@ describe("session artifact helpers", () => { `abc.jsonl.deleted.2026-01-01T00-00-00.000Z.${"a".repeat(32)}`, ), ).toBe("abc"); + expect( + parseUsageCountedSessionIdFromFileName("abc.jsonl.reset.2026-01-01T00-00-00.000Z.zst"), + ).toBe("abc"); + expect( + parseUsageCountedSessionIdFromFileName( + "abc.checkpoint.not-a-uuid.jsonl.deleted.2026-01-01T00-00-00.000Z", + ), + ).toBe("abc.checkpoint.not-a-uuid"); expect(parseUsageCountedSessionIdFromFileName("abc.jsonl.bak.2026-01-01T00-00-00.000Z")).toBe( null, ); @@ -134,6 +181,26 @@ describe("session artifact helpers", () => { ), ).toBeNull(); expect(parseUsageCountedSessionIdFromFileName("abc.trajectory.jsonl")).toBeNull(); + expect( + parseUsageCountedSessionIdFromFileName( + "abc.trajectory.jsonl.deleted.2026-01-01T00-00-00.000Z", + ), + ).toBeNull(); + expect( + parseUsageCountedSessionIdFromFileName( + "abc.trajectory.jsonl.reset.2026-01-01T00-00-00.000Z.zst", + ), + ).toBeNull(); + for (const reason of ["reset", "deleted"] as const) { + expect( + parseUsageCountedSessionIdFromFileName( + `abc.checkpoint.11111111-1111-4111-8111-111111111111.jsonl.${reason}.2026-01-01T00-00-00.000Z.zst`, + ), + ).toBeNull(); + } + expect( + parseUsageCountedSessionIdFromFileName(".jsonl.reset.2026-01-01T00-00-00.000Z"), + ).toBeNull(); }); it("formats and parses archive timestamps", () => { diff --git a/src/config/sessions/artifacts.ts b/src/config/sessions/artifacts.ts index 417461798598..f471a99a7d98 100644 --- a/src/config/sessions/artifacts.ts +++ b/src/config/sessions/artifacts.ts @@ -137,10 +137,7 @@ export function isPrimarySessionTranscriptFileName(fileName: string): boolean { /** Returns true for transcript files counted in usage, including reset/deleted archives. */ export function isUsageCountedSessionTranscriptFileName(fileName: string): boolean { - if (isPrimarySessionTranscriptFileName(fileName)) { - return true; - } - return hasArchiveSuffix(fileName, "reset") || hasArchiveSuffix(fileName, "deleted"); + return parseUsageCountedSessionIdFromFileName(fileName) !== null; } /** Extracts the session id from a usage-counted transcript filename. */ @@ -153,7 +150,8 @@ export function parseUsageCountedSessionIdFromFileName(fileName: string): string const marker = `.jsonl.${reason}.`; const index = normalized.lastIndexOf(marker); if (index > 0 && hasArchiveSuffix(normalized, reason)) { - return normalized.slice(0, index); + const sessionId = normalized.slice(0, index); + return isPrimarySessionTranscriptFileName(`${sessionId}.jsonl`) ? sessionId : null; } } return null;