diff --git a/packages/server/src/server/agent/providers/claude/agent.error-result.test.ts b/packages/server/src/server/agent/providers/claude/agent.error-result.test.ts index 063c35346a..9bbaa49186 100644 --- a/packages/server/src/server/agent/providers/claude/agent.error-result.test.ts +++ b/packages/server/src/server/agent/providers/claude/agent.error-result.test.ts @@ -13,6 +13,12 @@ interface ScriptedTurn { before?: string; after?: string; isError: boolean; + usage?: { + inputTokens: number; + cachedInputTokens: number; + outputTokens: number; + totalCostUsd: number; + }; } // A query dependency that speaks Claude Code's stream interface, one scripted turn per prompt. @@ -59,11 +65,11 @@ function scriptedQuery(turns: ScriptedTurn[]): ClaudeQueryFactory { num_turns: 1, result: turn.isError ? (turn.errorReply ?? "") : (turn.after ?? turn.errorReply ?? ""), stop_reason: "stop_sequence", - total_cost_usd: 0, + total_cost_usd: turn.usage?.totalCostUsd ?? 0, usage: { - input_tokens: 0, - output_tokens: 0, - cache_read_input_tokens: 0, + input_tokens: turn.usage?.inputTokens ?? 0, + output_tokens: turn.usage?.outputTokens ?? 0, + cache_read_input_tokens: turn.usage?.cachedInputTokens ?? 0, }, modelUsage: {}, permission_denials: [], @@ -148,6 +154,24 @@ test("a turn Claude Code ends on an API error fails with the error, shown once", expect(assistantTexts(next)).toEqual(["Done."]); }); +test("a failed turn retains its final token counts and cost", async () => { + const usage = { + inputTokens: 120, + cachedInputTokens: 80, + outputTokens: 30, + totalCostUsd: 0.012, + }; + const [failed] = await runTurns([ + { before: "Checking the deploy doc.", errorReply: LIMIT, isError: true, usage }, + ]); + + expect(failed).toContainEqual( + expect.objectContaining({ type: "usage_updated", usage: expect.objectContaining(usage) }), + ); + expect(terminal(failed)).toMatchObject({ type: "turn_failed", error: LIMIT }); + expect(assistantTexts(failed)).toEqual(["Checking the deploy doc."]); +}); + test("an error reply the turn moves past is still shown", async () => { const [recovered, completed] = await runTurns([ { diff --git a/packages/server/src/server/agent/providers/claude/agent.ts b/packages/server/src/server/agent/providers/claude/agent.ts index 0d180b5801..cad0d23762 100644 --- a/packages/server/src/server/agent/providers/claude/agent.ts +++ b/packages/server/src/server/agent/providers/claude/agent.ts @@ -4998,6 +4998,7 @@ class ClaudeAgentSession implements AgentSession { .map((item) => (item.type === "assistant_message" ? item.text : "")) .join("\n") .trim(); + if (usage) events.push({ type: "usage_updated", provider: "claude", usage }); events.push(...this.sidechainTracker.finishAll("failed")); events.push(this.buildTurnFailedEvent(resultText || withheldText || "Claude run failed")); return;