Skip to content

fix(repo-explorer): recover from a blank survey turn instead of aborting - #222

Open
gadievron wants to merge 1 commit into
masterfrom
fix/repo-explorer-empty-turn-resilient
Open

fix(repo-explorer): recover from a blank survey turn instead of aborting#222
gadievron wants to merge 1 commit into
masterfrom
fix/repo-explorer-empty-turn-resilient

Conversation

@gadievron

Copy link
Copy Markdown
Collaborator

Root cause

context/repo_explorer.py::explore_repository runs a multi-turn agentic survey (application-context / threat-model generation). Its binding.adapter.complete() call was unguarded. An empty/malformed completion raises LLMResponseError in every adapter — anthropic, google, openai-responses, and openai-chat once #208 lands — so a single blank turn aborted a survey that may already have read useful context, turning a transient into a total failure. (Pre-existing: the "nudge and continue" recovery only ever worked for the one provider that returned an empty end_turn instead of raising; surfaced by the #206/#207/#208 collision review.)

Fix

Catch LLMResponseError around complete() and retry the same messages (appending nothing keeps the user/assistant roles alternating). Bound consecutive blanks with MAX_CONSECUTIVE_EMPTY_TURNS = 2 so a persistently-empty model still fails loudly and well short of the MAX_TURNS budget. A refusal (LLMRefusalError, a subclass) is caught first and re-raised — a deliberate safety signal must never be churned past.

Reproduction

A survey where turn 1's completion is empty (adapter raises LLMResponseError) and turn 2 is a valid finish.

Regression tests (tests/test_repo_explorer_loop.py)

  • test_empty_turn_is_recovered_not_fatal — transient blank → survey recovers and finishes.
  • test_persistent_empty_turns_fail_loud_and_bounded — every turn blank → raises, and does not burn the whole budget.
  • test_refusal_is_not_retried_propagates — a refusal propagates immediately, is not retried.

The tests raise repo_explorer's own bound exception class so they stay stable when another test purges utilities.* from sys.modules (a pre-existing test-isolation hazard).

# RED (origin/master): 3 failed, 5 passed
# GREEN (this branch):  8 passed
# Full suite:           2536 passed, 28 skipped, 0 failed

Compatibility

No API/signature change. Behavior change: a blank turn is retried (bounded) instead of aborting; a persistently-empty model still fails loudly. Independent of #208 (anthropic/google already raised on empty), and composes with it.

An empty/malformed completion raises LLMResponseError in every adapter
(anthropic/google/openai-responses, and openai-chat once its empty-content
guard lands). The exploration loop called complete() unguarded, so a single
blank turn aborted a survey that may already have read useful context --
turning a transient into a total failure of application-context / threat-model
generation.

Catch it and retry the same messages (appending nothing keeps the user/
assistant roles alternating), bounded by MAX_CONSECUTIVE_EMPTY_TURNS so a
persistently-empty model still fails loudly and well short of the MAX_TURNS
budget. A refusal (LLMRefusalError, a subclass) is caught first and re-raised
so a safety signal is never churned past.

Tests: recover-on-transient, fail-loud-and-bounded-on-persistent, and
refusal-propagates. The tests raise repo_explorer's own bound exception class
so they stay stable when another test purges utilities.* from sys.modules.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@gadievron
gadievron force-pushed the fix/repo-explorer-empty-turn-resilient branch from 80e8a47 to 7c4d2c4 Compare August 8, 2026 08:31
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