Skip to content

Agent stalls: model announces work, ends turn with end_turn and no tool calls — loop accepts it as done #165

Description

@BunsDev

Symptom

The agent announces work and then just stops — the turn ends with stop_reason "end_turn" and nothing happens. Observed repeatedly in one session (BUILD mode, claude-opus-4-8, local):

  • Turn A ends with: "Enough exploration — I have the wiring points. Writing the CLI command now. First the self-contained module…" → no tool calls, turn over.
  • User: "proceed with implementation" → turn ends with zero output; TUI shows the fix(tui): expand paste placeholders at submit time #149 placeholder (no response — model ended the turn with stop_reason "end_turn").
  • User: "continue until completion" → model runs one Bash(cd …) round, then narrates "Now writing the CLI uninstall module." across rounds and ends with "Writing the CLI uninstall module now." → no Write/Edit calls ever happen, turn shows "done".

Session evidence: ~/.coven-code/sessions/d0bca548-…json (assistant messages [7], [9], [11] all end end_turn with announced-but-unexecuted work; [9] is fully empty).

Root cause

run_query_loop treats end_turn as authoritative in both provider paths (crates/query/src/lib.rs — the Anthropic match stop arm and the OpenAI-compat round loop). When the model emits intent-narration text but no tool calls and stops, the loop fires Stop hooks and returns QueryOutcome::EndTurn. The #149 follow-up made the empty variant visible (placeholder text) but not recoverable, and the announce-then-stop variant isn't even surfaced — the turn just reads as if the agent chose to stop mid-task.

There is already a working in-loop recovery precedent: the max_tokens arm injects MAX_TOKENS_RECOVERY_MSG and continues, bounded by MAX_TOKENS_RECOVERY_LIMIT, with should_emit_turn_complete suppressing the intermediate TurnComplete.

Proposed fix

Bounded stall recovery mirroring the max_tokens pattern, in both provider paths:

  • When a round ends end_turn with no tool calls and the text is empty or its closing sentences are an imminent-action announcement ("Writing the CLI uninstall module now.", "Let me confirm deps, then write the module.") — inject a continuation nudge as a user message and continue instead of ending the turn.
  • Bounded at 2 nudges per user turn; counter resets on any tool round. On exhaustion, fall through to today's behavior (placeholder for the empty case).
  • Deliberate handoffs never trigger recovery: questions and wait-for-user phrasing ("Should I…?", "Let me know…", "I'll wait for your review.") end the turn as before.
  • Suppress the intermediate TurnComplete while recovering (same as max_tokens recovery).
  • Kill switch: COVEN_CODE_DISABLE_STALL_RECOVERY=1.

Worst case for a false positive is one extra bounded round-trip in which the model states the outcome and stops — the nudge text explicitly allows "if the work is complete, state the final outcome".

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions