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

Closed
opened 2026-06-20 13:18:40 +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.

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 (that's the follow-up phase).
  • No changes to attach.rs forwarding.

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 · original status: Backlog

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. ## 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 (that's the follow-up phase). * No changes to `attach.rs` forwarding. ## 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** · original status: Backlog*
This repo is archived. You cannot comment on issues.
1 Participants
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: naps62/maestro#8