Skip to content

feat: signal a blocked credential so a caller can route around it - #12

Merged
pedromvgomes merged 1 commit into
mainfrom
feature/detect-usage-limits-error
Sep 9, 2026
Merged

feat: signal a blocked credential so a caller can route around it#12
pedromvgomes merged 1 commit into
mainfrom
feature/detect-usage-limits-error

Conversation

@pedromvgomes

Copy link
Copy Markdown
Owner

Every bad outcome this library reported was a statement about the request. One class is not: when an allowance is spent or a credential is rejected, the request was never considered at all. It is the only class where retrying against a different provider is the right response — and IsError was the same true for it as for a task the agent simply failed, so a caller could not tell "route elsewhere" from "this fails wherever it runs".

What this adds

Result.Blocked names the reason:

  • BlockExhausted — the allowance for the current window is spent. Lifts on a clock; ResetsAt says when, where the provider reports one.
  • BlockRejected — the credential is invalid or expired. Never lifts without a human.

A reason earns a place in the vocabulary only where the caller's correct response differs. An unrecognised failure is not a block: IsError with no Blocked, which correctly says "do not route on this".

BlockReporter declares which reasons a dialect can actually recognise, surfaced as Driver.DetectableBlocks(). It carries the set rather than a yes, because the two providers differ and the difference is load-bearing.

Why the providers answer differently

  • claudecode implements it and reads both. A blocked run reports subtype: "success" with is_error set and the HTTP status in api_error_status — exactly how the captured 401 behaves — so the status is the whole signal.
  • codex does not implement it. Its only event stream is codex exec --json, whose terminal turn.failed carries a prose message and nothing else; the error-code vocabulary the CLI keeps internally (usageLimitExceeded, unauthorized) never reaches the wire. Verified empirically against the pinned 0.153.4. A dialect matching that English would recognise only the wording it was written against.

That asymmetry is what the capability is for. Without it, a chain built on codex looks identical to one built on claudecode right up to the night a window runs out.

The library signals; it does not route

A Driver binds one provider at New, and a Request is not portable between dialects — AllowedTools and PermissionMode are spelled in the provider's own vocabulary, SessionID does not cross at all, MaxTurns is counted in incomparable units. Retrying "the same request" elsewhere would silently run a different one. Composition over two drivers belongs above this library.

Fixture provenance

claudecode/testdata/blocked-exhausted.json is derived, not captured — producing an exhausted run on demand means genuinely spending a subscription window. It is built from wire tokens in the pinned 2.1.266 artifact, and claudecode/testdata/README.md records that and the rule: recognition keys on a status code, never on display prose. It is replaced by a capture the first time one is obtainable.

Deferred

Throttled and pay-as-you-go credit exhaustion are real distinctions both vendors make, but neither reaches the wire yet. A block that arrives with no envelope at all also has no home: Decoder sees only stdout, and closing that gap means a second capability that stays unbuilt until a run is observed taking that path.

Decisions and rejected alternatives: ADR 0006. Glossary: CONTEXT.md.

Checks

go test ./... and go vet ./... pass; gofmt clean. agtk code-review deep panel (6 reviewer runs + judge) returned 0 findings.

Every bad outcome the library reported was a statement about the request, and
one class is not: when an allowance is spent or a credential is rejected, the
request was never considered. It is the only class where retrying against a
different provider is right, and IsError was the same true for it as for a
failed task — so a caller could not tell "route elsewhere" from "this fails
wherever it runs".

Result.Blocked names the reason. BlockExhausted lifts on a clock and carries
ResetsAt where the provider reports one; BlockRejected never lifts without a
human. BlockReporter declares which reasons a dialect can actually recognise,
so a chain is not built on a provider that will never fire it.

claudecode reads both from api_error_status: a blocked run reports subtype
"success" with is_error set and the status in that field, exactly as a rejected
token does. codex implements BlockReporter not at all — its only event stream
is `codex exec --json`, whose turn.failed carries prose and nothing else, and a
dialect matching that English would recognise only the wording it was written
against.

The library signals and does not route. A Driver binds one provider, and a
Request is not portable between dialects, so retrying "the same request"
elsewhere would silently run a different one.
@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown

bulwark

  • scan — no findings

  • coverage — go: 83.9% (baseline 83.8%); go patch: 100.0% (20/20 new lines; baseline 83.8%)

📦 Full bulwark output — complete scan and coverage logs.

@agtk-code-review agtk-code-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review by agtk — panel standard

No findings survived the panel.

Ran and reported nothing: correctness, security

panel standard, 3 runs, 1 convention docs read, $1.6565 · range main...7d334d397e54ed26061c22f7b1c7a57681bcbb96 · manifest built-in default

@pedromvgomes
pedromvgomes merged commit a408aac into main Sep 9, 2026
9 checks passed
@pedromvgomes
pedromvgomes deleted the feature/detect-usage-limits-error branch September 9, 2026 14:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant