Skip to content

fix: add per-attempt request timeout so a hung provider call fails into the retry ladder - #52

Merged
aliasunder merged 6 commits into
mainfrom
fix/request-timeout
Aug 15, 2026
Merged

aliasunder merged 6 commits into
mainfrom
fix/request-timeout

Conversation

@aliasunder

Copy link
Copy Markdown
Owner

Problem

A stalled OpenRouter request hangs the job until the runner's 6-hour kill. chat.send had no timeout, and the retry/fallback ladder only engages once an attempt fails — so a hung primary call means the fallback model never gets a chance.

Observed live on vault-onboarding PR #7: a deepseek/deepseek-v4-flash-0731 call sat 17+ minutes past its typical ~6-minute completion, with fallback_model unset, freezing the check indefinitely (it eventually returned, but nothing bounded it).

Fix

  • New request_timeout_seconds input (default 600), validated as a positive integer in config and threaded into the OpenRouter client as requestTimeoutMs
  • chat.send now passes the SDK's RequestOptions.timeoutMs — on timeout the SDK aborts the HTTP call and throws a status-less error, which the existing classifier already treats as a retryable api_error: same-model retry once, then the fallback model
  • Docs updated in the same change: action.yml input + README inputs table

Verification

  • tsc clean, eslint clean, Prettier clean
  • 453/453 tests pass; the whole-call send assertion now covers options.timeoutMs, and config tests cover the new input's parse/reject paths

🤖 Generated with Claude Code

…to the retry ladder

A stalled OpenRouter request previously hung the job until the runner's
6-hour kill: chat.send had no timeout, and the retry/fallback ladder only
engages once an attempt fails. Observed live on vault-onboarding PR #7,
where a deepseek-v4-flash call sat 17+ minutes with fallback_model unset.

- New request_timeout_seconds input (default 600) threaded from action.yml
  through config into the OpenRouter client
- chat.send now passes the SDK's RequestOptions.timeoutMs, so a timed-out
  attempt aborts the HTTP call and classifies as a status-less retryable
  api_error — same-model retry, then fallback model

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread src/main.ts
@umm-actually

umm-actually Bot commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

Bound the generation cost lookup with the same request timeout
Medium severity · correctness · high confidence

src/openrouter/client.ts:312 — beyond the diff's line ranges, in code the changes touch or depend on.

Pre-existing: the generation cost lookup runs with no timeout, so a stalled generations endpoint hangs the job until the runner's 6-hour kill — the exact incident class this PR's timeout was added to eliminate — even though the review was already accepted. The PR's own per-attempt timeout on chat.send is the fix template for this sibling SDK call.

Failure scenario: An accepted chat response carries no usage cost (the makeNoCostChatResult path), and OpenRouter's generations endpoint stalls; requestReview awaits getGeneration without any bound, so the job sits until the runner kill and the accepted review never posts — the accepted attempt is in hand but postInlineFindings is never reached.

Suggested fix
Extend OpenRouterLike.generations.getGeneration to accept an options argument (request, options?: { timeoutMs?: number }) and call it as sdk.generations.getGeneration({ id: generationId }, { timeoutMs: requestTimeoutMs }); a timed-out lookup falls through the existing catch → warn → null-cost path.

@umm-actually

umm-actually Bot commented Aug 15, 2026 •

Copy link
Copy Markdown
Contributor

umm-actually re-reviewed at 78d8f28

No new findings (3 tracked finding(s) across all runs).


umm-actually · deepseek/deepseek-v4-flash-0731

aliasunder and others added 2 commits August 15, 2026 00:07
A positive-integer check alone accepts values whose milliseconds overflow
the timer cap (or reach Infinity), which timer implementations clamp to
~1 ms — every request would time out instantly. Bound the input at
2,147,483 seconds so seconds x 1000 always stays a valid delay.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The cost lookup ran with no timeout, so a stalled generations endpoint
could hang the job after the review itself was already accepted — the
same incident class the per-attempt timeout eliminates. The lookup is
best-effort, so a timeout degrades to a null cost with a warning.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@aliasunder

Copy link
Copy Markdown
Owner Author

Addressing the PR-level umm-actually finding (generation cost lookup timeout, comment 5300432976): Valid — fixed in ff502de. getGeneration now receives the same timeoutMs as chat.send, so a stalled generations endpoint can no longer hang the job after the review is accepted. The lookup stays best-effort — a timeout degrades to a null cost with a warning, exactly like other lookup failures. The whole-call assertion on the stub now covers the passed options.


🔍 ship-check · pr-monitor · claude-fable-5

Comment thread src/config.ts
aliasunder and others added 3 commits August 15, 2026 00:39
UMM_REQUEST_TIMEOUT_SECONDS tunes the per-attempt timeout from repo
settings, matching the OPENROUTER_MODEL pattern. Uses an explicit || '600'
fallback because a bare unset var passes an empty string, which would
override the action default and fail validation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Workflows wiring a bare unset repo variable pass an empty string, which
would otherwise override the action.yml default and fail validation.
Empty now means "not provided" — same semantics as max_findings and
fallback_model — so bare-var wiring works without an || fallback.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@aliasunder
aliasunder merged commit 12ee025 into main Aug 15, 2026
7 checks passed
@aliasunder
aliasunder deleted the fix/request-timeout branch August 15, 2026 04:45
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