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
This commit is contained in:
程序员阿江(Relakkes) 2026-05-05 19:52:48 +08:00
parent f02fe83e28
commit 1029c9e7bc
2 changed files with 11 additions and 2 deletions

View File

@ -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

View File

@ -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)