Skip to content

fix(sourcehunt): stop a hunter that makes no new progress for N steps - #202

Open
tsunoda-kazuya wants to merge 1 commit into
Lazarus-AI:mainfrom
tsunoda-kazuya:fix/sourcehunt-stalled-stop
Open

fix(sourcehunt): stop a hunter that makes no new progress for N steps#202
tsunoda-kazuya wants to merge 1 commit into
Lazarus-AI:mainfrom
tsunoda-kazuya:fix/sourcehunt-stalled-stop

Conversation

@tsunoda-kazuya

Copy link
Copy Markdown
Contributor

Summary

  • Add a stalled stop reason that fires after max_steps_without_progress consecutive steps with no new finding, no new potential, and no first read of a previously unread file.
  • Complements the existing degenerate_loop (repeated identical calls) and empty_response (empty turns) terminals, which do not cover varied-but-unproductive reading.
  • New HuntTuning.max_steps_without_progress config field (default 8, 0 disables) and matching --max-steps-without-progress CLI flag.
  • Deep-mode read_file now populates ctx.deep_files_read: set[str] on first read of an unread file, so the progress signature is meaningful in deep mode (where ctx.files_read — populated only by the constrained read_source_file — stays empty). execute and write_file do not count as progress.

Problem

A deep hunter can keep issuing distinct, schema-valid reads across the same file (varying offsets) or a stream of varied failing commands without ever recording a finding or flagging a potential. None of the current terminals fire: the calls are not identical (so no degenerate_loop), the turns are not empty (so no empty_response), and the budget is unlimited when budget_usd=0, so the loop runs to max_steps (2000 in deep mode). With output-token caps no longer sent (#182), each such step is also longer than before, so a stuck hunter is now more expensive per iteration.

Fix

  • Track steps_since_progress in NativeHunter.arun; the per-step signature is (len(findings), len(potentials), len(ctx.files_read), len(ctx.deep_files_read)). Reset on any change, increment when unchanged.
  • Deep-mode read_file adds the resolved path to ctx.deep_files_read on success; execute and write_file do not touch the set.
  • Return HunterRunResult(stop_reason="stalled") past the threshold, keeping accumulated findings and potentials.
  • prev_step_had_skip gates the counter so steps skipped by degenerate_loop throttling do not compete with the new terminal.

Validation

  • uv run --frozen --extra dev pytest -q tests/test_deep_agent_loop.py tests/test_sourcehunt_hunter.py — 109 passed; new regressions: hunter stalls when re-reading the same path 10 times, when running varied failing execs, when spamming write_file; distinct-path reading does not stall; max_steps_without_progress=0 disables the guard; degenerate_loop still wins for pure identical-call loops.
  • uv run --frozen --extra dev ruff check clearwing/agent/tools/hunt/sandbox.py clearwing/agent/tools/hunt/deep_agent.py clearwing/sourcehunt/hunter.py clearwing/sourcehunt/config.py clearwing/sourcehunt/runner.py clearwing/sourcehunt/pool.py clearwing/sourcehunt/subsystem.py clearwing/ui/commands/sourcehunt.py tests/test_deep_agent_loop.py
  • git diff --check

Backward compatibility

max_steps_without_progress = 8 is the shipped default; 0 restores the previous behaviour (no stalled terminal). The new stop reason is additive; existing consumers of stop_reason see the new string only when the guard fires.

Independent of the other 4 PRs in this batch; trivial rebase if any lands first.

Track steps since the last new finding, new potential, or first read of a
previously-unread file (constrained read_source_file or deep read_file).
When nothing advances for max_steps_without_progress steps (default 8, 0
disables) the hunter stops with stop_reason=stalled. Steps that only
reissued an already-throttled call are left to the degenerate_loop
terminal, so identical-call loops keep reporting degenerate_loop.

Deep hunters read source via read_file/execute rather than the constrained
read_source_file that populates ctx.files_read, so the progress signature
also tracks ctx.deep_files_read — the set of paths successfully read via
the deep read_file tool. Only the first read of a new path counts; a
stream of varied failing execute commands or repeated write_file calls
does not reset the counter. Without this, deep mode (the production
default) either collapses to (findings, potentials) and falsely kills
early exploration, or resets on any tool call and lets a wandering agent
run to max_steps.

The threshold is configurable via SourceHuntConfig.tuning
.max_steps_without_progress and the sourcehunt CLI's
--max-steps-without-progress flag.

Complements degenerate_loop (repeated identical calls) and empty_response
(empty turns), which do not catch varied-but-unproductive reading; that
case otherwise runs to max_steps.
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