Commit Graph

3 Commits

Author SHA1 Message Date
34b41be1df Cancel pending timers when a child is closed
Stale timer bodies were re-delivered to the orchestrator pane after
the parent had already processed the sub-agent's reply and called
close_process. The timer registry held no link to the child
lifecycle, so timers owned by or watching the closed child lingered
until something triggered a fire — e.g. a trailing classifier tick
for the now-removed child.

Add an OnChildClosed hook to ChildEventListener, emit it from
Session.Close (and the terminal-corpse path in reapChild), and have
the timer manager prune the registry: cancel timers owned by the
closed child; remove the closed child from each timer's watched
list (cancel the timer outright when watched empties).

Natural exit deliberately does not route through this hook — the
classifier already emits an idle transition on exit which delivers
any legitimate "fire when sub-agent finishes" semantics exactly
once; cancelling on exit would swallow that.
2026-05-18 12:37:32 +01:00
543c7cc59a Fix idle timer review issues 2026-05-15 11:18:03 +01:00
2b9e1ed77c Add idle-state classifier and Solo-parity timer tools
Classifies every running child as idle/working/thinking/permission/error
using one of three pluggable strategies (output_activity,
osc_title_stability, osc_title_status) plus optional regex promoters
applied to the tail of recent output. State and last-match reason are
exposed via MCP on ProcessInfo and get_process_status. Per-preset
configuration lives on a new preset.IdleDetection block with bundled
defaults for the first-party claude/codex/opencode presets.

OSC title plumbing is exposed as Emulator.Title(), polled from the
session pump after each emulator write so title-change activity feeds
into the classifier without an extra cgo callback.

The MCP timer surface expands to match Solo: timer_set,
timer_fire_when_idle_any/all, timer_cancel, timer_pause, timer_resume,
timer_list. timer_wait is now a thin wrapper that shares the same
manager so it shows up in timer_list while pending. Timer bodies are
delivered to the owner process through the existing
InjectAsOrchestrator path. Top-level (non-agent) callers can attach
timers to a specific process via owner_process_id; omitting it grants
universal cancel/pause/resume/list privileges.

The sidebar gains a state glyph per process row and appends a
nearest-timer indicator when one is pending or paused.

Tests: idle_test.go covers the classify() pure function across the
three strategies and regex promotion; timers_test.go covers the
manager. Harness scenarios cover output_activity, osc_title_stability,
osc_title_status, and regex promotion, plus timer_set delivery,
cancel, pause/resume, idle_any-on-transition, idle_all-pending, and
idle_all-already-satisfied. A new wait_until_mcp harness step type
polls an MCP method until an assertion holds.
2026-05-15 09:49:59 +01:00