You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Spiral detector: auto-pause after N consecutive non-progressive turns
Why now
While dogfooding #225's decompose flow (PR #241), I observed a textbook agent spiral that the existing architecture has no circuit breaker for:
turn 41: edit index.html (insert tab wrapper after <main>)
turn 42: view index.html ("the insert went to wrong place, let me look")
turn 43: edit index.html (re-insert)
turn 44: view index.html ("file is getting messed up, let me look")
turn 45: edit index.html (try restore)
turn 46: edit index.html (broke <style> block)
turn 47: edit index.html (try fix)
turn 48: edit index.html (broke more)
... ~14 minutes of compute ...
The agent kept hitting the same find-and-insert against fragile whitespace failure mode. No layer in the stack noticed it was making things worse, so no layer asked the user "this isn't working — should I try a different approach or hand back to you?"
Filing as its own subsystem because the detection logic is meaningfully separable from the recovery action (rollback) and the infrastructure resilience (failover). All three feed the same goal — keep async work alive when the user steps away — but each is a discrete primitive that can be built and tested independently.
Proposed shape
A lightweight orchestrator-level monitor that runs alongside every multi-turn agent run and tracks per-file progress. When it detects a non-progressive pattern, it auto-pauses the run and prompts the user (or fires the rollback subsystem if user is away).
Detection signals (any one fires the pause)
Signal
Threshold
Rationale
File size oscillation
Same file's byte size changes between adjacent turns by ±X% N times in M turns
The hallmark of "edit → break → edit → break" cycles
Identical tool-call signature
Same (toolName, filePath, args.signature) 3+ times in 5 turns
Agent retrying without changing approach
Diagnostic error rate
>30% of recent turns emit a DiagnosticEventRow with severity error
Compounding errors
Self-diagnosed regression
Agent says "I broke it" / "let me restore" / "this isn't working" 2+ times in 3 turns
Agent's own meta-cognition flagging trouble
Verifier score drift
Decompose verifier (verify_ui_kit_visual_parity) score drops 2 iterations in a row
The decompose-specific case (already happens in e2e-nodebench-iter: 0.82 → 0.78)
Auto-pause action
When any signal fires:
Halt next agent turn (don't dispatch the queued tool call)
Push a typed spiral_detected chat message: structured card with the detected pattern + the last 3-5 turns summarized
Pause and wait for me — keep state, dispose of pending tool call
Rollback to turn N — fires the rollback subsystem (if available); jumps to last known-good
Hand back to me manually — close the agent loop, let user take over from current state
If the user is away (no input for >5 min after the pause), default to "Pause and wait for me" — never silently retry into the spiral
Why this is its own subsystem, not part of rollback
Rollback is a recovery action — it requires the user (or the agent) to know "this needs reverting." Spiral detection is what tells you it needs reverting in the first place.
Failover handles infrastructure failures (HTTP-level: 429, 5xx, timeout). Spiral detection handles semantic failures where every individual call succeeded but the cumulative behavior is destructive.
Without spiral detection, you can build perfect rollback + perfect failover and the agent will still burn 14 minutes before someone notices.
Implementation outline (5 PRs, sequenced)
#
PR
Effort
1
packages/core/src/orchestrator/progress-monitor.ts — pure tracking primitive: ingests turn events, exposes getRecentSignature(), getFileSizeHistory(), getErrorRate(). No decisions, just data
~1.5h
2
packages/core/src/orchestrator/spiral-detector.ts — pure decision primitive: takes a progress-monitor snapshot + thresholds, returns {detected: boolean, signal?: SpiralSignal, summary: string}. Deterministic, testable
~2h
3
Wire into agent.ts orchestrator — call spiral-detector between every turn; on detection, halt next dispatch + emit spiral_detected event
~1h
4
New spiral_detected chat message kind + 4-button action card in renderer
False positives on legitimate iteration (e.g. real refactor that touches one file 10 times)
Thresholds are tunable per-design; verify_* tools that actually progress (score going UP) suppress detection regardless
User interrupts a productive long task
Always offer "Continue anyway" as the first option; never silently abort
Detector itself spirals (asks every 3 turns)
Cooldown after dismissal: don't re-fire same signal type for 10 turns
Pause loses transient state (in-flight tool call, partial edit)
Use AbortController to cancel cleanly; persist state to scratchpad before halting
Adds latency to every turn
Detector is pure compute on local state, ~ms
Alignment check before code
Default thresholds — start strict (3 identical calls in 5 turns) or lenient (5 in 10)? My lean: strict for v1 since false-positive cost is "user clicks Continue", much lower than false-negative cost ("agent burns 14 minutes")
Where do thresholds live — global, per-design, per-tool? My lean: global default, per-design override via LESSONS.md config block
Should the agent itself receive the spiral notification? I.e. inject "Heads up: progress-monitor flagged you've tried find-and-insert against this whitespace 3 times — try a different approach" into next system prompt automatically. My lean: yes, opt-in via setting, default off until we see how the agent handles it
Default action when user is away — pause-and-wait vs auto-rollback (if available)? My lean: pause-and-wait for v1; auto-rollback is too aggressive without user trust built up
Together these three become a unified "Autonomous continuation" subsystem
The detection-vs-recovery split here matches a pattern from the broader reliability literature (circuit-breaker / health-check + remediation-action separation). Both can ship independently and either is useful alone, but they multiply each other's value when paired.
Looking forward to direction before writing any of the actual code.
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
Uh oh!
There was an error while loading. Please reload this page.
-
Spiral detector: auto-pause after N consecutive non-progressive turns
Why now
While dogfooding #225's decompose flow (PR #241), I observed a textbook agent spiral that the existing architecture has no circuit breaker for:
The agent kept hitting the same
find-and-insert against fragile whitespacefailure mode. No layer in the stack noticed it was making things worse, so no layer asked the user "this isn't working — should I try a different approach or hand back to you?"Filing as its own subsystem because the detection logic is meaningfully separable from the recovery action (rollback) and the infrastructure resilience (failover). All three feed the same goal — keep async work alive when the user steps away — but each is a discrete primitive that can be built and tested independently.
Proposed shape
A lightweight orchestrator-level monitor that runs alongside every multi-turn agent run and tracks per-file progress. When it detects a non-progressive pattern, it auto-pauses the run and prompts the user (or fires the rollback subsystem if user is away).
Detection signals (any one fires the pause)
(toolName, filePath, args.signature)3+ times in 5 turns>30%of recent turns emit aDiagnosticEventRowwith severityerrorverify_ui_kit_visual_parity) score drops 2 iterations in a rowe2e-nodebench-iter: 0.82 → 0.78)Auto-pause action
When any signal fires:
spiral_detectedchat message: structured card with the detected pattern + the last 3-5 turns summarizedWhy this is its own subsystem, not part of rollback
Implementation outline (5 PRs, sequenced)
packages/core/src/orchestrator/progress-monitor.ts— pure tracking primitive: ingests turn events, exposesgetRecentSignature(),getFileSizeHistory(),getErrorRate(). No decisions, just datapackages/core/src/orchestrator/spiral-detector.ts— pure decision primitive: takes aprogress-monitorsnapshot + thresholds, returns{detected: boolean, signal?: SpiralSignal, summary: string}. Deterministic, testableagent.tsorchestrator — callspiral-detectorbetween every turn; on detection, halt next dispatch + emitspiral_detectedeventspiral_detectedchat message kind + 4-button action card in rendererRisks & Mitigations
verify_*tools that actually progress (score going UP) suppress detection regardlessAlignment check before code
LESSONS.mdconfig blockCross-references
The detection-vs-recovery split here matches a pattern from the broader reliability literature (circuit-breaker / health-check + remediation-action separation). Both can ship independently and either is useful alone, but they multiply each other's value when paired.
Looking forward to direction before writing any of the actual code.
Beta Was this translation helpful? Give feedback.
All reactions