Commit Graph
5 Commits
Author SHA1 Message Date
Miguel Palhas f689c916bf fix(maestro): poll awaiting-input until leaderless state holds
The pipeline leader exits as soon as both stages are forked, which can
beat the surviving stage opening the tty and parking on read — the one-
shot assertion then ran against a group with no parked member yet.
Poll for the answer, accepting it only while the foreground group is
still leaderless so a shell that regained the tty cannot satisfy it.

Fixes #113
2026-08-24 22:50:02 +01:00
Miguel PalhasandClaude Opus 5 9d777e2a22 fix(maestro): snapshot /proc once per pass and check the whole group
ci / web (pull_request) Successful in 26s
ci / rust (pull_request) Successful in 1m51s
ci / web (push) Successful in 32s
ci / rust (push) Successful in 2m31s
The subtree search returned early whenever it found *any* group member,
so an in-tree stage that was busy or sleeping on a timer masked a
reparented member genuinely parked on the tty.

Checking every member instead would have meant a /proc scan per session
per tick whenever nothing was parked — the common case. ProcGroups
snapshots /proc once per activity pass and indexes by process group, so
the cost is one walk however many sessions the daemon owns, and the
subtree walk disappears: the index is a superset of it.

Also fixes the background-helper test, which had stopped discriminating.
It polled until awaiting_input was true and caught a moment before the
helper spawned, so it passed against a deepest-descendant walk too. It now
waits for the helper to exist first. Both regression tests were re-checked
by patching each old implementation back in.

Reported in review on #51.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 17:23:17 +01:00
Miguel PalhasandClaude Opus 5 5c3997d1d3 fix(maestro): search the foreground group for a live member
ci / web (pull_request) Successful in 27s
ci / rust (pull_request) Successful in 1m36s
tpgid names a process *group*, not a process. Its leader can exit while
another member still holds the terminal — a foreground pipeline whose
first stage finished — and then /proc/<tpgid> does not exist, so reading
it directly reported the session idle while it sat waiting on the user.

awaiting_input now searches the pane's subtree for live processes whose
pgrp matches, and asks whether any is parked on the tty. Members stay in
that subtree because their parent shell is alive, so they never reparent.

Reported in review on #51. The regression test needs an interactive shell
— job control is what puts a pipeline in its own process group — and waits
for the leader to actually be gone before asserting, otherwise it passes
against the old lookup while the shell is still foreground.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 17:10:32 +01:00
Miguel PalhasandClaude Opus 5 dbbe330159 test(maestro): drop catch_unwind from the activity poll helper
ci / rust (pull_request) Successful in 2m23s
ci / web (pull_request) Successful in 29s
A plain deadline loop says the same thing without catching a panic to
turn it back into an assert.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 17:05:02 +01:00
Miguel PalhasandClaude Opus 5 1d611ee402 fix(maestro): ask the kernel which process owns the terminal
ci / web (pull_request) Successful in 28s
ci / rust (pull_request) Successful in 1m44s
awaiting_input walked `children` to the deepest descendant, guessing that
the newest child is the foreground one. An agent that starts a background
helper and then blocks on read landed on the helper, so a session genuinely
waiting on the user was reported as idle.

tpgid in /proc/<pid>/stat is the kernel's own answer — the foreground
process group of the controlling terminal — and the group leader's pid is
the group id. One field read replaces a 64-deep walk.

Adds tests/activity.rs: the first end-to-end coverage of this pipeline,
which had none. The background-helper case fails against the old walk and
passes against tpgid.

Also makes the attach handler subscribe before upgrading. Checking
existence and then subscribing inside the callback left a window for the
liveness watcher to reap in between, which attach::serve compensated for
with a clean close; acquiring the subscription up front means the 404 and
the subscription are one step and the window does not exist.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 17:02:13 +01:00