diff options
Diffstat (limited to 'src/features/conversation-cache')
| -rw-r--r-- | src/features/conversation-cache/logic.test.ts | 23 | ||||
| -rw-r--r-- | src/features/conversation-cache/logic.ts | 22 |
2 files changed, 38 insertions, 7 deletions
diff --git a/src/features/conversation-cache/logic.test.ts b/src/features/conversation-cache/logic.test.ts index 880c32e..7a31783 100644 --- a/src/features/conversation-cache/logic.test.ts +++ b/src/features/conversation-cache/logic.test.ts @@ -138,3 +138,26 @@ describe("selectEvictions", () => { expect(result).toEqual(["b"]); }); }); + +describe("reconcileCache — turnId provenance upgrade", () => { + it("upgrades a cached row lacking turnId when the incoming copy carries it", () => { + const cached: StoredChunk[] = [{ seq: 1, role: "user", chunk: { type: "text", text: "hi" } }]; + const incoming: StoredChunk[] = [ + { seq: 1, role: "user", chunk: { type: "text", text: "hi" }, turnId: "t1" }, + { seq: 2, role: "assistant", chunk: { type: "text", text: "yo" }, turnId: "t1" }, + ]; + const { merged, toAppend } = reconcileCache(cached, incoming); + expect(merged.map((c) => c.turnId)).toEqual(["t1", "t1"]); + expect(toAppend.map((c) => c.seq)).toEqual([1, 2]); + }); + + it("does not rewrite a cached row that already has a turnId", () => { + const cached: StoredChunk[] = [ + { seq: 1, role: "user", chunk: { type: "text", text: "hi" }, turnId: "t1" }, + ]; + const { toAppend } = reconcileCache(cached, [ + { seq: 1, role: "user", chunk: { type: "text", text: "hi" }, turnId: "t1" }, + ]); + expect(toAppend).toEqual([]); + }); +}); diff --git a/src/features/conversation-cache/logic.ts b/src/features/conversation-cache/logic.ts index 3cb23e2..344a061 100644 --- a/src/features/conversation-cache/logic.ts +++ b/src/features/conversation-cache/logic.ts @@ -5,27 +5,35 @@ import type { ConversationCacheIndexEntry, ReconcileResult } from "./types"; * Merge authoritative seq-keyed chunks with cached chunks. * * Deduplicates by `seq`, produces seq-monotonic order. - * `toAppend` = the incoming chunks whose `seq` is not already in `cached` - * (exactly what to persist). Idempotent; tolerant of out-of-order/overlapping `incoming`. + * `toAppend` = the incoming chunks whose `seq` is not already in `cached`, + * plus cached rows UPGRADED with a `turnId` the incoming copy carries and the + * cached copy lacks (exactly what to persist — the store upserts by seq). Idempotent; tolerant of out-of-order/overlapping `incoming`. */ export function reconcileCache( cached: readonly StoredChunk[], incoming: readonly StoredChunk[], ): ReconcileResult { - const seen = new Set<number>(); + const bySeq = new Map<number, StoredChunk>(); for (const chunk of cached) { - seen.add(chunk.seq); + bySeq.set(chunk.seq, chunk); } const toAppend: StoredChunk[] = []; for (const chunk of incoming) { - if (!seen.has(chunk.seq)) { + const have = bySeq.get(chunk.seq); + if (have === undefined) { toAppend.push(chunk); - seen.add(chunk.seq); + bySeq.set(chunk.seq, chunk); + } else if (have.turnId === undefined && chunk.turnId !== undefined) { + // Upgrade a cached row written before the server stamped `turnId` + // (seqs are immutable, so only this provenance field can differ). + const upgraded = { ...have, turnId: chunk.turnId }; + toAppend.push(upgraded); + bySeq.set(chunk.seq, upgraded); } } - const merged = [...cached, ...toAppend].sort((a, b) => a.seq - b.seq); + const merged = [...bySeq.values()].sort((a, b) => a.seq - b.seq); return { merged, toAppend }; } |
