fix: preserve context window data when turn_completed lacks usage (#2494)

* fix: preserve context window data when turn_completed lacks usage

Merge incoming usage into agent.lastUsage instead of replacing it
wholesale. Some providers (Claude autonomous turns, OpenCode) emit
turn_completed without a usage payload, which previously wiped the
contextWindowMaxTokens/contextWindowUsedTokens accumulated during
streaming usage_updated events. The result was the context window
meter disappearing from the composer once the agent finished work.

* test(server): preserve usage after autonomous completion

---------

Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
This commit is contained in:
Slava Goltser
2026-07-29 22:10:18 +08:00
committed by GitHub
co-authored by paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
parent acb8506d1f
commit b897c59e9c
2 changed files with 22 additions and 2 deletions
@@ -4757,7 +4757,7 @@ test("replaceAgentRun stays running when a stale old terminal arrives before the
unsubscribe();
});
test("applies live autonomous events while no foreground run is active", async () => {
test("applies live autonomous events and preserves usage omitted from completion", async () => {
const workdir = mkdtempSync(join(tmpdir(), "agent-manager-live-events-"));
const storagePath = join(workdir, "agents");
const storage = new AgentStorage(storagePath, logger);
@@ -4816,6 +4816,16 @@ test("applies live autonomous events while no foreground run is active", async (
provider: "codex",
turnId: autonomousTurnId,
});
capturedSession!.pushEvent({
type: "usage_updated",
provider: "codex",
usage: {
inputTokens: 10,
contextWindowMaxTokens: 200_000,
contextWindowUsedTokens: 175,
},
turnId: autonomousTurnId,
});
capturedSession!.pushEvent({
type: "timeline",
provider: "codex",
@@ -4831,6 +4841,11 @@ test("applies live autonomous events while no foreground run is active", async (
const updated = manager.getAgent(snapshot.id);
expect(updated?.lifecycle).toBe("idle");
expect(updated?.lastUsage).toEqual({
inputTokens: 10,
contextWindowMaxTokens: 200_000,
contextWindowUsedTokens: 175,
});
expect(manager.getTimeline(snapshot.id)).toContainEqual({
type: "assistant_message",
text: "AUTONOMOUS_PUMP_MESSAGE",
@@ -3552,7 +3552,12 @@ export class AgentManager {
},
"agent.manager.turn.completed",
);
agent.lastUsage = event.usage;
if (event.usage) {
agent.lastUsage = { ...agent.lastUsage, ...event.usage };
}
// If no usage on turn_completed, keep lastUsage as-is so context window
// data accumulated during streaming isn't lost when the provider omits
// it from the completion event.
agent.lastError = undefined;
if (!isForegroundEvent && agent.lifecycle !== "idle" && !agent.pendingReplacement) {
(agent as ActiveManagedAgent).lifecycle = "idle";