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/groups.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) (limited to 'src/core/chunks/groups.ts') diff --git a/src/core/chunks/groups.ts b/src/core/chunks/groups.ts index c1c80c9..9df0fa5 100644 --- a/src/core/chunks/groups.ts +++ b/src/core/chunks/groups.ts @@ -22,6 +22,8 @@ export type RenderGroup = readonly stepId: string; readonly entries: readonly ToolBatchEntry[]; readonly provisional: boolean; + /** The producing turn (from the batch's first call chunk), when known. */ + readonly turnId?: string; }; /** @@ -79,6 +81,7 @@ export function groupRenderedChunks(rendered: readonly RenderedChunk[]): readonl // (Svelte `each_key_duplicate` aborts the reactive flush — tab appears dead). const seenCallIds = new Set(); let provisional = false; + let turnId: string | undefined; for (const inner of rendered) { if (inner.chunk.type === "tool-call" && inner.chunk.stepId === stepId) { if (seenCallIds.has(inner.chunk.toolCallId)) continue; @@ -86,9 +89,14 @@ export function groupRenderedChunks(rendered: readonly RenderedChunk[]): readonl const result = resultByCallId.get(inner.chunk.toolCallId) ?? null; entries.push({ call: inner.chunk, result }); if (inner.provisional) provisional = true; + turnId ??= inner.turnId; } } - groups.push({ kind: "tool-batch", stepId, entries, provisional }); + groups.push( + turnId !== undefined + ? { kind: "tool-batch", stepId, entries, provisional, turnId } + : { kind: "tool-batch", stepId, entries, provisional }, + ); continue; } -- cgit v1.2.3