From d7d3ac7a20815b1890ee7142dcc51e5e6d2dda03 Mon Sep 17 00:00:00 2001 From: Adam Malczewski Date: Mon, 22 Jun 2026 12:17:08 +0900 Subject: fix(metrics): preserve turn-metrics for trimmed turns MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the chat limit unloads a turn's content (user message + chunks), the segment disappears and the turn-metrics row was lost. Now unmatched entries (fully trimmed turns) emit a standalone turn-metrics row at the top of the transcript, so the user still sees 'turn N ยท X tok' for unloaded turns. Note: trimming during generation only affects COMMITTED chunks (old turns). Provisional chunks (the in-flight turn) are never trimmed โ€” the big trim happens at seal when provisional โ†’ committed. This is by design. --- src/core/metrics/place.test.ts | 12 ++++++++---- src/core/metrics/place.ts | 17 +++++++++++++++++ 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/src/core/metrics/place.test.ts b/src/core/metrics/place.test.ts index d91f1ab..22f8639 100644 --- a/src/core/metrics/place.test.ts +++ b/src/core/metrics/place.test.ts @@ -391,7 +391,7 @@ describe("interleaveTurnMetrics", () => { expectTurnMetricsAt(rows, 4, "t1"); }); - it("more metrics than segments: only T entries placed (extra ignored)", () => { + it("more metrics than segments: unmatched entry emits standalone turn-metrics", () => { const g1 = userGroup(1, "q1"); const g2 = toolCallGroup(2, "s1", "c1"); const step1 = makeStep("s1", 100, 50); @@ -401,9 +401,13 @@ describe("interleaveTurnMetrics", () => { [makeEntry("t1", 100, 50, [step1]), makeEntry("t2", 200, 80, [step2])], ); - expect(rows).toHaveLength(4); - expectStepMetricsAt(rows, 2, "s1", 0); - expectTurnMetricsAt(rows, 3, "t1"); + // Unmatched entry (t2) emits a standalone turn-metrics row at the top. + expect(rows).toHaveLength(5); + expectTurnMetricsAt(rows, 0, "t2"); + expectGroupAt(rows, 1, g1); + expectGroupAt(rows, 2, g2); + expectStepMetricsAt(rows, 3, "s1", 0); + expectTurnMetricsAt(rows, 4, "t1"); }); it("turn with no steps emits only turn-metrics (no step-metrics)", () => { diff --git a/src/core/metrics/place.ts b/src/core/metrics/place.ts index 1009b15..b60c675 100644 --- a/src/core/metrics/place.ts +++ b/src/core/metrics/place.ts @@ -154,6 +154,23 @@ export function interleaveTurnMetrics( const rows: MetricsRow[] = []; const firstUserIdx = segmentStarts[0] ?? 0; + + // Emit turn-metrics rows for entries that weren't matched to any segment + // (fully trimmed turns โ€” their content was unloaded by the chat limit, but + // their aggregate metrics still show so the user knows what was trimmed). + for (let i = 0; i < entries.length; i++) { + if (usedEntries.has(i)) continue; + const e = entries[i]; + if (e === undefined || e.total === null) continue; + rows.push({ + kind: "turn-metrics", + turn: e.total, + turnNumber: i + 1, + cumulativeUsage: cumulativeByEntry[i] ?? e.total.usage, + prevTurnUsage: prevUsageByEntry[i] ?? null, + }); + } + for (let i = 0; i < firstUserIdx; i++) { const g = groups[i]; if (g !== undefined) { -- cgit v1.2.3