Skip to content

State-aware idle detection with backoff and max idle duration #28

@malpou

Description

@malpou

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions