| 1 | import assert from "node:assert/strict"; |
| 2 | import { TranscriptStore } from "../lib/transcriptStore"; |
| 3 | import { initialState, reducer } from "../lib/useController"; |
| 4 | import { FakeBackend } from "./helpers/transcriptFakeBackend"; |
| 5 | import { hasReusableCachedTranscript } from "../lib/hydrateHistoryApply"; |
| 6 | |
| 7 | const body = "full content ".repeat(1000); |
| 8 | const backend = new FakeBackend([{ role: "user", content: "prompt" }, { role: "assistant", content: "preview" }], |
| 9 | new Map([["s1:r0:m1:o0:content", body]])); |
| 10 | const store = new TranscriptStore(backend, { historyBodyBudgetBytes: 1024, maxResidentSessions: 1 }); |
| 11 | store.setPinned("background", true); |
| 12 | await store.loadLatest("background", "/background"); |
| 13 | assert.equal(await store.requestFullContent("background", "s1:r0:m1:o0", "content"), body, "explicit content remains complete"); |
| 14 | assert.ok(store.totalBodyBytes() <= 1024, "invisible expanded body is reclaimed even with a live pin"); |
| 15 | assert.ok(store.isResident("background", "/background"), "memory pressure cannot retire the running session"); |
| 16 | const reads = backend.contentCalls.length; |
| 17 | assert.equal(await store.requestFullContent("background", "s1:r0:m1:o0", "content"), body); |
| 18 | assert.ok(backend.contentCalls.length > reads, "reclaimed content remains reachable"); |
| 19 | store.setPinned("background", false); |
| 20 | const state = { ...initialState, historyPrefixCount: 1, historyTotalTurns: 12, historyOlderLoading: true, items: [ |
| 21 | { kind: "user" as const, id: "history", text: "cached" }, |
| 22 | { kind: "user" as const, id: "pending", text: "pending" }, |
| 23 | ] }; |
| 24 | let released = false; |
| 25 | store.subscribe("background", change => { |
| 26 | if (change.evictedPath === "/background") { |
| 27 | released = true; |
| 28 | const next = reducer(state, { type: "history_cache_evicted" }); |
| 29 | assert.deepEqual(next.items.map(item => item.id), ["pending"], "eviction releases cached rows but preserves pending input"); |
| 30 | assert.equal(hasReusableCachedTranscript(next, {}), false, "pending input cannot masquerade as resident history without a fingerprint"); |
| 31 | assert.equal(next.historyOlderLoading, false, "eviction releases superseded paging indicators"); |
| 32 | } |
| 33 | }); |
| 34 | await store.loadLatest("other", "/other"); |
| 35 | assert.ok(released, "LRU eviction notifies the controller to release its references"); |
| 36 | assert.ok(!store.isResident("background", "/background")); |
| 37 | |
| 38 | // Canonical bodies can reveal tool rows, so reclamation must also remove their |
| 39 | // structural projection rather than publishing only field patches. |
| 40 | const canonical = JSON.stringify({ role: "assistant", content: "expanded", tool_calls: [ |
| 41 | { id: "revealed-call", name: "bash", arguments: "x".repeat(4000) }, |
| 42 | ] }); |
| 43 | const canonicalBackend = new FakeBackend([], new Map([["owner:canonicalMessage", canonical]])); |
| 44 | const canonicalStore = new TranscriptStore(canonicalBackend, { historyBodyBudgetBytes: 1024 }); |
| 45 | canonicalStore.noteActiveTab("canonical"); |
| 46 | canonicalStore.setPinned("canonical", true); |
| 47 | canonicalStore.installSlice("canonical", "/canonical", { |
| 48 | entries: [{ entryId: "owner", turn: 1, order: 0, message: { role: "assistant", content: "preview" }, |
| 49 | refs: [{ entryId: "owner", field: "canonicalMessage", size: canonical.length, chunks: 2, revision: 1, digest: "body" }] }], |
| 50 | nextCursor: "", hasOlder: false, totalTurns: 1, startTurn: 1, endTurn: 1, stale: false, revision: 1, digest: "body", |
| 51 | }); |
| 52 | let removedTool = false; |
| 53 | canonicalStore.subscribe("canonical", change => { |
| 54 | if (change.projection?.removeIds?.includes("revealed-call")) removedTool = true; |
| 55 | }); |
| 56 | await canonicalStore.requestFullContent("canonical", "owner", "content"); |
| 57 | canonicalStore.noteActiveTab("other", "canonical"); |
| 58 | assert.ok(removedTool, "reclamation publishes removal of tool rows revealed by an expanded canonical body"); |
| 59 | assert.ok(canonicalStore.isResident("canonical", "/canonical"), "structural reclamation preserves the live session"); |
| 60 | { |
| 61 | const backend = new FakeBackend([{ role: "user", content: "u" }, { role: "assistant", content: "a" }]); |
| 62 | const store = new TranscriptStore(backend, { maxResidentSessions: 3 }); |
| 63 | let previous: string | undefined; |
| 64 | for (let i = 0; i < 8; i++) { |
| 65 | const tab = `active-${i}`; |
| 66 | store.noteActiveTab(tab, previous); |
| 67 | await store.loadLatest(tab, `/s/${i}.jsonl`); |
| 68 | assert.equal(store.residentSessionCount(), Math.min(i + 1, 3), "continuous navigation includes active window in resident cap"); |
| 69 | assert.equal(store.isResident(tab, `/s/${i}.jsonl`), true, "active reader is protected"); |
| 70 | previous = tab; |
| 71 | } |
| 72 | const protectedStore = new TranscriptStore(backend, { maxResidentSessions: 1 }); |
| 73 | for (const tab of ["running-a", "running-b"]) { |
| 74 | protectedStore.setPinned(tab, true); |
| 75 | await protectedStore.loadLatest(tab, `/s/${tab}`); |
| 76 | } |
| 77 | assert.equal(protectedStore.residentSessionCount(), 2, "budget pressure does not evict live execution owners"); |
| 78 | protectedStore.setPinned("running-a", false); |
| 79 | assert.equal(protectedStore.residentSessionCount(), 1, "ending a live pin immediately restores the cache budget"); |
| 80 | } |
| 81 | |
| 82 | console.log("transcript memory pressure: passed"); |
| 83 |