Commit Graph
11 Commits
Author SHA1 Message Date
logaritmiskandClaude Opus 5 b500882257 feat(supervisor): let a server keep its stdin open
Stdio-first MCP servers exit the moment they see EOF on fd 0. Under the
launchd agent the daemon's stdin is /dev/null, so a server that also
speaks HTTP still shuts down seconds after binding its port, and there
was no way to ask xy for anything else.

Adds a per-server `stdin` mode: `inherit` (the default, unchanged),
`null`, or `keep-open`. Under `keep-open` the child gets a pipe whose
write end RealChild holds and never writes to, so a read blocks.

The handle has to live on RealChild rather than on the TokioChild:
`Child::wait()` opens with `drop(self.stdin.take())`, so leaving it
where tokio put it reproduces the original bug the instant supervision
starts.

Refs #2

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TWLFEoRaRafJm1SpdhWQ6F
2026-09-07 09:50:42 +02:00
logaritmiskandClaude Opus 5 516d7d0b86 fix(daemon): keep serving while a gated server waits
The autostart loop awaited each Start ack, and a gated start does not ack
until its wait-for condition resolves. Because the accept loop and the
signal handler are installed after that loop, a server gated on a socket
that is not there yet left the daemon deaf for the whole gate: `xy list`
hung in the backlog and SIGTERM went unhandled. Nothing consumed the ack,
so drop the await and let the gates run concurrently with the accept loop.

Also clear started_at before publishing Waiting. A crash-and-regate kept
the dead process's start instant, so `xy list` showed a growing uptime for
a server that had been down for minutes -- in the one state whose whole
purpose is to explain what is going on.

And log the spawn error when a wait-for command cannot be run at all, so a
misspelled binary is distinguishable from a condition that is merely not
met yet.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EGntTHCW3sEPy1VBRopNNp
2026-08-07 16:17:20 +02:00
logaritmisk a533da8e80 fix(supervisor): non-vacuous retry-budget test, clear stale uptime, sync gate reconfigure 2026-08-07 16:00:42 +02:00
logaritmisk 9307af46a8 feat(supervisor): gate spawns on the wait-for condition 2026-08-07 15:46:38 +02:00
logaritmisk fbb58ebfef feat(protocol): wait-for schema and KDL parsing 2026-08-07 15:33:19 +02:00
logaritmiskandClaude Opus 5 0c8ae3b617 fix(supervisor): compute uptime at read time, not publish time
set_state computed uptime_secs from started_at.elapsed() and pushed the
result into the watch channel. do_start sets started_at immediately before
calling set_state(Running), so the published value was always ~0, and the
list/status handlers copied that frozen snapshot straight out. A server
that stayed healthy reported 0s forever, because the value was only
recomputed on the next state transition.

Status now carries started_at (an Instant, in-process only) and the
handlers derive uptime_secs when they build ServerSummary. The wire type
is unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EGntTHCW3sEPy1VBRopNNp
2026-08-07 13:11:01 +02:00
logaritmiskandClaude Opus 5 d75d57ff0b fix(supervisor): don't count the first start as a restart
do_start incremented restart_count on every spawn, including the initial
one, so a server that had never restarted reported 1. do_start now takes a
StartCause: the explicit Start command (which is also how servers auto-start
at daemon boot) is Initial and does not count; the explicit Restart command
and the automatic post-crash respawn are Restart and do.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EGntTHCW3sEPy1VBRopNNp
2026-08-07 13:09:47 +02:00
logaritmiskandClaude Sonnet 4.6 4a0b32d90e fix(supervisor): StartAck::SpawnFailed surfaces real failures
Add StartAck::SpawnFailed(String) so callers can distinguish a successful
start from a failed spawn. The Start command arm now sends SpawnFailed on
io::Error rather than the misleading Started. handlers.rs maps the new
variant to an RpcErrorCode::SpawnFailed JSON-RPC error response.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 12:32:07 +02:00
logaritmiskandClaude Sonnet 4.6 b366df0482 fix(supervisor): make backoff sleep interruptible by Stop/Shutdown
Replace the bare sleep(delay).await in the Restart backoff arm with a
tokio::select! over the timer and cmd_rx. Stop/Shutdown are now handled
immediately during backoff (Stop → Stopped, Shutdown → clean exit);
Start/Restart/Reconfigure skip the remaining delay and retry at once.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 12:31:32 +02:00
logaritmiskandClaude Sonnet 4.6 3e4ad79137 fix(supervisor): publish full status (pid, port, uptime, restart_count, last_exit) via watch channel
Replace watch::Receiver<ServerState> on SupervisorHandle with watch::Receiver<Status>,
a richer snapshot type that carries pid, port, uptime_secs, restart_count and last_exit.
SupervisorTask maintains current_pid and publishes a fresh Status on every state
transition; handlers.rs reads the full Status so list/status no longer return
zeroed/None fields.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 12:30:56 +02:00
logaritmiskandClaude Sonnet 4.6 a3c979511e feat(supervisor): supervisor task with state machine
One async task per managed server owns all state transitions via a
tokio::select! loop over cmd_rx and wait_child. Includes RealSpawner
and a smoke test covering the Start → Running → exit → Stopped →
Shutdown happy path.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 11:44:12 +02:00