mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-06 10:48:12 +02:00
fix(claude): recognize ACP quota fallback errors (#13831)
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work. > - The Claude adapter reports failures to the recovery system. > - Recovery must distinguish account quota from other provider limits. > - The Claude ACP bridge has a default message for an account with no quota. > - The current classifier misses that message and returns `acpx_turn_failed`. > - This pull request recognizes that exact typed message and uses the existing quota wait. ## Linked Issues or Issue Description Refs #13651. This is a narrow follow-up to its typed quota classification. Related PRs #13549 and #10276 address broader quota classification and reset handling. This change covers the bridge's exact default quota message. **What happened?** A typed Claude ACP `limit` failure with the title `The Claude account has no available quota.` returns `acpx_turn_failed`. The recovery result has no quota label. **Expected behavior** Return `provider_quota`. Use the existing one-hour quota backoff when the provider gives no reset time. Keep context, turn, rate, and configured budget limits out of the quota path. **Steps to reproduce** 1. Use the local ACP fixture to return the exact title above with category `limit` and severity `error`. 2. Run the Claude adapter in oneshot or persistent mode. 3. Before this change, the new regression cases receive `acpx_turn_failed` instead of `provider_quota` on ACPX 0.12.0 and 0.13.1. **Paperclip version or commit** Reproduced on `a959e4750`. Rebased onto current `master` before submission. **Deployment mode** Local source checkout with isolated ACP child-process fixtures. No live provider calls or customer-stack changes. ## What Changed - Recognize the exact Claude bridge quota fallback only for typed `limit` failures. - Add real-process regression cases for both pinned ACPX versions and both session modes. - Verify that unrelated categories, positive quota wording, and historical generic limit errors do not imply quota exhaustion. - Document the fallback and its existing recovery backoff. ## Verification - Red/green reproduction: all four new fallback cases failed before the classifier change and passed afterward. - 108 targeted tests passed across Claude ACP, quota, parser, and server recovery suites. - Claude adapter typecheck and build passed. - Real-process tests verify that provider text stays out of results and logs. - Full local `pnpm -r typecheck` and `pnpm build` passed. - The full local `pnpm test:run` attempt stopped after embedded PostgreSQL could not load a missing library symlink. The dependency setup was repaired in the worktree. The isolated database test then passed. The complete test suites passed in GitHub CI. - All 54 latest-head checks passed on `3d4d4e65386c5b6023ba34e6a2abcd94cff9a9bc`. Two optional Storybook jobs were skipped. - Greptile: 5/5, with no inline review threads or requested changes. ## Risks - Low risk. An exact match is required inside an existing typed `limit` failure. - If upstream changes this wording, this fallback can stop matching. Existing quota-message detection remains in place. - No schema, credential, or permission changes. Historical generic errors remain ambiguous and are not reclassified. ## Model Used OpenAI Codex, GPT-6. The session identifies the model family as GPT-6 but does not expose a more specific model ID or context-window size. Used reasoning, repository inspection, code editing, and terminal test execution. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] All Paperclip CI gates are green - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge Co-authored-by: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
a10702a878
commit
f447990d77
3 files changed
+39
-2
No files matched your search
@@ -4,7 +4,7 @@ import path from "node:path";
|
||||
import { createRequire } from "node:module";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { afterEach, expect, it } from "vitest";
|
||||
import { createClaudeAcpExecutor } from "./acp.js";
|
||||
import { classifyClaudeTerminalSessionFailure, createClaudeAcpExecutor } from "./acp.js";
|
||||
import type { AcpxEngineExecutorOptions } from "@paperclipai/adapter-utils/acpx-engine/execute";
|
||||
|
||||
const repoRoot = fileURLToPath(new URL("../../../../..", import.meta.url));
|
||||
@@ -82,12 +82,37 @@ it("classifies quota without a reset time for the existing recovery backoff", as
|
||||
expect(result.retryNotBefore).toBeUndefined();
|
||||
});
|
||||
|
||||
it.each([
|
||||
["0.12.0", "oneshot"],
|
||||
["0.12.0", "persistent"],
|
||||
["0.13.1", "oneshot"],
|
||||
["0.13.1", "persistent"],
|
||||
])("recognizes the Claude bridge quota fallback with ACPX %s in %s mode", async (version, mode) => {
|
||||
// @agentclientprotocol/claude-agent-acp's quota_exhausted fallback title.
|
||||
const title = "The Claude account has no available quota.";
|
||||
const { result, logs } = await executeFailure(
|
||||
title, "limit", mode, version === "0.13.1" ? runnerAcpx.createAcpRuntime : undefined,
|
||||
);
|
||||
expect(result).toMatchObject({
|
||||
exitCode: 1,
|
||||
errorMessage: "ACP agent reported a terminal limit failure.",
|
||||
errorCode: "provider_quota",
|
||||
errorFamily: "provider_quota",
|
||||
resultJson: { errorFamily: "provider_quota" },
|
||||
});
|
||||
expect(result.retryNotBefore).toBeUndefined();
|
||||
expect(JSON.stringify(result)).not.toContain(title);
|
||||
expect(logs).not.toContain(title);
|
||||
});
|
||||
|
||||
it.each([
|
||||
["Context window limit exceeded", "limit"],
|
||||
["Maximum number of turns reached", "limit"],
|
||||
["Configured budget limit reached", "limit"],
|
||||
["Rate limit exceeded; retry later", "limit"],
|
||||
["You've hit your session limit", "request"],
|
||||
["The Claude account has no available quota.", "request"],
|
||||
["The Claude account has available quota.", "limit"],
|
||||
["The worker connection closed", "connection"],
|
||||
])("keeps a non-quota typed failure out of quota recovery: %s", async (title, category) => {
|
||||
const { result, logs } = await executeFailure(title, category);
|
||||
@@ -97,3 +122,10 @@ it.each([
|
||||
expect(JSON.stringify(result)).not.toContain(title);
|
||||
expect(logs).not.toContain(title);
|
||||
});
|
||||
|
||||
it("does not infer quota from the historical generic terminal-limit error", () => {
|
||||
expect(classifyClaudeTerminalSessionFailure({
|
||||
category: "limit",
|
||||
title: "ACP agent reported a terminal limit failure.",
|
||||
}, now)).toBeNull();
|
||||
});
|
||||
@@ -324,7 +324,10 @@ export function classifyClaudeTerminalSessionFailure(
|
||||
// Only the provider's quota wording qualifies for a quota wait.
|
||||
if (failure.category !== "limit") return null;
|
||||
const surface = { errorMessage: [failure.title, failure.details].filter(Boolean).join("\n") };
|
||||
if (!isClaudeProviderQuotaError(surface)) return null;
|
||||
// claude-agent-acp uses this exact quota_exhausted fallback when no provider
|
||||
// title is available. It does not match the CLI's usage-limit wording.
|
||||
const isQuotaFallback = failure.title === "The Claude account has no available quota.";
|
||||
if (!isQuotaFallback && !isClaudeProviderQuotaError(surface)) return null;
|
||||
const retryNotBefore = extractClaudeRetryNotBefore(surface, now)?.toISOString();
|
||||
return {
|
||||
errorCode: "provider_quota",
|
||||
|
||||
Reference in new issue
Block a user