Skip to content

fix(buzz-agent): escalate LLM timeouts per retry and log per-call latency - #5130

Merged
wpfleger96 merged 3 commits into
mainfrom
wpfleger/llm-timeout-escalation-logging
Aug 7, 2026
Merged

fix(buzz-agent): escalate LLM timeouts per retry and log per-call latency#5130
wpfleger96 merged 3 commits into
mainfrom
wpfleger/llm-timeout-escalation-logging

Conversation

@wpfleger96

@wpfleger96 wpfleger96 commented Aug 7, 2026

Copy link
Copy Markdown
Member

Non-streaming LLM calls ("stream": false) through slow model/provider combinations routinely take longer than the fixed BUZZ_AGENT_LLM_TIMEOUT_SECS window (default 240 s) to return their first response byte. The retry loop then re-ran the identical 240 s bet three times, failed the turn, and the ACP harness requeued the whole turn from scratch: agents spent 30+ minutes producing nothing while every attempt died at the same wall. And because the LLM path only logged WARN lines on failure, a healthy-but-slow call was indistinguishable from a wedged one.

Timeout handling

  • Per-attempt escalation: the per-request budget doubles after each timeout failure (base × 2^n, capped at max(1200 s, base)escalated_timeout() in llm.rs), shared by the main post() loop and openrouter_post(). Non-timeout retryables (429/5xx/connect) do not escalate. A call that needs six minutes now succeeds on a later attempt instead of never.
  • Per-request total timeouts: enforcement moved from the client-level read_timeout to RequestBuilder::timeout() on each LLM request, so escalated budgets aren't silently floored by the shared client and each attempt's bound covers connect through body completion. Timeout error messages were updated to match the new semantics and still point at BUZZ_AGENT_LLM_TIMEOUT_SECS.

Observability

  • One INFO line per completed LLM call: model, provider, duration_ms, input_tokens, cached_input_tokens, output_tokens. Slowness and prompt-cache effectiveness are now visible in harness logs without waiting for a failure, and None vs 0 token reports stay distinguishable. Handoff summarization calls log the same line with duration only.
  • The agent main loop wraps the call in a session_id tracing span so each line is attributable to a session.

…ency

Non-streaming calls to slow models (claude-fable via the Databricks
gateway) routinely exceed the fixed 240s read timeout before the first
response byte arrives, so every retry re-ran an identical losing bet and
turns black-holed for 30+ minutes. Escalate the per-request budget 2x
after each timeout failure (capped at max(1200s, base)), default unset
timeouts to 600s for known slow-generation models, and emit one INFO
line per completed LLM call (duration + token usage incl. cache reads)
so slowness is visible before it becomes failure.

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
@wpfleger96
wpfleger96 requested a review from a team as a code owner August 7, 2026 01:08

@wesbillman wesbillman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewing on Wes's behalf.

The timeout escalation is incomplete for a response that times out after headers arrive. In both retry loops, a timeout from .send() increments timeout_failures and retries with the doubled budget, but a timeout from stream.chunk() returns immediately via terminal_llm_error (post: lines 2161–2166; openrouter_post: lines 2489–2495). RequestBuilder::timeout() covers the full request through body completion, and send() resolves once headers are available, so a gateway that sends headers before a slow or stalled body hits this terminal branch. It never receives the advertised next-attempt budget, and a partially delivered non-streaming JSON response can still fail at the original fixed wall.

Please route timeout-class body-read failures through the same retry/escalation path in both implementations (increment the timeout count, drop the response, back off, and continue while attempts remain), with an end-to-end regression proving a body timeout retries under a larger budget and succeeds. Non-timeout body decode/read failures can remain terminal.

A per-request timeout that fires after headers arrive surfaced in the
body-read loop and returned terminally, so it never received the
escalated next-attempt budget — route timeout-class body-read failures
through the same retry/escalation path (review finding). Also remove
the model-aware timeout default: timeout policy stays generic, models
are not special-cased.

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>

@wesbillman wesbillman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Re-reviewing exact head 74267415d50f9e900ebc9718d39b10fccd060603 on Wes's behalf.

The blocking body-read timeout gap from my prior review is fixed in both post() and openrouter_post(): timeout-class stream.chunk() failures now increment the timeout-failure count, back off, and continue to the next attempt, where escalated_timeout() supplies the larger budget. Non-timeout body errors remain terminal.

