diff options
Diffstat (limited to 'src/adapters')
| -rw-r--r-- | src/adapters/idb/index.test.ts | 15 | ||||
| -rw-r--r-- | src/adapters/idb/index.ts | 23 | ||||
| -rw-r--r-- | src/adapters/ws/logic.test.ts | 15 | ||||
| -rw-r--r-- | src/adapters/ws/logic.ts | 3 |
4 files changed, 56 insertions, 0 deletions
diff --git a/src/adapters/idb/index.test.ts b/src/adapters/idb/index.test.ts index 04e2177..aa56f15 100644 --- a/src/adapters/idb/index.test.ts +++ b/src/adapters/idb/index.test.ts @@ -128,3 +128,18 @@ describe("createIdbChunkStore", () => { expect(loaded2[0]?.chunk).toEqual(textChunk("b1")); }); }); + +it("persists epoch across adapter recreation and append, including empty histories", async () => { + const indexedDB = new IDBFactory(); + const store = createIdbChunkStore({ indexedDB }); + await store.append("c", [makeChunk(1, "legacy")]); + expect(await store.historyEpoch("c")).toBeUndefined(); + await store.reset("c", 2); + const reopened = createIdbChunkStore({ indexedDB }); + expect(await reopened.load("c")).toEqual([]); + expect(await reopened.historyEpoch("c")).toBe(2); + await reopened.append("c", [makeChunk(101, "new")]); + expect(await reopened.historyEpoch("c")).toBe(2); + await reopened.delete("c"); + expect(await reopened.historyEpoch("c")).toBeUndefined(); +}); diff --git a/src/adapters/idb/index.ts b/src/adapters/idb/index.ts index 3dffef9..8b1848a 100644 --- a/src/adapters/idb/index.ts +++ b/src/adapters/idb/index.ts @@ -20,6 +20,7 @@ interface ChunkRecord { interface MetaRecord { conversationId: string; lastAccess: number; + historyEpoch?: number; } export interface CreateIdbChunkStoreOptions { @@ -92,6 +93,26 @@ export function createIdbChunkStore(opts?: CreateIdbChunkStoreOptions): Conversa } return { + async historyEpoch(conversationId) { + const db = await getDb(); + const tx = db.transaction(META_STORE, "readonly"); + const meta = await requestToPromise<MetaRecord | undefined>( + tx.objectStore(META_STORE).get(conversationId), + ); + await txComplete(tx); + return meta?.historyEpoch; + }, + async reset(conversationId, historyEpoch) { + const db = await getDb(); + const tx = db.transaction([CHUNKS_STORE, META_STORE], "readwrite"); + tx.objectStore(CHUNKS_STORE).delete(keyRangeFor(conversationId)); + tx.objectStore(META_STORE).put({ + conversationId, + historyEpoch, + lastAccess: Date.now(), + } satisfies MetaRecord); + await txComplete(tx); + }, async load(conversationId: string): Promise<readonly StoredChunk[]> { const db = await getDb(); const tx = db.transaction(CHUNKS_STORE, "readonly"); @@ -122,7 +143,9 @@ export function createIdbChunkStore(opts?: CreateIdbChunkStoreOptions): Conversa } satisfies ChunkRecord); } + const meta = await requestToPromise<MetaRecord | undefined>(metaStore.get(conversationId)); metaStore.put({ + ...meta, conversationId, lastAccess: Date.now(), } satisfies MetaRecord); diff --git a/src/adapters/ws/logic.test.ts b/src/adapters/ws/logic.test.ts index 2179ad0..4be2cd6 100644 --- a/src/adapters/ws/logic.test.ts +++ b/src/adapters/ws/logic.test.ts @@ -562,3 +562,18 @@ describe("parseServerMessage — subagent tab groups", () => { ).toBeNull(); }); }); + +it("preserves a compaction epoch and rejects absent or invalid epochs", () => { + const msg = { + type: "conversation.compacted", + conversationId: "c", + newConversationId: "archive", + messagesSummarized: 2, + messagesKept: 1, + historyEpoch: 7, + }; + expect(parseServerMessage(JSON.stringify(msg))).toEqual(msg); + for (const historyEpoch of [undefined, -1, 1.5, "7"]) { + expect(parseServerMessage(JSON.stringify({ ...msg, historyEpoch }))).toBeNull(); + } +}); diff --git a/src/adapters/ws/logic.ts b/src/adapters/ws/logic.ts index cecc66c..858deaa 100644 --- a/src/adapters/ws/logic.ts +++ b/src/adapters/ws/logic.ts @@ -180,7 +180,10 @@ export function parseServerMessage(data: string): WsServerMessage | null { if (typeof parsed.newConversationId !== "string") return null; if (typeof parsed.messagesSummarized !== "number") return null; if (typeof parsed.messagesKept !== "number") return null; + if (!Number.isSafeInteger(parsed.historyEpoch) || (parsed.historyEpoch as number) < 0) + return null; const msg: ConversationCompactedMessage = { + historyEpoch: parsed.historyEpoch as number, type: "conversation.compacted", conversationId: parsed.conversationId, newConversationId: parsed.newConversationId, |
