diff --git a/packages/server/src/server/agent/providers/claude-agent.test.ts b/packages/server/src/server/agent/providers/claude-agent.test.ts index 68732b9e9..b6863a3f7 100644 --- a/packages/server/src/server/agent/providers/claude-agent.test.ts +++ b/packages/server/src/server/agent/providers/claude-agent.test.ts @@ -358,7 +358,7 @@ describe("ClaudeAgentSession context window usage", () => { }); } - test("convertUsage includes contextWindowMaxTokens and derives used tokens from result usage", async () => { + test("convertUsage includes contextWindowMaxTokens and derives used tokens from result usage as initial fallback", async () => { const session = await createSessionForTest(); const usage = session.convertUsage( @@ -388,7 +388,7 @@ describe("ClaudeAgentSession context window usage", () => { }); }); - test("contextWindowUsedTokens is derived from result usage when task_progress is missing", async () => { + test("contextWindowUsedTokens falls back to result usage when no task_progress was received", async () => { const session = await createSessionForTest(); const usage = session.convertUsage({ @@ -508,7 +508,7 @@ describe("ClaudeAgentSession context window usage", () => { }); }); - test("contextWindowUsedTokens resets between turns and falls back to result usage", async () => { + test("contextWindowUsedTokens persists across turns from last task_progress", async () => { const queryFactory = createQueryFactoryForTurns([ [ { @@ -600,20 +600,23 @@ describe("ClaudeAgentSession context window usage", () => { contextWindowMaxTokens: 200_000, contextWindowUsedTokens: 999, }); + // Turn 2 has no task_progress, so contextWindowUsedTokens retains the + // last known value from turn 1 rather than deriving from accumulated + // result.usage (which would be incorrect — those are session-level totals). expect(secondTurn.usage).toEqual({ inputTokens: 11, cachedInputTokens: 6, outputTokens: 8, totalCostUsd: 0.1, contextWindowMaxTokens: 200_000, - contextWindowUsedTokens: 28, + contextWindowUsedTokens: 999, }); } finally { await session.close(); } }); - test("convertUsage derives used tokens even when modelUsage is missing", async () => { + test("convertUsage derives used tokens from result usage as fallback when task_progress is missing", async () => { const session = await createSessionForTest(); const usage = session.convertUsage({ diff --git a/packages/server/src/server/agent/providers/claude-agent.ts b/packages/server/src/server/agent/providers/claude-agent.ts index 364a8af77..e780586e2 100644 --- a/packages/server/src/server/agent/providers/claude-agent.ts +++ b/packages/server/src/server/agent/providers/claude-agent.ts @@ -1460,7 +1460,6 @@ class ClaudeAgentSession implements AgentSession { const sdkMessage = this.toSdkUserMessage(prompt); this.lastForegroundPromptText = this.extractPromptText(prompt); - this.lastContextWindowUsedTokens = undefined; const turnId = this.createTurnId("foreground"); this.activeForegroundTurnId = turnId; this.foregroundHasVisibleActivity = false; @@ -2304,7 +2303,6 @@ class ClaudeAgentSession implements AgentSession { this.notifySubscribers(event); this.activeForegroundTurnId = null; this.lastForegroundPromptText = null; - this.lastContextWindowUsedTokens = undefined; this.cancelCurrentTurn = null; this.syncTurnState("foreground turn terminal"); } @@ -2320,12 +2318,10 @@ class ClaudeAgentSession implements AgentSession { if (this.activeForegroundTurnId) { this.activeForegroundTurnId = null; this.lastForegroundPromptText = null; - this.lastContextWindowUsedTokens = undefined; this.cancelCurrentTurn = null; this.syncTurnState("foreground turn terminal"); } else if (this.autonomousTurn) { this.autonomousTurn = null; - this.lastContextWindowUsedTokens = undefined; this.syncTurnState("autonomous turn terminal"); } } @@ -2338,7 +2334,6 @@ class ClaudeAgentSession implements AgentSession { this.autonomousTurn = { id: this.createTurnId("autonomous"), }; - this.lastContextWindowUsedTokens = undefined; this.notifySubscribers({ type: "turn_started", provider: "claude" }); this.syncTurnState("autonomous turn started"); } @@ -2349,7 +2344,6 @@ class ClaudeAgentSession implements AgentSession { } this.notifySubscribers({ type: "turn_completed", provider: "claude" }); this.autonomousTurn = null; - this.lastContextWindowUsedTokens = undefined; this.syncTurnState("autonomous turn completed"); } @@ -2920,8 +2914,14 @@ class ClaudeAgentSession implements AgentSession { usage.contextWindowMaxTokens = contextWindowMaxTokens; } if (typeof this.lastContextWindowUsedTokens === "number") { + // task_progress.total_tokens is the accurate context window fill level. + // Prefer it over result.usage which contains accumulated session totals. usage.contextWindowUsedTokens = this.lastContextWindowUsedTokens; } else if (message.usage) { + // Fallback: derive from result.usage when no task_progress has been + // received yet. These values are accumulated across all API calls, but + // for the first turn they equal the per-call values so the estimate is + // reasonable. Once a task_progress arrives it takes over permanently. const usageWithCacheCreation = message.usage as typeof message.usage & { cache_creation_input_tokens?: number; };