From b897c59e9c72cc3b0721287e9860fc3200d41d38 Mon Sep 17 00:00:00 2001 From: Slava Goltser Date: Wed, 29 Jul 2026 10:10:18 -0400 Subject: [PATCH] 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> --- .../src/server/agent/agent-manager.test.ts | 17 ++++++++++++++++- .../server/src/server/agent/agent-manager.ts | 7 ++++++- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/packages/server/src/server/agent/agent-manager.test.ts b/packages/server/src/server/agent/agent-manager.test.ts index 1ff1b1933..d55f1c598 100644 --- a/packages/server/src/server/agent/agent-manager.test.ts +++ b/packages/server/src/server/agent/agent-manager.test.ts @@ -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", diff --git a/packages/server/src/server/agent/agent-manager.ts b/packages/server/src/server/agent/agent-manager.ts index e6c4e68a8..c18e8c902 100644 --- a/packages/server/src/server/agent/agent-manager.ts +++ b/packages/server/src/server/agent/agent-manager.ts @@ -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";