a73x

ba2f646a

docs: decide one read per readiness, and the seal after the last rollback

a73x   2026-08-27 13:22

Commit message
docs: decide one read per readiness, and the seal after the last rollback

docs/decisions.md
Old New
@@ -7061,3 +7061,47 @@ share a daemon with an earlier group and are refused by name. Running groups
7061 alone found two defects the full suite could not: the leak sweep's 7061 alone found two defects the full suite could not: the leak sweep's
7062 vacuous-green guard was keyed to daemon one's capture, and two socket names 7062 vacuous-green guard was keyed to daemon one's capture, and two socket names
7063 were declared in the wrong group. 7063 were declared in the wrong group.
7064
7065 ## 2026-08-27 — the daemon reads a frame in one read, and seals what an upgrade adopted
7066
7067 **One peer that stopped mid-frame parked the whole daemon.** Found on a live
7068 wall: `muxd stats` came back rc=124, and the daemon's stack said
7069 `unix_stream_read_generic` — one client had written part of a frame and gone
7070 quiet, and `proto.readFrame`'s loop was still waiting for the rest, with every
7071 other session's output, every await and every attach behind it. Killing that
7072 one client freed everything instantly. One byte on a fresh daemon reproduces
7073 it. The daemon side now does exactly one `read()` per readiness into a
7074 per-connection buffer and peels whole frames off it with `protocol.delimitFrame`
7075 through a single `Server.takeFrame`; a short read is just a buffer that is not
7076 a frame yet. Observers gained the same buffer, and promotion moves it with the
7077 fd, because the write that carries an attach usually carries the next frame
7078 behind it.
7079
7080 **The client keeps its blocking read on purpose.** There, one thread owns one
7081 transport and has nothing else to do while a frame is in flight, so blocking
7082 until the frame is whole is the correct shape — the entry on `Conn.awaitFrameFd`
7083 already argues that half, and this changes nothing about it. The rule is about
7084 who is multiplexed, not about which read is safer.
7085
7086 **What is still blocking on the daemon, and why it is filed rather than fixed.**
7087 Observer replies go out through `writeFrame` on the raw fd. Only
7088 `debug_dump --vt` can plausibly exceed the ~200 KiB socket send buffer, and only
7089 for a local peer that asks and then stops reading. Real, small, and not the
7090 thing that was wedging walls.
7091
7092 **An upgrade adopted fds and never put the flag back.** `execUpgrade` clears
7093 `FD_CLOEXEC` on the listener, the QUIC socket, every pty master, every agent
7094 listener and the manifest memfd so they survive the exec. Nothing re-set it, so
7095 every shell spawned after an upgrade inherited the lot: the listener on fd 3,
7096 an agent socket on 5, a pty master, and one `memfd:mux-upgrade` per survived
7097 upgrade — and that memfd carries the QUIC key bytes. `Server.sealAdoptedFds`
7098 now re-seals them, and the memfd is closed rather than sealed, having been read.
7099
7100 **The seal is placed after the last rollback exit, not in `initFromManifest`.**
7101 Rollback re-execs the OLD binary with those same fds, and a sealed fd does not
7102 cross that exec; the first placement broke the rollback e2e leg with a panic.
7103 So the call lives in `resumeRun`, past the point where rollback can still fire.
7104 The witness asks the OS, not the daemon: `/proc` for the post-exec shell's fd
7105 table (`socket:`, `memfd:`, `/dev/ptmx`) and the daemon's own for
7106 `memfd:mux-upgrade`. The QUIC socket's seal is unpinned — no e2e daemon in that
7107 leg runs `--quic`; filed.