fix(server): keep classified provider quota runs out of Sentry (#16029)

A run that stops because the user's provider account hit its usage
limit is an expected wait, not a Paperclip defect. The adapter already
classifies it (errorCode and resultJson.errorFamily are both
provider_quota) and recovery schedules a retry at the reset time.

Skip the Sentry report only when both adapter-owned fields agree, the
run status is failed, no signal stopped it, and the failure did not
happen during setup. Timeouts, cancellations, signals, other codes and
missing or mismatched families still report.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Dotta <cryppadotta@users.noreply.github.com>
This commit is contained in:
authored and GitHub committed 2026-10-12 15:59:27 -07:00
1 parent 64bd6e9e07
commit 4b958ecbd8
2 files changed
+44

No files matched your search

@@ -301,6 +301,39 @@ describeEmbeddedPostgres("reportRunFailure", () => {
expect(mockCaptureRunFailure).toHaveBeenCalledWith(expect.objectContaining({ agentAdapter: "unknown" }));
});
function providerQuotaRun(overrides: Partial<typeof heartbeatRuns.$inferSelect> = {}) {
return buildRun({
error: "ACP agent reported a terminal limit failure.\nYou've hit your session limit · resets 2am (UTC)",
errorCode: "provider_quota", executionStage: "dispatching", exitCode: 1, signal: null,
resultJson: { errorFamily: "provider_quota", stopReason: "adapter_failed" }, ...overrides,
});
}
it.each(["claude_local", "codex_local"])("keeps a classified %s provider quota wait local", async (adapterType) => {
await seedCompanyAndAgent({ adapterType });
const [run] = await db.insert(heartbeatRuns).values(providerQuotaRun()).returning();
const before = structuredClone(run);
for (const phase of [undefined, "execute"] as const) await reportRunFailure(db, run!, { phase });
expect(mockCaptureRunFailure).not.toHaveBeenCalled();
expect(await db.select().from(heartbeatRuns)).toEqual([before]);
});
it.each([
{ status: "timed_out" }, { status: "cancelled", startedAt: new Date(0) }, { signal: "SIGTERM" },
{ errorCode: "adapter_failed" }, { errorCode: "claude_transient_upstream" },
{ resultJson: {} }, { resultJson: null }, { resultJson: { errorFamily: "transient_upstream" } },
])("reports a provider quota outcome without matching adapter evidence: %j", async (overrides) => {
await seedCompanyAndAgent({ adapterType: "claude_local" });
await reportRunFailure(db, providerQuotaRun(overrides), { phase: "execute" });
expect(mockCaptureRunFailure).toHaveBeenCalledOnce();
});
it("keeps a provider quota code reportable during setup", async () => {
await seedCompanyAndAgent({ adapterType: "claude_local" });
await reportRunFailure(db, providerQuotaRun(), { phase: "setup" });
expect(mockCaptureRunFailure).toHaveBeenCalledOnce();
});
function aiSelectionRun(overrides: Partial<typeof heartbeatRuns.$inferSelect> = {}) {
return buildRun({
errorCode: "configuration_incomplete",
+11
View File
@@ -164,6 +164,16 @@ function isHermesConnectionFailure(run: HeartbeatRun, options: RunFailureReportO
(connection.reason === "remote_unavailable" && code === "hermes_gateway_upstream_error");
}
/**
* The adapter proved the user's provider account is out of quota, and recovery
* waits for its reset. Both fields come from the adapter's own classifier.
*/
function isProviderQuotaFailure(run: HeartbeatRun, options: RunFailureReportOptions): boolean {
return run.status === "failed" && run.errorCode === "provider_quota" &&
asRecord(run.resultJson)?.errorFamily === "provider_quota" &&
options.phase !== "setup" && run.signal == null;
}
/**
* Report a terminal run failure to Sentry. Returns at once for any status
* other than failures and unexpected started cancellations. Never throws — a Sentry failure or a
@@ -182,6 +192,7 @@ export function reportRunFailure(db: Db, run: HeartbeatRun, options: RunFailureR
if (isAiConnectionSelectionBlocker(run, options)) return Promise.resolve();
if (isLocalPathWorkspaceConfigurationBlocker(run, options)) return Promise.resolve();
if (isGitConnectionFailure(run, options)) return Promise.resolve();
if (isProviderQuotaFailure(run, options)) return Promise.resolve();
const runStatus = run.status;
const report = captureTerminalRunFailure(db, run, runStatus, options);
pendingRunFailureReports.add(report);