Skip to content

Idle-builder watchdog: catch the stops a Stop hook structurally cannot see #56

Description

@pseudoseed

Split out of #41 so that PR #46 can close it without deleting this work.

#46 ships the Stop hook, which is the primary mechanism: it fires at the moment a builder ends its turn mid-phase, synchronously, at zero latency. That covers the two reported incidents.

It cannot see three things, all of which look identical from outside:

  1. A builder whose process died. No turn ends, so no Stop hook fires. Remedy is afx spawn <id> --resume.
  2. A builder wedged inside a turn that never ends — a foreground poll loop, or a wait on a producer that already died (roles/builder.md documents a 45-minute case). The hook only runs at turn end, which never arrives.
  3. A builder whose pane is alive but whose harness has no Stop-hook support. The hook is emitted only for the Claude harness, via CLAUDE_HARNESS. opencode and codex builders get nothing.

Signal

Unchanged from #41: Tower already classifies every agent pane idle-vs-busy through the render gate (gate-profiles.ts carries an idleIndicatorPattern per harness; classifyBuffer in render-gate.ts distinguishes busy from idle from drafting). That is the same machinery afx send uses to decide deliverability, so it is load-bearing and measured against committed fixtures. Porch knows the phase. Nothing joins them.

Constraints

Carried over, and the first two are hard requirements:

  • Route through the mailbox, never a raw PTY write. Spec 1313 made afx send mailbox-first for a reason. held is an acceptable outcome; force-injection is not.
  • A nudge is never a gate approval. It must not read as authorization to pass a gate. Note the Stop hook's own gate predicate is status === 'pending' && requested_at — a bare pending means "seeded at project creation, never reached", which is what made the first version of that hook a total no-op.
  • Silence on the ambiguous case. No gate profile, or no-live-pty, means the watchdog cannot tell idle from busy and must say so to the architect rather than nudging on a guess.
  • Do not derive the stall from updated_at. Spec 1470 documents why: next() writes no state on the normal path, so updated_at stays pinned for the whole of a healthy build.
  • Log every nudge. A transcript with unexplained continue-messages is harder to debug, not easier.

Known cost to design around

The Stop hook nudges once per cycle, and stop_hook_active resets on each user message — so a human or architect asking a mid-phase builder a question already pays one forced extra turn. A watchdog layered on top must not compound that.

Metadata

Metadata

Assignees

No one assigned

    Labels

    area/towerTower, afx, terminals, messagingenhancementNew feature or requestfork-onlyDeliberately not upstreamed to cluesmith/codev

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions