summaryrefslogtreecommitdiffhomepage
path: root/src/features/conversation-cache/logic.ts
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-10-10 00:06:31 +0900
committerAdam Malczewski <[email protected]>2026-10-10 00:06:31 +0900
commit9f2dcfaab421fa84837685a0b65678acf03d9ded (patch)
treea1ef40fbbf98b136a471889c5b02795b739753be /src/features/conversation-cache/logic.ts
parent591e2dbe5cbf2283d58debdf92943a075edc4569 (diff)
downloaddispatch-web-9f2dcfaab421fa84837685a0b65678acf03d9ded.tar.gz
dispatch-web-9f2dcfaab421fa84837685a0b65678acf03d9ded.zip
fix(turn-metrics): exact per-turn placement via turnId, decode-only TPS, step-level retention
Placement - Chunks carry their producing turn: StoredChunk.turnId (backend [email protected]) for committed rows (persisted in the IDB cache; old cached rows are upgraded on sync) and the live event's turnId for provisional ones (the optimistic echo is tagged on turn-start). - interleaveTurnMetrics attributes every group to its turn (turnId, else the turn owning its stepId) and puts the turn total after the turn's LAST visible group. No user-message counting: sending, streaming, steering, images, errored turns and chat-limit unloading can no longer shift metrics between turns — at any point during a turn, not just after it ends. A turn cut in half by the chat limit keeps its total under its visible tail. - Legacy rows (no turnId) fall back to an order-preserving, count-guarded match that leaves a turn blank rather than show another turn's numbers. - A turn that errors re-syncs metrics (the backend now persists them). TPS - Step TPS = outputTokens / decodeMs only (no genTotalMs fallback — it includes time-to-first-token). Turn TPS sums tokens and decode time over timed steps only. Tool execution is excluded by the backend. Display - Retention = this turn's first-step cache reads / previous turn's last-step prompt (step-level; turn sums made it pin at 100%). - Turn row shows output tokens + input processed (with cached), instead of a misleading 'total tok' that summed every step's full prompt.
Diffstat (limited to 'src/features/conversation-cache/logic.ts')
-rw-r--r--src/features/conversation-cache/logic.ts22
1 files changed, 15 insertions, 7 deletions
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 };
}