The new loopback regression exercises the post-header partial-body timeout and confirms a second request succeeds; the pure escalation tests cover the larger-budget calculation. I ran that focused regression on this exact head and it passed. git diff --check also passed, and the current head's Unit Tests, Rust Lint, Security, macOS build, three completed smoke shards, Semgrep, zizmor, and DCO checks are green; several broad CI jobs are still running.

No remaining code findings from me. I am not submitting approval because Wes's standing policy requires an explicit approval request; the prior Changes Requested review will need to be dismissed or superseded when he decides to merge.

wesbillman
wesbillman previously approved these changes Aug 7, 2026
Drop the redundant base_timeout parameter from provider methods that
already hold cfg; extract the 429 Retry-After cap into a pure
retry_delay_for_429 seam so cap enforcement is unit-testable without a
60s sleep; mirror the body-read-timeout regression for openrouter_post;
document the worst-case escalation ceiling; log summarize durations.

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
@wpfleger96

Copy link
Copy Markdown
Member Author

🤖 good catch — you were right that .send() resolving at headers meant a stalled body never saw the escalated budget. Fixed in 74267415d: timeout-class stream.chunk() failures now go through the same retry path in both post() and openrouter_post() (increment the timeout count, drop the partial response, back off, continue while attempts remain), and non-timeout body errors plus final-attempt timeouts stay terminal. Both implementations have the end-to-end regression you asked for — first response sends headers plus a partial body then stalls past the base budget, second completes, asserts success and exactly two requests served (post_body_read_timeout_retries_with_escalated_budget and the openrouter_post mirror). Also replaced the unreachable!() at the bottom of post() with a real terminal error so that invariant isn't load-bearing anymore.

@wpfleger96
wpfleger96 enabled auto-merge (squash) August 7, 2026 15:03
@wpfleger96
wpfleger96 disabled auto-merge August 7, 2026 15:45
@wpfleger96
wpfleger96 merged commit 346ae8c into main Aug 7, 2026
33 checks passed
@wpfleger96
wpfleger96 deleted the wpfleger/llm-timeout-escalation-logging branch August 7, 2026 15:45
wpfleger96 pushed a commit that referenced this pull request Aug 7, 2026
* origin/main: (32 commits)
  Recover from max-token response truncation (#5223)
  chore(release): release Buzz Desktop version 0.5.6 (#5214)
  fix(mobile): keep latest messages above composer (#4981)
  fix(sdk): preserve self-mention p tags in message and forum event builders (#4975)
  bump @tauri-apps/cli to ~2.11.4 to fix linux app icon issue (#4858)
  feat(desktop): adding rich link previews to messages (#3818)
  fix(buzz-agent): Responses reasoning summary, Anthropic display:summarized, ACP v2 messageId (#5195)
  fix(desktop): retain distinct agent instances in autocomplete (#5202)
  fix(desktop): defer channel visibility change to Save (#5203)
  feat(desktop): Projects follow-ups — access restrictions, fast loading, activity feed polish (#5073)
  refactor(cli): replace probe/decider/detail split with single typed extractor (#5191)
  fix(desktop): drop unhandled rejection from throwing window.Notification (#5143)
  fix(desktop): fence localStorage SecurityError from killing the React tree (#5142)
  fix(desktop): make terminal output selectable (#4980)
  fix(desktop): use WEBKIT_DMABUF_RENDERER_FORCE_SHM for NVIDIA/AppImage (#3654) (#4505)
  Make public starter channels best effort (#5192)
  Mobile: add anchored reaction popover (#5025)
  feat(mobile): add bee pull-to-refresh (#5059)
  Remove agent creation success modal (#5063)
  fix(buzz-agent): escalate LLM timeouts per retry and log per-call latency (#5130)
  ...

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>

# Conflicts:
#	desktop/src/shared/api/tauri.ts
tlongwell-block pushed a commit that referenced this pull request Aug 7, 2026
Brings the bench branch up from 13c9e90 to current main (c7b6636) as
a merge commit — no rebase, no history rewrite. Clean auto-merge, zero
conflicts. Notably picks up:

- #5248: budget summarizer reasoning separately so it cannot starve
  the handoff summary
- #5223: recover from max-token response truncation
- #5195: Responses reasoning summary + ACP v2 messageId fixes
- #5130: escalate LLM timeouts per retry, log per-call latency

Originating Buzz thread:
buzz://message?channel=c3252dd2-0142-4e01-88c7-a2183c3960a5&id=9e60a8dfa64a59339d7b357c509e231f6bcf76e5507a92078a6723378f9d1e95

Co-authored-by: Eva <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
Signed-off-by: Eva <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
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.

2 participants