From 9f2dcfaab421fa84837685a0b65678acf03d9ded Mon Sep 17 00:00:00 2001 From: Adam Malczewski Date: Sat, 10 Oct 2026 00:06:31 +0900 Subject: fix(turn-metrics): exact per-turn placement via turnId, decode-only TPS, step-level retention MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Placement - Chunks carry their producing turn: StoredChunk.turnId (backend wire@0.13) 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. --- src/core/chunks/selectors.ts | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) (limited to 'src/core/chunks/selectors.ts') diff --git a/src/core/chunks/selectors.ts b/src/core/chunks/selectors.ts index e46d5f5..8b265a5 100644 --- a/src/core/chunks/selectors.ts +++ b/src/core/chunks/selectors.ts @@ -8,17 +8,31 @@ import type { RenderedChunk, TranscriptState } from "./types"; export function selectChunks(state: TranscriptState): readonly RenderedChunk[] { const result: RenderedChunk[] = []; for (const c of state.committed) { - result.push({ seq: c.seq, role: c.role, chunk: c.chunk, provisional: false }); + result.push( + c.turnId !== undefined + ? { seq: c.seq, role: c.role, chunk: c.chunk, provisional: false, turnId: c.turnId } + : { seq: c.seq, role: c.role, chunk: c.chunk, provisional: false }, + ); } for (const p of state.provisional) { - result.push({ seq: null, role: p.role, chunk: p.chunk, provisional: true }); + result.push( + p.turnId !== undefined + ? { seq: null, role: p.role, chunk: p.chunk, provisional: true, turnId: p.turnId } + : { seq: null, role: p.role, chunk: p.chunk, provisional: true }, + ); } - if (state.accumulating !== null) { + const acc = state.accumulating; + if (acc !== null) { const chunk: Chunk = - state.accumulating.kind === "text" - ? { type: "text", text: state.accumulating.text } - : { type: "thinking", text: state.accumulating.text }; - result.push({ seq: null, role: "assistant", chunk, provisional: true, streaming: true }); + acc.kind === "text" ? { type: "text", text: acc.text } : { type: "thinking", text: acc.text }; + result.push({ + seq: null, + role: "assistant", + chunk, + provisional: true, + streaming: true, + ...(acc.turnId !== undefined ? { turnId: acc.turnId } : {}), + }); } return result; } -- cgit v1.2.3