Problem
When a ralph enters an idle state (no work to do), it keeps iterating at full speed, burning tokens every cycle just to report "nothing to do."
Real example from a project manager ralph:
Iteration 4 (46.2s) — completed task, created PR
Iteration 5 (13.9s) — "Status: IDLE. Nothing to do."
Iteration 6 (11.4s) — "Status: IDLE. Nothing to do."
...
Iteration 11 (12.0s) — "Status: IDLE. Nothing to do."
Iterations 5-11 are pure waste. Each one runs all commands, assembles the full prompt, and pipes it to the agent just to get "idle" back.
Proposal
1. Structured state communication via markers
Define a structured protocol for agents to communicate state back to the orchestration loop. The agent includes a marker in its output:
<!-- ralph:state idle -->
This is an HTML comment, invisible in rendered markdown and unambiguous to parse. The engine checks result_text (already captured on AgentResult) for the marker after each iteration.
2. Frontmatter configuration
---
agent: claude -p
idle:
delay: 30s # initial delay before next iteration
backoff: 2.0 # multiplier applied each consecutive idle iteration
max_delay: 5m # cap on backoff
max: 6h # stop the loop entirely after this cumulative idle time
---
3. Behavior
- After an iteration, check agent output for the
<!-- ralph:state idle --> marker
- When idle is detected: wait
delay seconds, then delay * backoff, then delay * backoff^2, ..., capped at max_delay
- Reset on activity: as soon as an iteration does NOT signal idle, reset the delay to the initial value and clear the cumulative idle timer
- Max idle (
idle.max): if total consecutive idle time exceeds this duration, stop the loop with a clear message. Supports human-readable durations (30m, 6h, 1d)
- Terminal UX during wait: show a countdown so the user knows it's paused, not stuck. Single
Ctrl+C skips the delay and runs immediately; double Ctrl+C stops the loop
- Events: emit a new
ITERATION_IDLE event type so UIs can render idle state distinctly from completed/failed
- Commands always run, even during idle iterations. Commands feed new information into the loop, so skipping them could prevent the agent from noticing new work
The marker format (<!-- ralph:state <name> -->) should be designed to be expandable for future per-state config, but only idle is handled initially. If the idle block is absent from frontmatter, the loop runs exactly as before with no behavior change.
4. Implementation notesi
The engine already has the right hooks:
result_text on AgentResult / IterationEndedData carries agent output. Parse markers from there
_delay_if_needed() in engine.py already handles interruptible sleep between iterations. Extend it with backoff logic
_frontmatter.py parses the YAML config. Add idle as a new frontmatter field
RunConfig gets new idle-related fields; RunState tracks consecutive idle count and cumulative idle duration
Problem
When a ralph enters an idle state (no work to do), it keeps iterating at full speed, burning tokens every cycle just to report "nothing to do."
Real example from a project manager ralph:
Iterations 5-11 are pure waste. Each one runs all commands, assembles the full prompt, and pipes it to the agent just to get "idle" back.
Proposal
1. Structured state communication via markers
Define a structured protocol for agents to communicate state back to the orchestration loop. The agent includes a marker in its output:
This is an HTML comment, invisible in rendered markdown and unambiguous to parse. The engine checks
result_text(already captured onAgentResult) for the marker after each iteration.2. Frontmatter configuration
3. Behavior
<!-- ralph:state idle -->markerdelayseconds, thendelay * backoff, thendelay * backoff^2, ..., capped atmax_delayidle.max): if total consecutive idle time exceeds this duration, stop the loop with a clear message. Supports human-readable durations (30m,6h,1d)Ctrl+Cskips the delay and runs immediately; doubleCtrl+Cstops the loopITERATION_IDLEevent type so UIs can render idle state distinctly from completed/failedThe marker format (
<!-- ralph:state <name> -->) should be designed to be expandable for future per-state config, but onlyidleis handled initially. If theidleblock is absent from frontmatter, the loop runs exactly as before with no behavior change.4. Implementation notesi
The engine already has the right hooks:
result_textonAgentResult/IterationEndedDatacarries agent output. Parse markers from there_delay_if_needed()inengine.pyalready handles interruptible sleep between iterations. Extend it with backoff logic_frontmatter.pyparses the YAML config. Addidleas a new frontmatter fieldRunConfiggets new idle-related fields;RunStatetracks consecutive idle count and cumulative idle duration