From 624b808da0f2f8bbad8a4fbbcca3f82f24ecfc47 Mon Sep 17 00:00:00 2001 From: Adam Malczewski Date: Sat, 30 May 2026 23:14:55 +0900 Subject: feat(chunks): chunk-native frontend store with turn-sealed reconcile + per-chunk eviction Replace the stored ChatMessage[] with a chunk-native model: tab.chunks (sealed ChunkRow[]) + tab.live (transient in-flight turn buffer) + derived tab.renderGroups. This enables per-chunk eviction (trimming WITHIN a large turn) and raw-chunk pagination (loadOlderChunks), removing the whole-message eviction limitation. Backend: - Emit turn-start/turn-sealed around each turn; expose currentTurnId in the status snapshot. turn-sealed fires after the durable write (status:idle fires before it). - New GET /tabs/:id/chunks raw paginated endpoint (limit/before). - Wrap appendChunks in a single SQLite transaction. Frontend: - turn-sealed drives a turn-aware reconcile that folds the sealed turn into chunks while preserving a concurrent newer in-flight turn and pending queued messages; deferred while the user is scrolled up. - Stable turn-scoped render keys (${turnId}:${role}:${n}) avoid remount/flash. Reconcile correctness (three review passes): - preserve a concurrent newer turn when an earlier deferred reconcile flushes; - keep optimistic queued user messages (no loss); - turn-start backfill skips pending queued rows and tags only the turn initiator; - bind consumed interrupt messages to the in-flight turn so they collapse on seal (no lingering/duplicated bubble). Tests: chat-store reconcile/eviction/pagination suite; api chunks endpoint + events. --- packages/core/src/db/chunks.ts | 58 +++++++++++++++++++++++------------------- 1 file changed, 32 insertions(+), 26 deletions(-) (limited to 'packages/core/src/db/chunks.ts') diff --git a/packages/core/src/db/chunks.ts b/packages/core/src/db/chunks.ts index 6841eb5..077259d 100644 --- a/packages/core/src/db/chunks.ts +++ b/packages/core/src/db/chunks.ts @@ -52,32 +52,38 @@ export function appendChunks(tabId: string, drafts: ChunkRowDraft[]): ChunkRow[] VALUES ($id, $tabId, $seq, $turnId, $step, $role, $type, $dataJson, $now)`, ); const out: ChunkRow[] = []; - for (const draft of drafts) { - const id = randomUUID(); - insert.run({ - $id: id, - $tabId: tabId, - $seq: seq, - $turnId: draft.turnId, - $step: draft.step, - $role: draft.role, - $type: draft.type, - $dataJson: JSON.stringify(draft.data), - $now: now, - }); - out.push({ - id, - tabId, - seq, - turnId: draft.turnId, - step: draft.step, - role: draft.role, - type: draft.type, - data: draft.data, - createdAt: now, - }); - seq++; - } + // Wrap the whole batch in one transaction: a turn's chunks are persisted in + // a single `appendChunks` call, so this is one fsync per turn instead of one + // per row — the chosen low-IO write strategy for constrained backends. + const insertAll = db.transaction(() => { + for (const draft of drafts) { + const id = randomUUID(); + insert.run({ + $id: id, + $tabId: tabId, + $seq: seq, + $turnId: draft.turnId, + $step: draft.step, + $role: draft.role, + $type: draft.type, + $dataJson: JSON.stringify(draft.data), + $now: now, + }); + out.push({ + id, + tabId, + seq, + turnId: draft.turnId, + step: draft.step, + role: draft.role, + type: draft.type, + data: draft.data, + createdAt: now, + }); + seq++; + } + }); + insertAll(); return out; } -- cgit v1.2.3