fix(action): tighten provider error detection and propagate agent error events (#580)

* fix(action): tighten provider error detection and propagate agent error events

Both bugs from #562:

1. detectProviderError used substring matches against "429", "rate limit",
   etc. — false-positives on commit SHAs containing 429 and on x-ratelimit-*
   response headers in dumped 401 error JSON. rewrote with anchored regexes:
   numeric status codes only match adjacent to a recognised status key, and
   `\brate[_ ]limit(?=[_ ]|\b)` rejects ratelimit-* headers (no separator).
   word-boundary anchors on INTERNAL / UNAVAILABLE / quota / limit:0 reject
   INTERNAL_SERVER_ERROR / time_limit:0 substrings. added 11-case regression
   test.

2. opencode 401s slipped through `eventCount === 0 && lastProviderError`
   because opencode's own type=error event increments eventCount before
   the guard runs. added an explicit `error:` handler that captures the
   event and propagates it to a non-success AgentResult. opencode emits
   the message under `error.data.message`, not the top level. mirror fix
   in claude.ts: error_max_turns / error_during_execution / any error*
   subtype on the result event now flips success: false.

* fix(action): match quota inside identifiers like insufficient_quota

\bquota\b missed insufficient_quota / quota_exceeded / quotaExceeded
because _ is a word character and camelCase has no boundary. quota is
specific enough to be matched as a plain substring.

* fix(action): match `rate limited` and `rate limits exceeded`

Drop the trailing `(?=[_ ]|\b)` lookahead from the rate-limit regex. The
lookahead failed when `limit` was followed by another word character
(`limited`, `limits`), so `rate limited` and `rate limits exceeded` were
slipping past detection. The leading `\b` plus `[_ ]` separator already
rejects `x-ratelimit-*` / `anthropic-ratelimit-*` headers without it.

---------

Co-authored-by: pullfrog[bot] <226033991+pullfrog[bot]@users.noreply.github.com>
Co-authored-by: David Blass <david@arktype.io>
This commit is contained in:
pullfrog[bot]
2026-05-07 16:31:05 +00:00
committed by pullfrog[bot]
parent 366af55f19
commit 6f76a6a9da
4 changed files with 158 additions and 12 deletions
+31 -1
View File
@@ -254,7 +254,13 @@ interface OpenCodeErrorEvent {
type: "error";
timestamp?: string;
sessionID?: string;
error?: { name?: string; message?: string; data?: unknown; [key: string]: unknown };
// opencode emits the error message under `error.data.message`, not at the
// top level. see anomalyco/opencode packages/opencode/src/cli/cmd/run.ts.
error?: {
name?: string;
data?: { message?: string; [key: string]: unknown };
[key: string]: unknown;
};
[key: string]: unknown;
}
@@ -630,6 +636,16 @@ async function runOpenCode(params: RunParams): Promise<AgentResult> {
log.debug(withLabel(label, `tool output: ${outputStr}`));
}
},
error: (event: OpenCodeErrorEvent) => {
// opencode emits a `type=error` event when a provider call fails (e.g.
// 401 Invalid authentication credentials). the underlying CLI still
// exits 0 because the error was returned cleanly by the LLM SDK, so
// unless we capture this event the run is reported as success.
agentErrorEvent = event;
const errorName = event.error?.name || "unknown";
const errorMessage = event.error?.data?.message || event.error?.name || JSON.stringify(event);
log.info(`» ${params.label} error event: ${errorName}: ${errorMessage}`);
},
result: async (event: OpenCodeResultEvent) => {
const status = event.status || "unknown";
const duration = event.stats?.duration_ms || 0;
@@ -663,6 +679,7 @@ async function runOpenCode(params: RunParams): Promise<AgentResult> {
const recentStderr: string[] = [];
let lastProviderError: string | null = null;
let agentErrorEvent: OpenCodeErrorEvent | null = null;
let output = "";
let stdoutBuffer = "";
@@ -824,6 +841,19 @@ async function runOpenCode(params: RunParams): Promise<AgentResult> {
};
}
if (agentErrorEvent) {
const errorEvent: OpenCodeErrorEvent = agentErrorEvent;
const errorName = errorEvent.error?.name || "agent error";
const errorMessage =
errorEvent.error?.data?.message || errorEvent.error?.name || JSON.stringify(errorEvent);
return {
success: false,
output: finalOutput || output,
error: `${errorName}: ${errorMessage}`,
usage,
};
}
return { success: true, output: finalOutput || output, usage };
} catch (error) {
params.todoTracker?.cancel();