a73x

547a0911

docs: record the socket-path watch, its log trail and the QUIC admin door

a73x   2026-09-05 19:04

Commit message
docs: record the socket-path watch, its log trail and the QUIC admin door

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

CLAUDE.md
Old New
@@ -447,6 +447,21 @@ native session panes; it is built only by the native steps and is never installe
447 plus 68 bytes plus the pid's digits, and neither of those directories 447 plus 68 bytes plus the pid's digits, and neither of those directories
448 leaves room for it in 103 (measured 2026-09-03, docs/decisions.md). 448 leaves room for it in 103 (measured 2026-09-03, docs/decisions.md).
449 - No socket stealing: `mux d start` refuses a path another daemon owns. 449 - No socket stealing: `mux d start` refuses a path another daemon owns.
450 - **A deleted socket path is taken back within a second, and the log says
451 so.** A unix listener outlives its path: delete the file and the daemon
452 keeps every session on an inode nothing can reach by name (2026-09-04, a
453 live box, three sessions stranded and a second daemon auto-started on
454 the path). `Server.watchSockPath` stats the path once a second against
455 the bound inode and re-binds a missing one through the same
456 `serve.bind(.refuse_live)` a start uses — so a successor already holding
457 the path is refused, not stolen from, and its own stop hands the path
458 back on the next tick. Every socket event is one `mux d: socket PATH:`
459 line on the daemon's stderr (claim branch, bind dev+ino, loss, re-bind
460 or its refusal by name, unlink verdict at exit), and the wall appends
461 the errno its dial saw before it auto-starts. For the daemon a successor
462 keeps path-less, `mux d stop|dump|stats --quic HOST[:PORT] [--key]` is
463 the other door — the daemon has always served those verbs on a QUIC
464 client slot. `upgrade` stays `--sock`.
450 465
451 ## Working rules 466 ## Working rules
452 467
docs/decisions.md
Old New
@@ -9220,3 +9220,78 @@ also lets a new client attach to snapshots retained by an existing daemon.
9220 Terminal consumers still never receive raw control bytes from those cells. 9220 Terminal consumers still never receive raw control bytes from those cells.
9221 ASCII runs containing controls and structurally malformed rows remain 9221 ASCII runs containing controls and structurally malformed rows remain
9222 errors; the snapshot corruption checks are unchanged. 9222 errors; the snapshot corruption checks are unchanged.
9223
9224 ## 2026-09-05 — a deleted socket path: a trail in the log, a re-bind, and a QUIC door
9225
9226 Issues 04b3019d and 145807a2, from one incident on 2026-09-04: a daemon's
9227 `muxd.sock` vanished from `/run/user/1000` between 13:21 and 13:45. The
9228 daemon (three days up, upgraded in place that morning) kept its three
9229 sessions and kept listening on the unlinked inode; nothing could reach it
9230 by name; the next `mux` auto-started a second daemon on the same path; and
9231 the day's log held ghostty stream warnings and nothing else. Who deleted
9232 the file was never established.
9233
9234 ### What was decided
9235
9236 **The daemon logs its socket.** One `mux d: socket PATH: ...` line per
9237 event on stderr, which `forkDetached` points at the xdg log: the claim's
9238 branch (`nothing there` / `cleared a dead daemon's leftover`), the bind
9239 with dev+ino, an adopt across an upgrade, the loss of the path (`now
9240 missing` or `now dev=D ino=I` for a replacement), a re-bind or its refusal
9241 by error name, and the unlink guard's verdict at exit (`unlinked` / `left
9242 in place: it names another daemon's socket now` / `left in place: already
9243 gone`). The shared modules stay silent — `serve` is also askpass's binder
9244 inside a wall, where a print lands on a pane — so `sockpath.claim` returns
9245 a `Claimed`, `serve.Bound.close` and `unlinkIfOurs` return an `Unlink`,
9246 and the daemon prints. The wall's `startLocalDaemon` appends ONE line to
9247 the same log before it forks, naming what its dial answered
9248 (`FileNotFound` for an absent path, `ConnectionRefused` for a dead socket
9249 file): that line is what would have dated the second daemon against the
9250 first one's loss. `sockpath.probe` is `answers` with the error kept.
9251
9252 **The daemon takes the path back.** `Server.watchSockPath`, once a second
9253 from `pumpOnce`: one `fstatat` against `bound.path_id`. A missing path is
9254 re-bound through `serve.bind(.refuse_live)` — the same claim a start makes,
9255 so "no socket stealing" holds by construction — and the old listener fd is
9256 closed (accepted connections are their own fds; the old backlog held
9257 nothing reachable by name). A successor holding the path is
9258 `DaemonAlreadyRunning` from claim, logged once per reason, and the watch
9259 keeps looking: the successor's own `mux d stop` unlinks its file and the
9260 next tick reclaims. The window for a wall to auto-start a second daemon
9261 shrinks from forever to one tick. Rejected: a stat on every accept (a
9262 path-less daemon receives no accepts by path, so it would never fire — the
9263 timer is the only place the check can live) and re-binding without the
9264 claim (that IS socket stealing).
9265
9266 **The admin verbs get a second door.** `mux d stop|dump|stats --quic
9267 HOST[:PORT] [--key FILE]`, for the daemon a successor keeps path-less. The
9268 daemon has served those three verbs on a QUIC client slot since `mux a
9269 --quic` existed (`handleDaemonVerb` runs before the client-slot switch), so
9270 this is client-side only: `AdminTarget` in main.zig, the key by the same
9271 three-way rule as `mux a`. A QUIC ask is bounded (5 s) where the socket's
9272 is not, because UDP has no "nothing listening" errno. Stop's verdict over
9273 QUIC is the daemon's CONNECTION_CLOSE (`Server.deinit` tears client slots
9274 down before anything else), and a port that never answered is rc 1 — the
9275 socket's idempotent 0 is honest only where the errno says "absent", and
9276 silence over UDP may be a firewall or the wrong key. `--sock` with `--quic`
9277 on those verbs is a parse refusal; `start` still takes both. `upgrade` stays
9278 `--sock`: `upgrade_req` is served to observers only, and its manifest is
9279 local to the box.
9280
9281 ### Measured
9282
9283 - Unit: the server test deletes the path, sees the re-bind within one tick,
9284 dials it and gets a `sessions_reply`; binds a successor, sees
9285 `DaemonAlreadyRunning` recorded and the successor's inode untouched;
9286 closes the successor without unlinking and sees the leftover cleared and
9287 the path reclaimed.
9288 - e2e, boot group: a real detached daemon, `rm` of its socket, `mux d stats
9289 --sock` answering again within the budget, the loss and re-bind lines in
9290 the real log with the inode read off the bind line, the marker still on
9291 the grid and the pid unchanged.
9292 - e2e, session group: the `--quic` daemon with its socket deleted; stats,
9293 dump and stop over UDP; the stop graded by the pid going and the file
9294 gone; a second stop on the dead port rc 1.
9295 - `make check` flaked once on `Pty.adopt`'s 5 s exit-code wait while a
9296 sibling worktree's `make ci` was loading the box; green on the re-run,
9297 and unrelated to any file this branch touched.