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 session = await createSessionForTest();
|
||||||
|
|
||||||
const usage = session.convertUsage(
|
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 session = await createSessionForTest();
|
||||||
|
|
||||||
const usage = session.convertUsage({
|
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([
|
const queryFactory = createQueryFactoryForTurns([
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
@@ -600,20 +600,23 @@ describe("ClaudeAgentSession context window usage", () => {
|
|||||||
contextWindowMaxTokens: 200_000,
|
contextWindowMaxTokens: 200_000,
|
||||||
contextWindowUsedTokens: 999,
|
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({
|
expect(secondTurn.usage).toEqual({
|
||||||
inputTokens: 11,
|
inputTokens: 11,
|
||||||
cachedInputTokens: 6,
|
cachedInputTokens: 6,
|
||||||
outputTokens: 8,
|
outputTokens: 8,
|
||||||
totalCostUsd: 0.1,
|
totalCostUsd: 0.1,
|
||||||
contextWindowMaxTokens: 200_000,
|
contextWindowMaxTokens: 200_000,
|
||||||
contextWindowUsedTokens: 28,
|
contextWindowUsedTokens: 999,
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
await session.close();
|
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 session = await createSessionForTest();
|
||||||
|
|
||||||
const usage = session.convertUsage({
|
const usage = session.convertUsage({
|
||||||
|
|||||||
@@ -1460,7 +1460,6 @@ class ClaudeAgentSession implements AgentSession {
|
|||||||
|
|
||||||
const sdkMessage = this.toSdkUserMessage(prompt);
|
const sdkMessage = this.toSdkUserMessage(prompt);
|
||||||
this.lastForegroundPromptText = this.extractPromptText(prompt);
|
this.lastForegroundPromptText = this.extractPromptText(prompt);
|
||||||
this.lastContextWindowUsedTokens = undefined;
|
|
||||||
const turnId = this.createTurnId("foreground");
|
const turnId = this.createTurnId("foreground");
|
||||||
this.activeForegroundTurnId = turnId;
|
this.activeForegroundTurnId = turnId;
|
||||||
this.foregroundHasVisibleActivity = false;
|
this.foregroundHasVisibleActivity = false;
|
||||||
@@ -2304,7 +2303,6 @@ class ClaudeAgentSession implements AgentSession {
|
|||||||
this.notifySubscribers(event);
|
this.notifySubscribers(event);
|
||||||
this.activeForegroundTurnId = null;
|
this.activeForegroundTurnId = null;
|
||||||
this.lastForegroundPromptText = null;
|
this.lastForegroundPromptText = null;
|
||||||
this.lastContextWindowUsedTokens = undefined;
|
|
||||||
this.cancelCurrentTurn = null;
|
this.cancelCurrentTurn = null;
|
||||||
this.syncTurnState("foreground turn terminal");
|
this.syncTurnState("foreground turn terminal");
|
||||||
}
|
}
|
||||||
@@ -2320,12 +2318,10 @@ class ClaudeAgentSession implements AgentSession {
|
|||||||
if (this.activeForegroundTurnId) {
|
if (this.activeForegroundTurnId) {
|
||||||
this.activeForegroundTurnId = null;
|
this.activeForegroundTurnId = null;
|
||||||
this.lastForegroundPromptText = null;
|
this.lastForegroundPromptText = null;
|
||||||
this.lastContextWindowUsedTokens = undefined;
|
|
||||||
this.cancelCurrentTurn = null;
|
this.cancelCurrentTurn = null;
|
||||||
this.syncTurnState("foreground turn terminal");
|
this.syncTurnState("foreground turn terminal");
|
||||||
} else if (this.autonomousTurn) {
|
} else if (this.autonomousTurn) {
|
||||||
this.autonomousTurn = null;
|
this.autonomousTurn = null;
|
||||||
this.lastContextWindowUsedTokens = undefined;
|
|
||||||
this.syncTurnState("autonomous turn terminal");
|
this.syncTurnState("autonomous turn terminal");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2338,7 +2334,6 @@ class ClaudeAgentSession implements AgentSession {
|
|||||||
this.autonomousTurn = {
|
this.autonomousTurn = {
|
||||||
id: this.createTurnId("autonomous"),
|
id: this.createTurnId("autonomous"),
|
||||||
};
|
};
|
||||||
this.lastContextWindowUsedTokens = undefined;
|
|
||||||
this.notifySubscribers({ type: "turn_started", provider: "claude" });
|
this.notifySubscribers({ type: "turn_started", provider: "claude" });
|
||||||
this.syncTurnState("autonomous turn started");
|
this.syncTurnState("autonomous turn started");
|
||||||
}
|
}
|
||||||
@@ -2349,7 +2344,6 @@ class ClaudeAgentSession implements AgentSession {
|
|||||||
}
|
}
|
||||||
this.notifySubscribers({ type: "turn_completed", provider: "claude" });
|
this.notifySubscribers({ type: "turn_completed", provider: "claude" });
|
||||||
this.autonomousTurn = null;
|
this.autonomousTurn = null;
|
||||||
this.lastContextWindowUsedTokens = undefined;
|
|
||||||
this.syncTurnState("autonomous turn completed");
|
this.syncTurnState("autonomous turn completed");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2920,8 +2914,14 @@ class ClaudeAgentSession implements AgentSession {
|
|||||||
usage.contextWindowMaxTokens = contextWindowMaxTokens;
|
usage.contextWindowMaxTokens = contextWindowMaxTokens;
|
||||||
}
|
}
|
||||||
if (typeof this.lastContextWindowUsedTokens === "number") {
|
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;
|
usage.contextWindowUsedTokens = this.lastContextWindowUsedTokens;
|
||||||
} else if (message.usage) {
|
} 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 & {
|
const usageWithCacheCreation = message.usage as typeof message.usage & {
|
||||||
cache_creation_input_tokens?: number;
|
cache_creation_input_tokens?: number;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user