summaryrefslogtreecommitdiffhomepage
path: root/packages/core/src/db
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-05-30 23:14:55 +0900
committerAdam Malczewski <[email protected]>2026-05-30 23:14:55 +0900
commit624b808da0f2f8bbad8a4fbbcca3f82f24ecfc47 (patch)
tree869d34092345344ff13953398f876c8b38c8116a /packages/core/src/db
parentb19f1aafc43141a865ecd40a813ed3212e77d95e (diff)
downloaddispatch-624b808da0f2f8bbad8a4fbbcca3f82f24ecfc47.tar.gz
dispatch-624b808da0f2f8bbad8a4fbbcca3f82f24ecfc47.zip
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.
Diffstat (limited to 'packages/core/src/db')
-rw-r--r--packages/core/src/db/chunks.ts58
1 files changed, 32 insertions, 26 deletions
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;
}