Skip to content

fix(conversation): detect context-window condensation loops as stuck - #6215

Open
xxwtiancai wants to merge 1 commit into
OpenHands:mainfrom
xxwtiancai:fix/stuck-detector-context-window-loop-6104
Open

xxwtiancai wants to merge 1 commit into
OpenHands:mainfrom
xxwtiancai:fix/stuck-detector-context-window-loop-6104

Conversation

@xxwtiancai

@xxwtiancai xxwtiancai commented Oct 25, 2026 •

Copy link
Copy Markdown

HUMAN:

Problem: Re-prompting after failed context condensation loops indefinitely until hitting MaxIterationsReached.

Fix: Added trailing-event detection to mark runs STUCK on repeated condensation failures while ignoring proactive compaction.

Verification: Verified via scripted repro: the run now safely halts at the 5th rejection instead of spinning for 15 turns.


AGENT:

Why

When every agent LLM call is rejected with a context-window error while the condenser keeps succeeding, the conversation loops forever: each rejected call emits a CondensationRequest, the condenser emits a Condensation, and the next call is rejected again. StuckDetector._is_stuck_context_window_error was an unconditional stub (return False), so the cycle only stopped at max_iteration_per_run (default 500), wasting one rejected agent call plus one summarizer call per iteration in exactly the long conversations that are most expensive to restart. Fixes #6104.

Summary

  • Implemented scenario 5 in openhands/sdk/conversation/stuck_detector.py: the run is stuck when every event in the trailing window after the last user message belongs to rejected-call/condensation cycles (CondensationRequest/Condensation), with no action, observation, or agent message between them. At least one CondensationRequest must be present, so proactive condensation (token/event pressure, which emits only Condensation) never matches.
  • Reused the existing scenario-5 hook, the existing len(events) >= 10 gate in is_stuck(), and the existing STUCK run-loop exit — no new plumbing, no new configuration field, no changes to scenarios 1–4 or the nudge.
  • Removed the stale TODO: blocked by agent-sdk#282 stub.

Issue Number

Fixes #6104

How to Test

uv run --frozen pytest tests/cross/test_stuck_detector.py \
  tests/cross/test_stuck_detector_config.py \
  tests/sdk/conversation/local/test_stuck_detector_nudge.py \
  tests/sdk/conversation/local/test_stuck_detector_context_window.py -q

New coverage (all verified to fail without the product change and pass with it):

  • tests/cross/test_stuck_detector.py: trailing run of 6 cycles → stuck; 4 cycles (8 events) below the 10-event gate → not stuck; 12 proactive Condensation events with no CondensationRequest → not stuck; cycles interrupted by an ActionEvent → not stuck; user message after the cycles resets the window → not stuck.
  • tests/sdk/conversation/local/test_stuck_detector_context_window.py: the issue's scripted repro (agent LLM always raises LLMContextWindowExceedError via TestLLM, summarizer always succeeds) ends STUCK with no ConversationErrorEvent and agent calls strictly below max_iteration_per_run; with stuck_detection=False the same loop still ends ERROR/MaxIterationsReached.

The reproduction is the mocked scripted run the issue and its triage specify (no real LLM involved); the condenser, git-less run loop, and stuck plumbing are the real code paths.

Video/Screenshots

Reproduction notes instead of media (non-visual bug):

  • Without the fix, the scripted repro ends error after 15 rejected agent calls + 15 summarizer calls with MaxIterationsReached (matches the issue's reported log).
  • With the fix, the same scripted run ends stuck after 5 rejected calls (agent_llm.call_count == 5 < 30) with no ConversationErrorEvent.

Type

  • Bug fix
  • Feature
  • Refactor
  • Breaking change
  • Docs / chore

A context-window-rejected agent call emits a CondensationRequest and the
condenser answers with a Condensation, but nothing ever stopped that
cycle: _is_stuck_context_window_error was an unconditional stub, so a
conversation whose prompt never fits kept burning one rejected agent
call plus one summarizer call per iteration until max_iteration_per_run.

Implement the detection: the run is stuck when every event in the
trailing window after the last user message belongs to such
rejected-call/condensation cycles, with at least one
CondensationRequest (which proactive token/event-pressure condensation
does not emit) and no action, observation, or agent message between
them. The existing 10-event scenario gate and the STUCK run-loop exit
bound the detection; a user message still resets the window.

Fixes OpenHands#6104
@xxwtiancai
xxwtiancai marked this pull request as ready for review October 25, 2026 08:29
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.

[Bug]: StuckDetector never detects a context-window error loop; condensation repeats until max_iteration_per_run

1 participant