From 3aa31b607520dbc08ca0920bc4f7f5b1c1b91be6 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Fri, 25 Sep 2026 13:48:48 -0400 Subject: [PATCH 1/2] =?UTF-8?q?=F0=9F=93=8F=20fix:=20Keep=20the=20Context?= =?UTF-8?q?=20Gauge's=20Used=20Count=20Above=20Its=20Own=20Breakdown?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Chat/__tests__/useTokenUsage.spec.tsx | 22 +++++++++++++++++++ client/src/hooks/Chat/useTokenUsage.ts | 9 ++++++-- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/client/src/hooks/Chat/__tests__/useTokenUsage.spec.tsx b/client/src/hooks/Chat/__tests__/useTokenUsage.spec.tsx index 9e93a7c061c..c22f0e3be8c 100644 --- a/client/src/hooks/Chat/__tests__/useTokenUsage.spec.tsx +++ b/client/src/hooks/Chat/__tests__/useTokenUsage.spec.tsx @@ -138,6 +138,28 @@ describe('useTokenUsage — post-snapshot output', () => { expect(result.current.runwayTurns).toBe(2); }); + it('never reports less than the instructions and messages the snapshot breaks down', () => { + /** Remaining was measured as if only the 2 message tokens were sent, while the + * same snapshot publishes a 57-token system prompt: 7 used would leave the + * breakdown a negative message share. */ + const inconsistent = { + ...tailSnapshot, + remainingContextTokens: 199998, + completedOutputTokens: 5, + breakdown: { maxContextTokens: 200000, instructionTokens: 57, messageTokens: 2 }, + } as ContextSnapshot; + const { result } = renderTokenUsage(undefined, { snapshot: inconsistent }); + + expect(result.current.usedTokens).toBe(57 + 2 + 5); + }); + + it('keeps the remaining-based count when it exceeds the breakdown', () => { + const { result } = renderTokenUsage(); + + /** 195000 pre-invoke used covers content the 14000-token breakdown omits. */ + expect(result.current.usedTokens).toBe(197000); + }); + it('counts the finalized output in what a summarization could reclaim', () => { const { result } = renderTokenUsage(); diff --git a/client/src/hooks/Chat/useTokenUsage.ts b/client/src/hooks/Chat/useTokenUsage.ts index 96dcf4da471..9c54de2ce02 100644 --- a/client/src/hooks/Chat/useTokenUsage.ts +++ b/client/src/hooks/Chat/useTokenUsage.ts @@ -313,10 +313,15 @@ export default function useTokenUsage({ effective.remainingContextTokens != null ? normalizeTokenCount(effective.remainingContextTokens) : null; + const breakdownUsed = instructionTokens + normalizeTokenCount(breakdown.messageTokens); + /** A remaining count measured against a smaller instruction total than the + * snapshot publishes would put used below the instruction share the + * breakdown subtracts, hiding the Messages row. Used never undercuts the + * parts the breakdown itself reports. */ const baseUsed = remainingContextTokens != null - ? maxTokens - remainingContextTokens - : instructionTokens + normalizeTokenCount(breakdown.messageTokens); + ? Math.max(maxTokens - remainingContextTokens, breakdownUsed) + : breakdownUsed; /** The snapshot is pre-invoke: in-flight output rides on `liveTokens` (0 * unless streaming this branch), the last call's finalized output on * `completedOutputTokens`, and retained tool results on From 44d877717b6ce164e67a939728bd112f731d7297 Mon Sep 17 00:00:00 2001 From: Lia Date: Sun, 27 Sep 2026 12:33:17 +0000 Subject: [PATCH 2/2] =?UTF-8?q?=F0=9F=93=8F=20fix:=20Include=20summary=20t?= =?UTF-8?q?okens=20in=20context=20gauge=20breakdown=20floor?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Chat/__tests__/useTokenUsage.spec.tsx | 55 ++++++++++++++++++- client/src/hooks/Chat/useTokenUsage.ts | 10 ++-- client/src/utils/tokens.spec.ts | 9 ++- client/src/utils/tokens.ts | 10 ++-- 4 files changed, 72 insertions(+), 12 deletions(-) diff --git a/client/src/hooks/Chat/__tests__/useTokenUsage.spec.tsx b/client/src/hooks/Chat/__tests__/useTokenUsage.spec.tsx index c22f0e3be8c..442ca2872a8 100644 --- a/client/src/hooks/Chat/__tests__/useTokenUsage.spec.tsx +++ b/client/src/hooks/Chat/__tests__/useTokenUsage.spec.tsx @@ -153,10 +153,32 @@ describe('useTokenUsage — post-snapshot output', () => { expect(result.current.usedTokens).toBe(57 + 2 + 5); }); + it('includes the summary when a remaining-based count undercuts the breakdown', () => { + const inconsistent = { + ...tailSnapshot, + remainingContextTokens: 199998, + completedOutputTokens: 5, + breakdown: { + ...tailSnapshot.breakdown, + instructionTokens: 57, + summaryTokens: 11, + messageTokens: 2, + }, + } as ContextSnapshot; + const { result } = renderTokenUsage(undefined, { snapshot: inconsistent }); + + expect(result.current.usedTokens).toBe(57 + 11 + 2 + 5); + expect(result.current.usedTokens - 57 - 11).toBe(7); + }); + it('keeps the remaining-based count when it exceeds the breakdown', () => { - const { result } = renderTokenUsage(); + const withSummary = { + ...tailSnapshot, + breakdown: { ...tailSnapshot.breakdown, summaryTokens: 700 }, + } as ContextSnapshot; + const { result } = renderTokenUsage(undefined, { snapshot: withSummary }); - /** 195000 pre-invoke used covers content the 14000-token breakdown omits. */ + /** 195000 pre-invoke used covers content the 14700-token breakdown omits. */ expect(result.current.usedTokens).toBe(197000); }); @@ -179,6 +201,20 @@ describe('useTokenUsage — post-snapshot output', () => { breakdown: { maxContextTokens: 200000, instructionTokens: 4000, messageTokens }, }) as unknown as ContextSnapshot; + const summarizedLegacySnapshot = (): ContextSnapshot => ({ + ...legacySnapshot(2), + anchorMessageId: 'a2', + completedOutputTokens: 5, + breakdown: { ...legacySnapshot(2).breakdown, instructionTokens: 57, summaryTokens: 11 }, + }); + + it('includes the summary in a snapshot saved without remaining headroom', () => { + const { result } = renderTokenUsage(undefined, { snapshot: summarizedLegacySnapshot() }); + + expect(result.current.usedTokens).toBe(57 + 11 + 2 + 5); + expect(result.current.usedTokens - 57 - 11).toBe(7); + }); + it('projects a branch whose snapshots predate the remaining-token field', () => { /** Both readings are instruction+messages sums — 13000 then 14000, so the * call grew 1000. Reading the absent remaining as zero would score both @@ -267,6 +303,21 @@ describe('useTokenUsage — post-snapshot output', () => { expect(result.current.runwayTurns).toBe(194); }); + it('restores the summary in a legacy snapshot saved on the viewed branch', () => { + const saved = summarizedLegacySnapshot(); + const { result } = renderTokenUsage(new Map(), { + snapshot: { ...tailSnapshot, anchorMessageId: 'unrelated-branch' }, + messages: messages.map((message) => + message.messageId === 'a2' + ? ({ ...message, metadata: { contextUsage: saved } } as TMessage) + : message, + ), + }); + + expect(result.current.snapshot?.anchorMessageId).toBe('a2'); + expect(result.current.usedTokens).toBe(57 + 11 + 2 + 5); + }); + it('excludes the retained latest tool result from compaction savings', () => { const { result } = renderTokenUsage(undefined, { messages: messages.map((message) => diff --git a/client/src/hooks/Chat/useTokenUsage.ts b/client/src/hooks/Chat/useTokenUsage.ts index 9c54de2ce02..2af292eb147 100644 --- a/client/src/hooks/Chat/useTokenUsage.ts +++ b/client/src/hooks/Chat/useTokenUsage.ts @@ -313,11 +313,13 @@ export default function useTokenUsage({ effective.remainingContextTokens != null ? normalizeTokenCount(effective.remainingContextTokens) : null; - const breakdownUsed = instructionTokens + normalizeTokenCount(breakdown.messageTokens); + const breakdownUsed = + instructionTokens + + normalizeTokenCount(breakdown.summaryTokens) + + normalizeTokenCount(breakdown.messageTokens); /** A remaining count measured against a smaller instruction total than the - * snapshot publishes would put used below the instruction share the - * breakdown subtracts, hiding the Messages row. Used never undercuts the - * parts the breakdown itself reports. */ + * snapshot publishes would put used below the instruction and summary + * shares the breakdown subtracts, hiding the Messages row. */ const baseUsed = remainingContextTokens != null ? Math.max(maxTokens - remainingContextTokens, breakdownUsed) diff --git a/client/src/utils/tokens.spec.ts b/client/src/utils/tokens.spec.ts index 6f5dcef76d2..ddac55b6351 100644 --- a/client/src/utils/tokens.spec.ts +++ b/client/src/utils/tokens.spec.ts @@ -1016,14 +1016,19 @@ describe('per-message usage index (branch + total)', () => { 'a1', { contextBudget: 1000, - breakdown: { maxContextTokens: 1000, instructionTokens: 100, messageTokens: 60 }, + breakdown: { + maxContextTokens: 1000, + instructionTokens: 100, + summaryTokens: 25, + messageTokens: 60, + }, }, ], /** Nothing to read at all — skipped rather than counted as a full window. */ ['a2', { contextBudget: 1000, breakdown: { maxContextTokens: 1000 } }], ]); - expect(collectAnchorSeries(CONVO, 'a2', anchors)).toEqual([{ used: 160, basis: 'breakdown' }]); + expect(collectAnchorSeries(CONVO, 'a2', anchors)).toEqual([{ used: 185, basis: 'breakdown' }]); }); it('latestExchangeTokens sums the tail response and its user turn', () => { diff --git a/client/src/utils/tokens.ts b/client/src/utils/tokens.ts index c7205e15d1e..e3fc8d50a32 100644 --- a/client/src/utils/tokens.ts +++ b/client/src/utils/tokens.ts @@ -547,7 +547,7 @@ export function prunedBranchTokens( /** * One persisted snapshot's used-context reading, with the basis it was measured * on. `remaining` is the authoritative pre-invoke figure (`budget − remaining`); - * `breakdown` is the instruction+messages sum a snapshot saved before + * `breakdown` is the instructions+summary+messages sum a snapshot saved before * `remainingContextTokens` existed still supports. The two measure different * quantities (the backend's remaining covers content the breakdown does not), * so a growth delta may only compare readings of the same basis. @@ -615,8 +615,8 @@ export function collectAnchorSeries( snapshot.contextBudget ?? snapshot.breakdown?.maxContextTokens, ); const configuration = snapshotConfiguration(snapshot); - /** Same precedence as the render path's `baseUsed`: the backend's - * remaining headroom when it was saved, else the breakdown sum. */ + /** Runway growth keeps the raw remaining basis, even if the gauge floors + * that reading to the breakdown; older snapshots use all breakdown parts. */ if (snapshot.remainingContextTokens != null && budget > 0) { const remaining = normalizeTokenCount(snapshot.remainingContextTokens); series.push({ used: Math.max(0, budget - remaining), basis: 'remaining', configuration }); @@ -624,7 +624,9 @@ export function collectAnchorSeries( const used = normalizeTokenCount( snapshot.effectiveInstructionTokens ?? snapshot.breakdown?.instructionTokens, - ) + normalizeTokenCount(snapshot.breakdown?.messageTokens); + ) + + normalizeTokenCount(snapshot.breakdown?.summaryTokens) + + normalizeTokenCount(snapshot.breakdown?.messageTokens); if (used > 0) { series.push({ used, basis: 'breakdown', configuration }); }