From 1029c9e7bc0a13ec7dd2a72877721259afaee08f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E9=98=BF=E6=B1=9F=28Relakkes?= =?UTF-8?q?=29?= Date: Tue, 5 May 2026 19:52:48 +0800 Subject: [PATCH] fix: align transcript context fallback with cli totals The live desktop context path already routes through the CLI get_context_usage control request and the shared analyzeContextUsage implementation. The offline transcript fallback was still using its own prompt-token-only estimate, so disconnected sessions could report a lower context total than the live /context path. The fallback now reuses the same current-context total helper and includes output tokens as next-turn context. Constraint: Desktop must still show context estimates when the CLI process is not running Rejected: Keep transcript fallback prompt-only | it diverges from the live CLI /context total and can drop after responses Confidence: high Scope-risk: narrow Directive: Transcript context fallback must stay aligned with calculateCurrentContextTokenTotal Tested: bun test src/server/__tests__/conversations.test.ts --test-name-pattern "context-only|structured session inspection|reconstruct Sonnet 4.6 transcript usage" Tested: bun test src/utils/__tests__/context.test.ts Tested: bun run check:server --- src/server/__tests__/conversations.test.ts | 3 ++- src/server/services/sessionService.ts | 10 +++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/server/__tests__/conversations.test.ts b/src/server/__tests__/conversations.test.ts index 4beaea2c..b8b8dda1 100644 --- a/src/server/__tests__/conversations.test.ts +++ b/src/server/__tests__/conversations.test.ts @@ -372,8 +372,9 @@ describe('ConversationService', () => { expect(usage?.models[0]?.model).toBe('claude-sonnet-4-6') expect(usage?.models[0]?.contextWindow).toBe(200_000) expect(contextEstimate?.model).toBe('claude-sonnet-4-6') - expect(contextEstimate?.totalTokens).toBe(100) + expect(contextEstimate?.totalTokens).toBe(120) expect(contextEstimate?.rawMaxTokens).toBe(200_000) + expect(contextEstimate?.categories.some((category) => category.name === 'Output tokens' && category.tokens === 20)).toBe(true) } finally { if (previousConfigDir === undefined) { delete process.env.CLAUDE_CONFIG_DIR diff --git a/src/server/services/sessionService.ts b/src/server/services/sessionService.ts index 9e9f916d..eb75e7ed 100644 --- a/src/server/services/sessionService.ts +++ b/src/server/services/sessionService.ts @@ -13,6 +13,7 @@ import { sanitizePath as sanitizePortablePath } from '../../utils/sessionStorage import type { FileHistorySnapshot } from '../../utils/fileHistory.js' import { calculateUSDCost, MODEL_COSTS } from '../../utils/modelCost.js' import { + calculateCurrentContextTokenTotal, MODEL_CONTEXT_WINDOW_DEFAULT, getContextWindowForModel, getModelMaxOutputTokens, @@ -891,12 +892,19 @@ export class SessionService { if (!latest) return null const rawMaxTokens = this.getTranscriptContextWindow(latest.model) - const totalTokens = latest.inputTokens + latest.cacheReadInputTokens + latest.cacheCreationInputTokens + const promptTokens = latest.inputTokens + latest.cacheReadInputTokens + latest.cacheCreationInputTokens + const totalTokens = calculateCurrentContextTokenTotal(promptTokens, { + input_tokens: latest.inputTokens, + output_tokens: latest.outputTokens, + cache_read_input_tokens: latest.cacheReadInputTokens, + cache_creation_input_tokens: latest.cacheCreationInputTokens, + }) const percentage = rawMaxTokens > 0 ? Math.round((totalTokens / rawMaxTokens) * 100) : 0 const categories: TranscriptContextEstimate['categories'] = [ { name: 'Input tokens', tokens: latest.inputTokens, color: '#8f3217' }, { name: 'Cache read', tokens: latest.cacheReadInputTokens, color: '#0f5c8f' }, { name: 'Cache write', tokens: latest.cacheCreationInputTokens, color: '#7c3aed' }, + { name: 'Output tokens', tokens: latest.outputTokens, color: '#2f7d32' }, { name: 'Free space', tokens: Math.max(0, rawMaxTokens - totalTokens), color: '#a1a1aa', isDeferred: true }, ].filter((category) => category.tokens > 0)