fix(buzz-agent): recover from context-window 400s instead of sticking - #4946
Merged
Conversation
added 3 commits
August 4, 2026 18:52
…king A context-window rejection from the provider was terminal *and* permanent. Three defects compounded: 1. No reactive classification. A `context_length_exceeded` 400 surfaced as a generic `AgentError::Llm` and ended the turn. 2. The `HandoffOutcome::Skipped -> truncate_history(cfg.max_history_bytes)` fallback is a measured no-op at context-window scale: it is a request-body byte limiter (16 MiB default) while the thing it must defend is a token window (200k default). An 800 KB history that already exceeds the window evicts nothing. 3. Permanent stick. A failed request reports no usage, so `last_request_input_tokens` stays frozen at the last *successful* sub-threshold reading, `should_handoff()` returns false forever, and the oversized history stays in the in-memory session. Every later prompt in that session fails the same way — a stick that persists across turns for the lifetime of the session. Restarting the agent DOES clear it, since history lives only in the in-memory session map and is never written to disk; restart is the manual workaround, not an exception to the stick. Fix, reactive path only: - `AgentError::LlmContextExceeded` classified where status and body are still separate values, gated on status 400 AND narrow markers, at BOTH `!status.is_success()` terminals — the shared `post()` (Anthropic, OpenAI/Databricks) and the separate `openrouter_post()` implementation. The agent loop matches on the type, never on a formatted string. - `forced_handoff()` bypasses both `should_handoff()` and `max_handoffs`, and `recover_from_context_overflow()` walks a ladder that rebuilds the summarize prompt with an explicitly decreasing history budget, anchored on the *observed* rejected size rather than the config window the provider just contradicted. Floored at 4 KiB, bounded at 3 recoveries per `run()`; exhaustion re-surfaces the provider's own error rather than looping. The ladder loops internally because the rescue summarize can itself be rejected. - After a successful recovery, both `last_request_input_tokens` and `last_request_history_bytes` are cleared so the proactive gate is not blind. Named behavior change: the Anthropic and OpenRouter arms of `complete()` no longer use `?`, so all providers reach the `result.map_err` model-stamping mapper. Existing Anthropic/OpenRouter errors therefore gain the `(model)` stamp they previously bypassed. This makes the code match the doc comment at its own convergence point, which already claimed to be universal and was not. External shape preserved: `json_rpc_code()` falls through to -32000. Out of scope: proactive window/usage accounting (why the gate was miscalibrated at all) is parked pending a live probe of the endpoint's enforced window. No guessed constants. Testing: 11 new arms (end-to-end stuck-loop -> recovery, ordinary-400 stays terminal, budget exhaustion, prompt-exactly-once across a forced handoff, shrink-below-rejected-size, usage-baseline clear, per-terminal positive and negative classification arms, Responses-API non-interference) plus a 10-mutant matrix, all killed. `cargo test -p buzz-agent` 485/0; fmt and `clippy --all-targets -D warnings` clean. Co-authored-by: Tyler Longwell <tlongwell@block.xyz> Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
…-overflow-recovery * origin/main: fix: reauthenticate databricks model discovery (#4008) Revert "chore(release): release Buzz Desktop version 0.5.5" (#4797) feat: Buzz entity links — rich preview cards + in-app navigation for repos, PRs, and issues (#4695) Co-authored-by: Tyler Longwell <tlongwell@block.xyz> Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
Review found two defects in the reactive context-overflow recovery, plus a mutation gap the matrix re-run exposed. Blocker 1 — a finite `max_rounds` silently dropped the recovered turn. `round` is charged before `complete()` resolves, so a request the provider refused for context overflow still spent a round. With `max_rounds=1` the sequence was: request 400s, forced handoff destructively summarizes history, `continue`, cap check fires at the top of the loop, turn returns `MaxTurnRequests` — the user loses their context AND gets no answer, which is strictly worse than the error it replaced. Every prior e2e arm inherited `max_rounds=0` (unbounded), so the hole sat in the untested product of the two features. Reproduced first with a failing arm, then fixed by uncharging rather than exempting: on `ContextRecovery::Recovered`, `round` is decremented before `continue`. This cannot become an unbounded amnesty — refunds are paired 1:1 with a consumed rung of `MAX_CONTEXT_RECOVERIES_PER_RUN`, and an ordinary round is never refunded. A companion arm proves the ordinary cap still binds when no context-400 occurs. Blocker 3 — accuracy. The doc comment claimed the stick "outlives the process"; restart is precisely what clears it, since `Session.history` lives only in the in-memory session map. Reworded to say it persists across turns for the life of the session, and that restart is the manual workaround. Mutation gap — the ladder's rung-to-rung shrink was untested. A mutant pinning `shift` to 1 survived the whole suite, and had to: `attempts` is 0 on the first rung, so `shift = 1` IS production there, and no arm reached a second rung. `recovery_shrinks_further_on_each_rung` rejects rung 1's own summarize call with a context-400 — the realistic case the ladder was built for, since the summarizer travels the same provider path — forcing a second rung, and asserts rung 2's prompt is materially smaller. Guarded against measuring the wrong mechanism: it asserts rung 1 was actually skipped and that the 4 KiB floor was not involved. It kills the mutant that survived and passes on the control. Full matrix re-run at this tree (the fix edits the loop several mutants live in, so read-through was not licensed): control survives, M1/M2/M4/M5/M6/M7/M8/ M9a/M9b/M10/M11/M12 all killed, M7b argued-equivalent. `cargo test -p buzz-agent` 489/0; fmt and `clippy --all-targets -D warnings` clean. Co-authored-by: Tyler Longwell <tlongwell@block.xyz> Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
…-overflow-recovery * origin/main: fix(buzz-agent): scope handoff cap per turn, not per session lifetime (#4805) Fix mobile message timeline bounce (#4862) Polish mobile bottom sheets and profile cards (#4911) Fix media attachment actions (#4849) fix(desktop): remove join API token control (#4897) fix(desktop): allow shared agent mentions (#4913) Polish mobile top navigation (#4778) fix(release): tag immutable desktop candidates (#4811) fix(channels): restrict private-channel invitations (#4612) fix(acp): reject unattended permission requests (#4609) fix(workflow): bind trigger author to the signed event (#4607) fix(git): revoke access for banned relay members (#4608) fix(agent): recover from unsupported image input instead of poisoning the turn (#4896) Define private managed agent wire protocol (#4593) fix(mobile): serialize channel sections sync (#3165) fix(desktop): make missing-command error actionable for released builds (#4802) chore(release): release Buzz Desktop version 0.5.5 (#4809) feat: paste composer text without formatting (#4801) Revert "chore(release): release Buzz Desktop version 0.5.5" (#4808) chore(release): release Buzz Desktop version 0.5.5 (#4800) Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com> # Conflicts: # crates/buzz-agent/src/agent.rs # crates/buzz-agent/src/handoff.rs # crates/buzz-agent/src/types.rs # crates/buzz-agent/tests/regressions.rs
wpfleger96
pushed a commit
that referenced
this pull request
Aug 5, 2026
…ed-agent-store-merge * origin/main: (24 commits) fix(reactions): support max-length custom emoji (#3833) feat(desktop): allow leaving your final community (#3621) fix(buzz-agent): recover from context-window 400s instead of sticking (#4946) docs(persona-pack): fix stale desktop import instructions (#4500) fix(desktop): route macos notification clicks (#4799) feat(mobile): sync themes per community (#3767) feat(desktop): sync themes per community (#3653) feat(desktop): cap OpenClaw agent parallelism at 5 (#4019) fix(buzz-agent): scope handoff cap per turn, not per session lifetime (#4805) Fix mobile message timeline bounce (#4862) Polish mobile bottom sheets and profile cards (#4911) Fix media attachment actions (#4849) fix(desktop): remove join API token control (#4897) fix(desktop): allow shared agent mentions (#4913) Polish mobile top navigation (#4778) fix(release): tag immutable desktop candidates (#4811) fix(channels): restrict private-channel invitations (#4612) fix(acp): reject unattended permission requests (#4609) fix(workflow): bind trigger author to the signed event (#4607) fix(git): revoke access for banned relay members (#4608) ... Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com> # Conflicts: # desktop/src-tauri/src/managed_agents/runtime.rs
elifoster-block
added a commit
that referenced
this pull request
Aug 5, 2026
…p-csp * origin/main: (66 commits) fix(reactions): support max-length custom emoji (#3833) feat(desktop): allow leaving your final community (#3621) fix(buzz-agent): recover from context-window 400s instead of sticking (#4946) docs(persona-pack): fix stale desktop import instructions (#4500) fix(desktop): route macos notification clicks (#4799) feat(mobile): sync themes per community (#3767) feat(desktop): sync themes per community (#3653) feat(desktop): cap OpenClaw agent parallelism at 5 (#4019) fix(buzz-agent): scope handoff cap per turn, not per session lifetime (#4805) Fix mobile message timeline bounce (#4862) Polish mobile bottom sheets and profile cards (#4911) Fix media attachment actions (#4849) fix(desktop): remove join API token control (#4897) fix(desktop): allow shared agent mentions (#4913) Polish mobile top navigation (#4778) fix(release): tag immutable desktop candidates (#4811) fix(channels): restrict private-channel invitations (#4612) fix(acp): reject unattended permission requests (#4609) fix(workflow): bind trigger author to the signed event (#4607) fix(git): revoke access for banned relay members (#4608) ...
tellaho
added a commit
that referenced
this pull request
Aug 5, 2026
Co-authored-by: Taylor Ho <taylorkmho@gmail.com> Signed-off-by: Taylor Ho <taylorkmho@gmail.com> * origin/main: fix(reactions): support max-length custom emoji (#3833) feat(desktop): allow leaving your final community (#3621) fix(buzz-agent): recover from context-window 400s instead of sticking (#4946) docs(persona-pack): fix stale desktop import instructions (#4500) fix(desktop): route macos notification clicks (#4799) feat(mobile): sync themes per community (#3767) feat(desktop): sync themes per community (#3653) feat(desktop): cap OpenClaw agent parallelism at 5 (#4019) fix(buzz-agent): scope handoff cap per turn, not per session lifetime (#4805) Fix mobile message timeline bounce (#4862) Polish mobile bottom sheets and profile cards (#4911) Fix media attachment actions (#4849) fix(desktop): remove join API token control (#4897) fix(desktop): allow shared agent mentions (#4913) Signed-off-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@buzz.block.builderlab.xyz>
wpfleger96
added a commit
that referenced
this pull request
Aug 5, 2026
Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com> * origin/main: fix(reactions): support max-length custom emoji (#3833) feat(desktop): allow leaving your final community (#3621) fix(buzz-agent): recover from context-window 400s instead of sticking (#4946) docs(persona-pack): fix stale desktop import instructions (#4500) fix(desktop): route macos notification clicks (#4799) feat(mobile): sync themes per community (#3767) feat(desktop): sync themes per community (#3653) feat(desktop): cap OpenClaw agent parallelism at 5 (#4019) fix(buzz-agent): scope handoff cap per turn, not per session lifetime (#4805) Fix mobile message timeline bounce (#4862) Polish mobile bottom sheets and profile cards (#4911) Fix media attachment actions (#4849) fix(desktop): remove join API token control (#4897) Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
tellaho
added a commit
that referenced
this pull request
Aug 5, 2026
…-setting * origin/main: fix(reactions): support max-length custom emoji (#3833) feat(desktop): allow leaving your final community (#3621) fix(buzz-agent): recover from context-window 400s instead of sticking (#4946) docs(persona-pack): fix stale desktop import instructions (#4500) fix(desktop): route macos notification clicks (#4799) feat(mobile): sync themes per community (#3767) feat(desktop): sync themes per community (#3653) feat(desktop): cap OpenClaw agent parallelism at 5 (#4019) fix(buzz-agent): scope handoff cap per turn, not per session lifetime (#4805) Fix mobile message timeline bounce (#4862) Polish mobile bottom sheets and profile cards (#4911) Fix media attachment actions (#4849) fix(desktop): remove join API token control (#4897) Co-authored-by: Taylor Ho <taylorkmho@gmail.com> Signed-off-by: Taylor Ho <taylorkmho@gmail.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.
Provider
context_length_exceeded400s permanently wedged agent sessions: the turn errored, the oversized history persisted in the in-memory session, and the usage baseline stayed frozen at the last successful sub-threshold reading (failed requests report no usage), so the preflight handoff gate never fired again — every later prompt failed identically until an agent restart. The byte-truncation fallback never intervened because it is a request-body limiter (estimated_bytes), not a context-window defence; at context-window scale it is a measured no-op.This adds the reactive recovery path:
AgentError::LlmContextExceededis classified at both non-success provider terminals — the sharedpost()(Anthropic, OpenAI, Databricks) andopenrouter_post()— onstatus == 400plus a context-window body match, so ordinary 400s stay terminal.should_handoff()andBUZZ_AGENT_MAX_HANDOFFS, bounded by its own per-turn budget (MAX_CONTEXT_RECOVERIES_PER_RUN = 3).max_context_tokens, the number the provider just contradicted — rung to rung, with a 4096-byte floor. A summarize call rejected for the same reason takes the next rung instead of re-sticking. At the floor (overflow dominated by unshrinkable frame: system prompt, tool schemas, live prompt) recovery is refused and the provider error surfaces clearly instead of self-healing.Named behavior changes:
(model)stamp. Provider arms return theirResultinto the central error mapper instead of early-returning past it, making the code match its documented single-convergence contract at that mapper.max_roundsnow counts completions the loop acts on. A request rejected with a context-400 that is then successfully recovered refunds its round before the retry, paired 1:1 with a consumed recovery rung, so the round cap is neither weakened nor able to drop a recovered turn unanswered.Related: #4805 — the complementary proactive fix (per-session handoff-cap kill switch that let sessions grow to the provider wall). #4805 prevents reaching the wall; this PR recovers at it.