Skip to content

fix(buzz-agent): recover from context-window 400s instead of sticking - #4946

Merged
wpfleger96 merged 4 commits into
mainfrom
sami/reactive-context-overflow-recovery
Aug 5, 2026
Merged

fix(buzz-agent): recover from context-window 400s instead of sticking#4946
wpfleger96 merged 4 commits into
mainfrom
sami/reactive-context-overflow-recovery

Conversation

@wpfleger96

Copy link
Copy Markdown
Member

Provider context_length_exceeded 400s 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:

  • Typed classification. AgentError::LlmContextExceeded is classified at both non-success provider terminals — the shared post() (Anthropic, OpenAI, Databricks) and openrouter_post() — on status == 400 plus a context-window body match, so ordinary 400s stay terminal.
  • Forced handoff. A context-400 forces a summarize-handoff that bypasses should_handoff() and BUZZ_AGENT_MAX_HANDOFFS, bounded by its own per-turn budget (MAX_CONTEXT_RECOVERIES_PER_RUN = 3).
  • Shrink ladder. The summarize prompt budget halves from the observed rejected history size — not from 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.
  • Baseline reset. The stale usage baseline is cleared when a request fails, so the preflight gate cannot stay frozen sub-threshold on retries.

Named behavior changes:

  1. Anthropic and OpenRouter errors now carry the (model) stamp. Provider arms return their Result into the central error mapper instead of early-returning past it, making the code match its documented single-convergence contract at that mapper.
  2. max_rounds now 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.

npub17jjz49l9jjmhhk7cac63j8yt9z555n9cw8vk7v5jz4vzw4ppld5qgj57cc 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>
@wpfleger96
wpfleger96 requested a review from a team as a code owner August 5, 2026 19:48
…-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
wpfleger96 merged commit ed4b3e7 into main Aug 5, 2026
33 checks passed
@wpfleger96
wpfleger96 deleted the sami/reactive-context-overflow-recovery branch August 5, 2026 20:58
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>
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