diff --git a/packages/server/src/server/agent/providers/acp-agent.test.ts b/packages/server/src/server/agent/providers/acp-agent.test.ts index 9d106a2c1a..f847745d69 100644 --- a/packages/server/src/server/agent/providers/acp-agent.test.ts +++ b/packages/server/src/server/agent/providers/acp-agent.test.ts @@ -825,6 +825,46 @@ describe("mapACPUsage", () => { }); }); +describe("ACP context-window usage", () => { + async function emitUsageUpdate(update: { + used: number; + size: number; + }): Promise<{ events: unknown[] }> { + const session = createSessionWithConfig({ provider: "dsh" }); + asInternals(session).sessionId = "session-1"; + const events: unknown[] = []; + session.subscribe((event) => { + if (event.type === "usage_updated") events.push(event); + }); + await session.sessionUpdate({ + sessionId: "session-1", + update: { sessionUpdate: "usage_update", used: update.used, size: update.size }, + }); + return { events }; + } + + test("forwards usage_update as context-window usage state", async () => { + const { events } = await emitUsageUpdate({ used: 13_759, size: 1_000_000 }); + + expect(events).toEqual([ + { + type: "usage_updated", + provider: "dsh", + usage: { contextWindowMaxTokens: 1_000_000, contextWindowUsedTokens: 13_759 }, + }, + ]); + }); + + test("emits nothing when size and used cannot both drive a meter", async () => { + await expect( + emitUsageUpdate({ used: -1, size: 0 }).then((result) => result.events), + ).resolves.toEqual([]); + await expect( + emitUsageUpdate({ used: 13_759, size: 0 }).then((result) => result.events), + ).resolves.toEqual([]); + }); +}); + describe("deriveModesFromACP", () => { test("prefers explicit ACP mode state", () => { const result = deriveModesFromACP( diff --git a/packages/server/src/server/agent/providers/acp-agent.ts b/packages/server/src/server/agent/providers/acp-agent.ts index 524ede92dd..de8055c787 100644 --- a/packages/server/src/server/agent/providers/acp-agent.ts +++ b/packages/server/src/server/agent/providers/acp-agent.ts @@ -3178,8 +3178,29 @@ export class ACPAgentSession implements AgentSession, ACPClient { } } + // ACP reports context occupancy out of band from the prompt response, so the + // composer's context meter cannot read it from `turn_completed`. Forward it as + // the same `usage_updated` snapshot the native adapters emit, preserving the + // turn usage this session already recorded. The guards match the meter's own + // validity rules, so a provider cannot push a window the meter would reject. private handleUsageUpdate(update: UsageUpdate): void { - void update; + const contextWindowMaxTokens = + Number.isFinite(update.size) && update.size > 0 ? update.size : undefined; + const contextWindowUsedTokens = + Number.isFinite(update.used) && update.used >= 0 ? update.used : undefined; + if (contextWindowMaxTokens === undefined || contextWindowUsedTokens === undefined) { + return; + } + this.pushEvent({ + type: "usage_updated", + provider: this.provider, + usage: { + ...this.currentTurnUsage, + contextWindowMaxTokens, + contextWindowUsedTokens, + }, + turnId: this.activeForegroundTurnId ?? undefined, + }); } private handlePromptResponse(response: PromptResponse, turnId: string): void {