daemon: infer session activity state (Active/Idle/AwaitingInput) + foreground process classification #18

Closed
opened 2026-06-20 14:19:15 +01:00 by naps62-yolo · 0 comments
Collaborator

Today the daemon is a transparent pipe: it forwards raw PTY bytes (pty.rs run_reader → broadcast → attach.rs) and the only session state is SessionStatus::{Running, Dead} (maestro-protocol/src/lib.rs). There's no way for clients/registry to know what's running inside a session or whether it's working, idle, or waiting on the user.

This adds an orthogonal activity dimension inferred entirely from structured tmux//proc state — no byte-stream parsing, no work on the attach hot path.

Model changes (maestro-protocol)

Additive/optional so the existing snapshot + diff flow stays forward-compatible:

  • New ActivityState enum:
    • Active — producing output / foreground busy
    • Idle — foreground process alive but quiescent
    • AwaitingInput — blocked on a read() from the pty
  • Extend Session:
    • foreground: Option<String> — current pane command (e.g. claude, bash)
    • activity: ActivityState
    • last_activity_at: DateTime<Utc>

Keep SessionStatus unchanged — activity is a separate field, not an overload of status. Do not put this in the metadata map — that bag is client-owned and opaque to the daemon; these are daemon-derived typed fields.

Detection (daemon, in the existing liveness watcher)

Piggyback on the watcher tick that already polls every ~2s — do not add a new timer.

  1. Foreground command (classifier for claude/codex/opencode/bash):
    tmux list-panes -t maestro-<id> -F '#{pane_pid} #{pane_current_command}'
  2. Quiescence: one Instant per PtyBridge, bumped on each read in run_reader (pty.rs ~line 234). No bytes for N seconds ⇒ not Active.
  3. AwaitingInput: from pane_pid, walk to the deepest descendant; check /proc/<pid>/stat state S + an fd whose readlink points at the pane's pts. Best-effort — if proc inspection fails, fall back to Idle.
  4. Combine: recent output → Active; else input-blocked → AwaitingInput; else → Idle.

Emission

  • On any state transition, emit the existing SessionUpdated variant (registry fan-out + clients get it for free).
  • Add hysteresis/debounce (e.g. only flip to Idle after the quiescence window) so transitions don't spam diffs.

Out of scope

  • No PTY byte-stream parsing.
  • No agent-specific prompt detection (follow-up issue).
  • No changes to attach.rs forwarding.
  • TUI/web rendering beyond maestro list (separate display work).

Risks / notes

  • pane_current_command can show the shell rather than the agent if the agent is a subprocess — the proc-walk covers that case.
  • tmux subprocess calls block the async runtime (known issue elsewhere in the repo). Keep detection off the hot path — spawn_blocking or the watcher's own task.

Acceptance

  • Spawning claude vs bash reflects the correct foreground in maestro list.
  • An idle shell reports Idle; a shell blocked at a prompt reports AwaitingInput; a session mid-output reports Active.
  • Transitions emit SessionUpdated; no diff storms at steady state.

Migrated from Linear NAP-340.

Today the daemon is a transparent pipe: it forwards raw PTY bytes (`pty.rs` `run_reader` → broadcast → `attach.rs`) and the only session state is `SessionStatus::{Running, Dead}` (`maestro-protocol/src/lib.rs`). There's no way for clients/registry to know *what's running inside* a session or *whether it's working, idle, or waiting on the user*. This adds an **orthogonal activity dimension** inferred entirely from structured tmux/`/proc` state — **no byte-stream parsing**, no work on the attach hot path. ## Model changes (`maestro-protocol`) Additive/optional so the existing snapshot + diff flow stays forward-compatible: - New `ActivityState` enum: - `Active` — producing output / foreground busy - `Idle` — foreground process alive but quiescent - `AwaitingInput` — blocked on a `read()` from the pty - Extend `Session`: - `foreground: Option<String>` — current pane command (e.g. `claude`, `bash`) - `activity: ActivityState` - `last_activity_at: DateTime<Utc>` Keep `SessionStatus` unchanged — activity is a separate field, not an overload of status. **Do not put this in the `metadata` map** — that bag is client-owned and opaque to the daemon; these are daemon-derived typed fields. ## Detection (daemon, in the existing liveness watcher) Piggyback on the watcher tick that already polls every ~2s — **do not add a new timer**. 1. **Foreground command** (classifier for claude/codex/opencode/bash): `tmux list-panes -t maestro-<id> -F '#{pane_pid} #{pane_current_command}'` 2. **Quiescence**: one `Instant` per `PtyBridge`, bumped on each read in `run_reader` (`pty.rs` ~line 234). No bytes for N seconds ⇒ not `Active`. 3. **AwaitingInput**: from `pane_pid`, walk to the deepest descendant; check `/proc/<pid>/stat` state `S` + an fd whose readlink points at the pane's pts. Best-effort — if proc inspection fails, fall back to `Idle`. 4. **Combine**: recent output → `Active`; else input-blocked → `AwaitingInput`; else → `Idle`. ## Emission - On any state transition, emit the existing `SessionUpdated` variant (registry fan-out + clients get it for free). - Add hysteresis/debounce (e.g. only flip to `Idle` after the quiescence window) so transitions don't spam diffs. ## Out of scope - No PTY byte-stream parsing. - No agent-specific prompt detection (follow-up issue). - No changes to `attach.rs` forwarding. - TUI/web rendering beyond `maestro list` (separate display work). ## Risks / notes - `pane_current_command` can show the shell rather than the agent if the agent is a subprocess — the proc-walk covers that case. - tmux subprocess calls block the async runtime (known issue elsewhere in the repo). Keep detection off the hot path — `spawn_blocking` or the watcher's own task. ## Acceptance - Spawning `claude` vs `bash` reflects the correct `foreground` in `maestro list`. - An idle shell reports `Idle`; a shell blocked at a prompt reports `AwaitingInput`; a session mid-output reports `Active`. - Transitions emit `SessionUpdated`; no diff storms at steady state. --- _Migrated from Linear NAP-340._
naps62-yolo self-assigned this 2026-06-21 15:32:28 +01:00
This repo is archived. You cannot comment on issues.
1 Participants
Due Date
No due date set.
Reference: naps62/maestro#18