From 1151c4f6fcfb4b879778fd46dbc55e99c1d2a154 Mon Sep 17 00:00:00 2001 From: DarkPhilosophy <19309990+DarkPhilosophy@users.noreply.github.com> Date: Wed, 15 Jul 2026 05:04:27 +0300 Subject: [PATCH] fix(advisor): share usage-limit classification --- .../coding-agent/src/session/agent-session.ts | 4 +- .../coding-agent/test/advisor-toggle.test.ts | 46 ++++++++++++++++++- 2 files changed, 46 insertions(+), 4 deletions(-) diff --git a/packages/coding-agent/src/session/agent-session.ts b/packages/coding-agent/src/session/agent-session.ts index e2549bae1..459fd6bda 100644 --- a/packages/coding-agent/src/session/agent-session.ts +++ b/packages/coding-agent/src/session/agent-session.ts @@ -112,7 +112,6 @@ import { clearAnthropicFastModeFallback, deriveClaudeDeviceId, Effort, - isUsageLimitOutcome, parseRateLimitReason, realizesPriorityServiceTier, resolveModelServiceTier, @@ -130,7 +129,6 @@ import { modelsAreEqual } from "@oh-my-pi/pi-catalog/models"; import { MacOSPowerAssertion } from "@oh-my-pi/pi-natives"; import { escapeXmlText, - extractHttpStatusFromError, extractRetryHint, formatDuration, getAgentDbPath, @@ -3099,7 +3097,7 @@ export class AgentSession { // only — other failures keep the plain retry/notify path (never // suspect-mark a credential on a transient advisor error). const message = error instanceof Error ? error.message : String(error); - if (!isUsageLimitOutcome(extractHttpStatusFromError(error), message)) return; + if (!AIError.isUsageLimit(error)) return; const result = await this.#modelRegistry.authStorage.markUsageLimitReached( advisorModel.provider, advisorProviderSessionId, diff --git a/packages/coding-agent/test/advisor-toggle.test.ts b/packages/coding-agent/test/advisor-toggle.test.ts index 5a93da56b..5287c6693 100644 --- a/packages/coding-agent/test/advisor-toggle.test.ts +++ b/packages/coding-agent/test/advisor-toggle.test.ts @@ -1,8 +1,10 @@ -import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from "bun:test"; +import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "bun:test"; import * as fs from "node:fs"; import * as path from "node:path"; import { Agent, type AgentMessage } from "@oh-my-pi/pi-agent-core"; import type { Model } from "@oh-my-pi/pi-ai"; +import * as AIError from "@oh-my-pi/pi-ai/error"; +import { createMockModel } from "@oh-my-pi/pi-ai/providers/mock"; import { getBundledModel } from "@oh-my-pi/pi-catalog/models"; import { ModelRegistry } from "@oh-my-pi/pi-coding-agent/config/model-registry"; import { Settings } from "@oh-my-pi/pi-coding-agent/config/settings"; @@ -296,4 +298,46 @@ describe("AgentSession advisor toggle", () => { expect(sid).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i); expect(sid).not.toContain("-advisor"); }); + it("marks structurally classified advisor usage limits", async () => { + const mock = createMockModel({ responses: [{ content: ["primary complete"] }] }); + const primaryAgent = new Agent({ + initialState: { + model, + systemPrompt: ["Test"], + tools: [], + messages: [], + }, + streamFn: mock.stream, + }); + const settings = Settings.isolated({ "compaction.enabled": false }); + settings.setModelRole("advisor", `${model.provider}/${model.id}`); + const quotaSession = new AgentSession({ + agent: primaryAgent, + sessionManager: SessionManager.inMemory(), + settings, + modelRegistry, + advisorTools: [], + }); + + try { + expect(quotaSession.setAdvisorEnabled(true)).toBe(true); + const advisorAgent = quotaSession.getAdvisorAgent(); + if (!advisorAgent) throw new Error("Expected advisor agent to exist"); + vi.spyOn(advisorAgent, "prompt").mockRejectedValue( + new AIError.ProviderHttpError("Generic provider failure", 429, { code: "insufficient_quota" }), + ); + const markUsageLimitReached = vi + .spyOn(authStorage, "markUsageLimitReached") + .mockResolvedValue({ switched: false }); + + await quotaSession.prompt("Trigger advisor"); + await quotaSession.waitForIdle(); + + expect(markUsageLimitReached).toHaveBeenCalledTimes(1); + expect(markUsageLimitReached.mock.calls[0]?.[0]).toBe(model.provider); + } finally { + await quotaSession.dispose(); + vi.restoreAllMocks(); + } + }); });