summaryrefslogtreecommitdiffhomepage
path: root/src/adapters
diff options
context:
space:
mode:
Diffstat (limited to 'src/adapters')
-rw-r--r--src/adapters/idb/index.test.ts15
-rw-r--r--src/adapters/idb/index.ts23
-rw-r--r--src/adapters/ws/logic.test.ts15
-rw-r--r--src/adapters/ws/logic.ts3
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,