Persist context window usage across turns from last task_progress
Stop resetting lastContextWindowUsedTokens between turns. The result.usage fallback contains accumulated session totals which grow incorrectly across turns. Once a task_progress arrives, its value is the accurate context fill level and should be retained until the next task_progress supersedes it.
This commit is contained in:
@@ -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({
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user