fix(opencode): surface silent provider quota limits (D9) - #184
Merged
Conversation
Some providers never emit a `{type:"error"}` event on a quota block: Z.ai's
`zai-coding-plan` (GLM) treats the `AI_APICallError: Usage limit reached` as a
retryable stream error and loops on backoff, printing it only to opencode's own
log. The `--format json` stream stayed silent, so `parse_opencode_limit` (a
JSON-event scan) saw nothing and the run stalled until the 60m wall timeout,
misclassified as `Timeout` with `saw_error=false` (observed live, FinCal #71).
- command: pass `--print-logs --log-level ERROR` so opencode's logfmt logs land
on the stderr the headless runner already drains.
- events: add `parse_opencode_log_limit`, a substring scan over the combined
stdout+stderr log for the "usage limit reached" / billing-cycle wording that
only appears in the logfmt lines, with a text-based reset-hint extractor
(handles Z.ai's `reset at <ts>`).
- lib (execute): fall back to the log scan when the JSON scan finds no limit, so
the run classifies as `Outcome::Limit` (stop-on-limit) instead of `Timeout`.
Fixtures use the exact captured lines (glm-5.2 5h cap, kimi billing cycle).
Note: this corrects the classification but still waits for the child to exit
(the wall timeout). Killing the child on first detection to reclaim the wasted
time is a follow-up in the shared headless runner.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
An
opencoderun against a provider whose quota is exhausted could stall for thefull per-issue wall timeout (60m in the observed case) and then be misclassified
as
Timeoutwithsaw_error=false— so stop-on-limit never fired and the queuekept burning budget on subsequent issues.
Root cause: some providers never emit a
{type:"error"}event on a quota block.Z.ai's
zai-coding-plan(GLM) treats theAI_APICallError: Usage limit reachedas a retryable stream error and loops on exponential backoff, printing it only
to opencode's own log. The
--format jsonstream stayed silent, soparse_opencode_limit(a JSON-event scan) saw nothing. Observed live 2026-07-11(FinCal #71, glm-5.2 — 5-hour cap); an equivalent kimi
billing cycleblock wasseen earlier.
Fix
--print-logs --log-level ERRORso opencode's own logfmt logsland on the stderr the headless runner already drains.
parse_opencode_log_limit, a substring scan over the combinedstdout+stderr log for the
usage limit reached/ billing-cycle wording that onlyappears in the logfmt lines (the JSON-event scan structurally can't see it), with
a text-based reset-hint extractor that handles Z.ai's
reset at <ts>.the run classifies as
Outcome::Limit(stop-on-limit) instead ofTimeout.Fixtures use the exact captured log lines (glm-5.2 5h cap, kimi billing cycle).
Scope / follow-up
This corrects the classification but still waits for the child to exit (the wall
timeout). Killing the child on first detection to reclaim the wasted time is a
deliberate follow-up in the shared headless runner — held back as a separate change
since it touches plumbing used by all adapters. As a zero-code mitigation, a lower
--max-minutes-per-issuebounds the wasted time today.Verification
cargo fmt --check,cargo clippy -p ralphy-agent-opencode --all-targets -- -D warnings,and
cargo test -p ralphy-agent-opencode(53 passed) all green on themainbase.🤖 Generated with Claude Code