feat: add --timeout to bound a run and raise provider caps - #27
Merged
Conversation
Every provider shipped an invisible hard cap and a long review had no
recourse: the CLI-tool providers kill their subprocess after 5 minutes,
ollama's http.Client after 5, and the Anthropic SDK refuses outright past
10 ("streaming is required for operations that may take longer than 10
minutes"). doctor's per-provider probe fast-fails at 5 seconds.
A context deadline alone does not fix this — it can only shorten a run.
clireview derives its child context from the caller's, so min(5m, 20m)
still dies at five; http.Client.Timeout fires independently of ctx; and
the SDK's ceiling is a pre-flight refusal, not a wait. So the resolved
value is also handed down to the provider through a new optional
provider.TimeoutSetter (the same additive marker shape as
PlainTextEmitter), implemented by exactly clireview, ollama and anthropic
— the three with a cap to raise — and reached only through the
newProviderWithTimeout / setup.TestConnectionTimeout seams.
--timeout accepts a Go duration (90s, 10m, 1h30m) or a bare whole number
of seconds (600), since that is what CI authors type. Resolution is
--timeout > review.timeout > built-in, so --timeout 0 restores the
built-ins for a single run. Invalid or negative values fail in
resolveContext, before the diff is read. Unset, behavior is unchanged.
The budget covers the whole run — diff, provider call, render, and time
spent at a confirmation prompt. doctor keeps its impatient 5s default but
it is now overridable, because on a slow link 5 seconds reports merely
slow as unreachable. mcp gets a per-tool-call budget rather than a
deadline on the long-lived server, by preserving global.timeout across
the runReviewForMCP flag reset — which also fixes guard --timeout
silently losing the flag.
Expiry checks the run's own ctx (providers report deadlines in three
different dialects) and rewrites the error, so a self-inflicted deadline
is never mistaken for a provider outage. Exit code stays 1; the cache key
is unchanged.
See ADR-0038.
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
Long reviews die and the user has no recourse. Every provider ships an invisible hard cap:
claude-cli/gemini-cli/codex-cliclireview.Spec.Timeout→exec.CommandContextollamahttp.Client.Timeout(whole request)anthropicstreaming is required for operations that may take longer than 10 minutesdoctorper-provider probedoctor.connectionTimeoutOn a large diff, or a mid-size local model on modest hardware, those are exactly the runs that die — and the only workaround was to shrink the diff.
Why a deadline alone is not the fix
context.WithTimeoutgives a deadline, and a deadline can only ever cut a run short.clireviewderives its own child context from the caller's, somin(5m, 20m)still dies at five;http.Client.Timeoutfires independently ofctx; the Anthropic SDK's ceiling is a pre-flight refusal, not a wait. A user who types--timeout 20mand still gets killed at 5 has been actively misled.So the resolved value is also handed down to the provider, through a new optional interface:
Same additive marker shape as the existing
PlainTextEmitter, soProviderandFactory— both semver-locked — are untouched and no existing implementation breaks. Implemented by exactly the three providers with a cap to raise (clireview,ollama,anthropic); deliberately not byopenai+ its OpenAI-compatible siblings,gemini, ormock, which purely followctx. Callers reach it through two seams —cli.newProviderWithTimeoutandsetup.TestConnectionTimeout— never by type-asserting ad hoc.Surface
90s,10m,1h30m) or a bare whole number of seconds —--timeout 600is what CI authors type, and rejecting it for a missing unit is a papercut with no upside.--timeout>review.timeout> built-in.--timeout 0restores the built-ins for a single run.resolveContext, before the diff is read.review.timeoutis stored as a string (timeout: "10m") so the YAML stays readable, and is validated on write — a typo fails atconfig set, not on every later run.Scope decisions
doctorkeeps its impatient 5s default but it is now overridable. On a high-latency link, 5 seconds reports merely-slow as unreachable — the wrong diagnosis for someone debugging their setup.mcpgets a per-tool-call budget, never a deadline on the long-lived stdio server, by preservingglobal.timeoutacross therunReviewForMCPflag reset. That also fixesguard --timeoutsilently losing the flag.ctx(providers report deadlines in three different dialects) and rewrites the error, so a self-inflicted deadline is never mistaken for a provider outage. Exit code stays 1; the cache key is unchanged.Verification
make checkgreen — gofmt, vet, golangci-lint,go test ./..., release-check, i18n-check, spdx-check, gosec.Manual, against an unreachable ollama in a scratch repo:
--timeout abcand--timeout=-5serror before any workdoctor5s default → 12s with--timeout 12sreview.timeout: 6s→ 6s run;--timeout 2s→ 2s;--timeout 0→ ran past 6s until killed at 25sSetTimeout(5s)then succeedsNew tests:
internal/cli/timeout_test.go(parse table, precedence chain,withTimeout,wrapTimeoutErr),SetTimeouttests for all three providers,doctor.ConnTimeout,config get/set review.timeoutround-trip, and five--timeoutintegration tests.Docs
README (global flags + a Timeouts section + the config block), CHANGELOG (Unreleased). Wiki pages (
Global-flags,Configuration-files,Doctor-command,Providers-command,MCP-server,Provider-CLI-tools,Provider-Ollama) are updated locally and pushed separately, as always.🤖 Generated with Claude Code