Skip to content

feat: detect stalled turns and force text-only recovery #1314

Description

@flame4

Problem

In long-running turns the agent can get stuck emitting placeholder tool calls that make no progress. The existing ToolCallDeduplicator only blocks exact duplicates within the same step, so the model can still loop by using slightly different no-op calls such as:

  • Bash(':')
  • Bash('true')
  • Read('/dev/null')
  • Read('')
  • echo / printf placeholders

When this happens, the turn keeps running until it hits max_steps_per_turn, wasting tokens and time instead of returning a response to the user.

Anonymized transcript excerpt

step  2: Read('/dev/null')
step  3: Read('/dev/null')
step  4: Read('/dev/null')
step  5: Read('')
step  9: Bash('true')
step 10: Bash("printf ''")
step 14: Bash('true')
step 22: Bash(':')
step 23: Bash('true')
step 24: Bash(': ')
step 25: Bash(':')
...
step 36: Bash(':')
step 37: Bash(':')
step 38: Bash(':')

These calls produce no file changes, no new background work, and no useful new information.

Proposed solution

Add a ProgressDetector that watches external, observable state rather than trying to interpret model intent:

  1. External state changes
    • git status --porcelain output in the working directory.
    • Background task lifecycle/status changes.
  2. Information gain
    • Successful tool outputs that are non-trivial and have not been seen before in the current turn.

After a configurable number of consecutive idle steps, the harness should:

  • inject a system reminder asking the model to stop calling tools and respond in text;
  • run the next model step with no tools available ({ tools: [] }), forcing a text-only response.

This provides a graceful fallback instead of letting the turn crash into max_steps_exceeded.

Configuration

Two new loop_control options would be useful:

  • progress_stall_threshold — number of consecutive idle steps before forcing text-only mode (default 8).
  • progress_min_info_gain_length — minimum successful output length to count as information gain (default 60).

Example config.toml:

[loop_control]
progress_stall_threshold = 12
progress_min_info_gain_length = 120

I have a PR ready that implements this approach.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    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