feat(loops): no-progress / doom-loop detection — stop on repeated identical responses (#1157)#1368
Merged
Conversation
Adds the nullable `agent_loops.no_progress_threshold INTEGER` column for doom-loop detection. NULL = disabled (back-compat for in-flight loops). Dual-track per Invariant #9: SQLite `_migrate_agent_loops_no_progress` (idempotent `_safe_add_column`) + Alembic `0007_agent_loops_no_progress` (`ADD COLUMN IF NOT EXISTS`, chained after `0006_agent_reports`). DDL in both `schema.py` (SQLite) and `tables.py` (MetaData). `_loop_row_to_dict` + `LoopOperations.create_loop` carry the field through. Refs #1157 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The runner fingerprints each successful run's full response
(`_fingerprint = sha256(" ".join(text.split()))` — collapses whitespace,
preserves word boundaries, empty/whitespace-only fold to one fingerprint)
and tracks consecutive identical results in a runner-local
`last_fingerprint`/`repeat_count` (no persistence). Once K consecutive
runs match, the loop stops with status `stopped` / `stop_reason="no_progress"`.
Precedence: `stop_signal_matched` is checked first and wins; a pending
`user_stopped` or passed deadline also outranks `no_progress` (re-checked
before the break) so an explicit stop is never relabeled. Exact-hash only —
no fuzzy/semantic similarity. NULL/0 threshold disables.
Refs #1157
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
`StartLoopRequest` gains `no_progress_threshold: Optional[int]`
(default 3, ge=0) with a `field_validator` rejecting `1` → 422
("repeated identical" needs ≥2). The router threads it into
`loop_service.start_loop()`, and `LoopStatusResponse` /
`_build_status_response` carry it back through `GET /api/loops/{id}`.
Refs #1157
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds the `no_progress_threshold` param to the `run_agent_loop` MCP tool with a zod `.int().min(0).refine(v !== 1)` validator mirroring the backend (0 disables, default 3, 1 rejected). Threaded through `TrinityClient.startAgentLoop`. Refs #1157 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds the "No-progress threshold" number input (default 3, 0 disables) with helper text to the Run-loop form. The `0` disable sentinel is sent explicitly — a truthy guard would drop it. `formatStopReason` maps the new `no_progress` reason to a readable label. Refs #1157 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
~13 unit tests: stop-at-K, near-miss counter reset, response normalization (whitespace/empty collapse), precedence over stop_signal / user_stop / deadline, K > max_runs, the WS event contract, router default-on, K=1 → 422, and GET round-trip of the threshold. Refs #1157 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
requirements §38.3, architecture (Sequential Agent Loops + agent_loops schema/stop_reason enum), the run-agent-loop feature flow, and a feature-flows index row. Refs #1157 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Refs #1157 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…e-1157 # Conflicts: # docs/memory/feature-flows.md
vybe
approved these changes
Jun 28, 2026
vybe
left a comment
Contributor
There was a problem hiding this comment.
Validated via /validate-pr: clean. Issue #1157 (public P2 feat, status-ready), Closes-keyword present, security clean, dual-track migration (SQLite + Alembic 0007_agent_loops_no_progress, no collision with dev@0006), tests + docs present. Rebased onto dev (resolved feature-flows.md changelog conflict — kept both #1157 and #1085 rows). APPROVE.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes #1157.
A sequential agent loop that feeds
{{previous_response}}forward can get stuck re-emitting the same response every iteration — a classic autonomous-agent "doom loop" — burning the entiremax_runsbudget while making zero progress. Iteration/time caps don't catch it. This adds no-progress detection: fingerprint each successful run's response and stop once K consecutive runs are identical.What changed
no_progress_threshold(int;0disables; default 3 for new loops via API/MCP; NULL ⇒ disabled so in-flight loops are unaffected).1is rejected → 422 ("repeated identical" needs ≥2).services/loop_service.py):_fingerprint(text) = sha256(" ".join(text.split()))(collapses whitespace, preserves word boundaries; empty/whitespace-only fold to one fingerprint). Runner-locallast_fingerprint/repeat_count, no persistence. Stopsstopped/stop_reason="no_progress". Exact-hash only — no fuzzy/semantic.stop_signal_matchedwins (checked first); a pendinguser_stopped/deadline_exceededalso outranksno_progress(re-checked before the break) — an explicit stop is never relabeled._migrate_agent_loops_no_progress+ Alembic0007_agent_loops_no_progress(ADD COLUMN IF NOT EXISTS).run_agent_loop+ zod.refinemirror), frontendLoopsPanel.vue(form field +0-sentinel handling +formatStopReasonlabel).Behavior change
New API/MCP loops stop after 3 identical responses by default. Operators of legitimately repeat-confirming loops should pass
no_progress_threshold=0.Testing
pytest tests/unit/test_loop_service.py tests/unit/test_loops_router_validation.py→ 42 passed.tsc --noEmit→ clean.Docs
requirements §38.3, architecture (Sequential Agent Loops +
agent_loopsschema/stop_reason),feature-flows/run-agent-loop.md, feature-flows index. CSO--diffaudit: 0 findings.🤖 Generated with Claude Code