From 0bdb04b0aa0c0500e68e2e985b1a2880730ad7e8 Mon Sep 17 00:00:00 2001 From: Adam Malczewski Date: Wed, 21 Oct 2026 14:46:06 +0900 Subject: fix(review-followup-fec): reconcile epoch rollback and retain reconnect backoff --- src/features/conversation-cache/cache.ts | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) (limited to 'src/features/conversation-cache/cache.ts') diff --git a/src/features/conversation-cache/cache.ts b/src/features/conversation-cache/cache.ts index 431a2a7..600a6e6 100644 --- a/src/features/conversation-cache/cache.ts +++ b/src/features/conversation-cache/cache.ts @@ -6,6 +6,19 @@ export interface ConversationCache { historyEpoch(conversationId: string): Promise; reset(conversationId: string, historyEpoch: number): Promise; + /** + * Replace from a fresh full-history response, including an epoch decrease. + * Only succeeds if the cache still has the epoch observed BEFORE that request. + * Unlike monotonic reset/commit, this is for confirmed server authority, not + * broadcasts or cursor/backfill responses. False means the response is stale. + */ + replace( + conversationId: string, + incoming: readonly StoredChunk[], + historyEpoch: number, + expectedEpoch: number | undefined, + ): Promise; + /** Load all cached chunks for a conversation. */ load(conversationId: string): Promise; @@ -73,6 +86,13 @@ export function createConversationCache( const current = await store.historyEpoch(id); if (current === undefined || current < epoch) await store.reset(id, epoch); }), + replace: (id, incoming, epoch, expectedEpoch) => + exclusive(id, async () => { + if ((await store.historyEpoch(id)) !== expectedEpoch) return false; + await store.reset(id, epoch); + if (incoming.length > 0) await store.append(id, incoming); + return true; + }), async load(conversationId) { await pending.get(conversationId); return store.load(conversationId); @@ -82,7 +102,9 @@ export function createConversationCache( return exclusive(conversationId, async () => { if (epoch !== undefined) { const current = await store.historyEpoch(conversationId); - if (current !== undefined && current > epoch) return store.load(conversationId); + // Once an epoch is known, only explicit invalidation or guarded + // full-history replacement may change it (in either direction). + if (current !== undefined && current !== epoch) return store.load(conversationId); if (current !== epoch) await store.reset(conversationId, epoch); } const cached = await store.load(conversationId); -- cgit v1.2.3