Idle Watchdog Concept¶
Home > Concepts > Idle Watchdog
This page explains why amplihack supervises agentic child processes with idle detection instead of wall-clock timeouts. It is the design rationale for GitHub issue #867.
Type: Explanation (Understanding-Oriented)
The Owner Rule¶
Never impose a wall-clock timeout that kills a working agentic step. Use idle/liveness detection instead — kill only a genuinely hung child (no stdout/stderr progress for an idle window), never a child that is still producing output.
Everything below follows from this rule.
The Problem With Wall-Clock Timeouts¶
A fixed deadline (for example "kill after 300 seconds") cannot tell the difference between two very different children:
- A healthy agent streaming tokens, editing files, and running tests. It may legitimately take 20 minutes on a hard task.
- A hung child that deadlocked, lost its connection, or is waiting on input that will never arrive. It produces nothing.
A wall-clock timeout kills both. Killing the first is a correctness bug: it discards good work, corrupts multi-turn agent state, and makes long tasks impossible. This is exactly what issue #867 fixes.
The Fix: Idle Detection¶
The idle watchdog watches output, not the clock.
byte on stdout/stderr ──► reset "last progress" timer
│
every poll tick ──┤
▼
now - last_progress > idle_timeout ? ──yes──► kill child
│
no ──► keep waiting
- Any new byte on
stdoutorstderrproves the child is alive and resets the timer. - The child is killed only after the idle threshold passes with zero output.
- There is no absolute cap: an agent that keeps talking runs as long as it needs.
For call sites whose output is redirected to a log file (so no live pipe is readable), "progress" is the log file's growing modification time instead of a byte on a pipe. Same principle, different signal.
One Helper, Consistent Behavior¶
A single module — amplihack-utils::idle_watchdog — implements the mechanism
once and is reused everywhere. It lives in a foundational crate with no
amplihack dependencies, so any crate can use it without creating dependency
cycles. It offers three entry points for three runtime shapes:
| Entry point | Runtime | Progress signal |
|---|---|---|
wait_with_idle_watchdog |
tokio | bytes on child pipes |
wait_with_idle_watchdog_sync |
blocking std | bytes on child pipes |
file_idle_since |
any | log file mtime growth |
See the Idle Watchdog Reference for exact signatures and configuration.
Where It Applies¶
Six subprocess call sites previously used wall-clock kills. All now defer to idle detection:
- Cascade steps — advance on the child's real exit status, not elapsed time.
- Multitask runs — the default policy
continue-preservenever kills on elapsed time;interrupt-preservekills only when the log file goes idle. - Copilot CLI client — an idle watchdog replaces the fixed CLI deadline.
- Remote executor — streams output and applies idle detection instead of a buffered wall-clock cap.
- Copilot adapter — the 600 s wall-clock clamp is removed; the adapter's configured timeout is reinterpreted as an idle-window override and handed to the site-3 client, so completion is driven by turn/goal completion plus idle detection rather than a fixed deadline.
- Foundational process wait — the shared idle-watchdog wrapper replaces the
timeout(child.wait())pattern;kill_on_drop(true)stays for drop-safety.
Site 1 (cascade) drives its child through this foundational wait (site 6), so
fixing the foundation fixes cascade kill behavior too. Site 5 (Copilot adapter)
does not flow through it — run_agent delegates to the SDK client, whose
subprocess is supervised by the site-3 sync watchdog in the Copilot CLI client.
What Stays a Timeout¶
Idle detection is right for agentic work whose duration is unpredictable. It is wrong for bounded network operations, which should fail fast. These keep their genuine timeouts:
- Git bundle transfer / download
azlinlist / kill / provision / connecttmuxcontrol operations
A stalled network call should time out; a thinking agent should not.
Trade-Offs¶
- Latency to detect a hang: a truly hung child survives up to one idle window (default 300 s) before it is reaped. This is deliberate — the cost of a rare, slightly-late kill is far lower than the cost of killing healthy agents.
- Noisy-but-stuck children: a child that keeps printing while making no real progress is not detected, by design. Idle detection tracks liveness, not usefulness; goal/turn completion is the agent's job to signal.
Related Docs¶
- Idle Watchdog Reference — API and configuration.
- Use the Idle Watchdog — wire up a call site.
- Automode Safety — related agent-safety guardrails.