From 8995f56d1f6bb5e113233d9a9d13805fd3ab2939 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: Mon, 20 Jul 2026 20:20:25 +0800 Subject: [PATCH] fix(server): preserve context estimates for empty usage #1063 --- src/server/__tests__/conversations.test.ts | 70 ++++++++++++++++++++++ src/server/services/sessionService.ts | 17 ++---- 2 files changed, 76 insertions(+), 11 deletions(-) diff --git a/src/server/__tests__/conversations.test.ts b/src/server/__tests__/conversations.test.ts index 0c37bd7a..035fa185 100644 --- a/src/server/__tests__/conversations.test.ts +++ b/src/server/__tests__/conversations.test.ts @@ -891,6 +891,76 @@ describe('ConversationService', () => { } }) + it('should fall back to transcript estimates when provider usage is empty or zero', async () => { + const previousConfigDir = process.env.CLAUDE_CONFIG_DIR + const previousNodeEnv = process.env.NODE_ENV + const tmpConfigDir = await fs.mkdtemp(path.join(os.tmpdir(), 'claude-transcript-zero-usage-')) + const workDir = await fs.mkdtemp(path.join(os.tmpdir(), 'claude-workdir-zero-usage-')) + process.env.CLAUDE_CONFIG_DIR = tmpConfigDir + process.env.NODE_ENV = 'development' + + try { + const svc = new SessionService() + + for (const usage of [ + {}, + { + input_tokens: 0, + output_tokens: 0, + cache_read_input_tokens: 0, + cache_creation_input_tokens: 0, + }, + ]) { + const { sessionId } = await svc.createSession(workDir) + const found = await svc.findSessionFile(sessionId) + expect(found).not.toBeNull() + + await fs.appendFile(found!.filePath, JSON.stringify({ + type: 'user', + uuid: crypto.randomUUID(), + timestamp: '2026-07-20T12:00:00.000Z', + cwd: workDir, + message: { + role: 'user', + content: [{ type: 'text', text: 'Estimate this transcript even when the provider does not report token counts.' }], + }, + }) + '\n') + await fs.appendFile(found!.filePath, JSON.stringify({ + type: 'assistant', + uuid: crypto.randomUUID(), + timestamp: '2026-07-20T12:00:01.000Z', + cwd: workDir, + message: { + role: 'assistant', + model: 'claude-sonnet-4-6', + content: [{ type: 'text', text: 'The local transcript estimate should remain available.' }], + usage, + }, + }) + '\n') + + const contextEstimate = await svc.getTranscriptContextEstimate(sessionId) + const inspectionSnapshot = await svc.getInspectionTranscriptSnapshot(sessionId) + + expect(contextEstimate?.model).toBe('claude-sonnet-4-6') + expect(contextEstimate?.totalTokens).toBeGreaterThan(0) + expect(inspectionSnapshot?.contextEstimate).toEqual(contextEstimate) + } + } finally { + if (previousConfigDir === undefined) { + delete process.env.CLAUDE_CONFIG_DIR + } else { + process.env.CLAUDE_CONFIG_DIR = previousConfigDir + } + if (previousNodeEnv === undefined) { + delete process.env.NODE_ENV + } else { + process.env.NODE_ENV = previousNodeEnv + } + await fs.rm(tmpConfigDir, { recursive: true, force: true }) + await fs.rm(workDir, { recursive: true, force: true }) + } + }) + it('should use active provider model context windows for transcript estimates', async () => { const previousConfigDir = process.env.CLAUDE_CONFIG_DIR const previousNodeEnv = process.env.NODE_ENV diff --git a/src/server/services/sessionService.ts b/src/server/services/sessionService.ts index 078fdc8a..6db9ce9e 100644 --- a/src/server/services/sessionService.ts +++ b/src/server/services/sessionService.ts @@ -2504,8 +2504,6 @@ export class SessionService { const outputTokens = typeof usage.output_tokens === 'number' ? usage.output_tokens : 0 const cacheReadInputTokens = typeof usage.cache_read_input_tokens === 'number' ? usage.cache_read_input_tokens : 0 const cacheCreationInputTokens = typeof usage.cache_creation_input_tokens === 'number' ? usage.cache_creation_input_tokens : 0 - const promptTokens = inputTokens + cacheReadInputTokens + cacheCreationInputTokens - if (promptTokens === 0 && outputTokens === 0) continue latest = { model, @@ -2774,16 +2772,13 @@ export class SessionService { const webSearchRequests = typeof usage.server_tool_use?.web_search_requests === 'number' ? usage.server_tool_use.web_search_requests : 0 - const promptTokens = inputTokens + cacheReadInputTokens + cacheCreationInputTokens - if (promptTokens !== 0 || outputTokens !== 0) { - latestContextUsage = { - model, - inputTokens, - outputTokens, - cacheReadInputTokens, - cacheCreationInputTokens, - } + latestContextUsage = { + model, + inputTokens, + outputTokens, + cacheReadInputTokens, + cacheCreationInputTokens, } if (