145bed7e
docs: the cold attach is one ssh run
a73x 2026-08-29 14:38
Commit message
CLAUDE.md
| Old | New | ||
|---|---|---|---|
| @@ -185,15 +185,19 @@ paths (`src/cli/main.zig` is the daemon). Test fixtures in `test/`: | |||
| 185 | per machine is the accepted cost. `mux d run` exits 0 on every shutdown — | 185 | per machine is the accepted cost. `mux d run` exits 0 on every shutdown — |
| 186 | a supervisor reads nonzero on a clean stop as a crash — and nonzero only | 186 | a supervisor reads nonzero on a clean stop as a crash — and nonzero only |
| 187 | for the boot failures `main.run` picks before anything binds. | 187 | for the boot failures `main.run` picks before anything binds. |
| 188 | - **A daemon starts only when someone asks; a read never starts one.** | 188 | - **A daemon starts only when someone asks; a read never starts one, and |
| 189 | `mux d start`, or the local client's own entry spawn, are the only | 189 | the ask is a WORD.** `mux d start`, `mux d endpoint --start`, or the local |
| 190 | starters. `mux d endpoint` and `mux d proxy` answer what is on the box and | 190 | client's own entry spawn are the only starters. Bare `mux d endpoint` and |
| 191 | exit 1 when nothing is — the wall polls `endpoint` over ssh once a second | 191 | `mux d proxy` answer what is on the box and exit 1 when nothing is — the |
| 192 | per host, and a poll that started daemons undid every remote `mux d stop` | 192 | wall polls `endpoint` over ssh once a second per host, and a poll that |
| 193 | within a second (found on a live box). The dial a user ASKED for — the | 193 | started daemons undid every remote `mux d stop` within a second (found on |
| 194 | `mux HOST` entry dial, picker Enter — runs `ssh HOST 'mux d start'` once | 194 | a live box). The dial a user ASKED for — the `mux HOST` entry dial, picker |
| 195 | and redials once (`HandoffTarget.asked`, spent after the first attempt); | 195 | Enter — runs `ssh HOST 'mux d endpoint --start'` ONCE, and that run's |
| 196 | the poll, `mux hosts` and every redial read `unreachable` instead. | 196 | announce IS the redial: the remote ensures the daemon and announces on the |
| 197 | same stdout (`HandoffTarget.asked`, spent after the first attempt). The | ||
| 198 | poll, `mux hosts` and every redial spell the bare verb and read | ||
| 199 | `unreachable` instead, so the rule holds by argv rather than by a client | ||
| 200 | branch. `--start` is `endpoint`'s alone; every other verb refuses it. | ||
| 197 | `asked` DEFAULTS to false: a new dial path that forgets the field starts | 201 | `asked` DEFAULTS to false: a new dial path that forgets the field starts |
| 198 | nothing, because a missing start says so on screen while a spurious one is | 202 | nothing, because a missing start says so on screen while a spurious one is |
| 199 | a daemon on someone else's box that nothing reports. | 203 | a daemon on someone else's box that nothing reports. |
| @@ -213,11 +217,14 @@ paths (`src/cli/main.zig` is the daemon). Test fixtures in `test/`: | |||
| 213 | on the parent and hits `unreachable // own pid is always valid` | 217 | on the parent and hits `unreachable // own pid is always valid` |
| 214 | (decisions.md, 2026-08-28). | 218 | (decisions.md, 2026-08-28). |
| 215 | - **Remote is still ssh, and still by name.** `handoff` spells | 219 | - **Remote is still ssh, and still by name.** `handoff` spells |
| 216 | `ssh HOST 'mux d endpoint'` / `'mux d start'`, so a remote running an | 220 | `ssh HOST 'mux d endpoint'` / `'mux d endpoint --start'`, so a remote |
| 217 | installation of v0.0.1-15 or older reads `[unreachable]` until it is | 221 | running an installation of v0.0.1-15 or older reads `[unreachable]` until |
| 218 | upgraded. There is no in-place upgrade across the rename either: a ≤15 | 222 | it is upgraded — ≤15 knows neither the mode word nor the flag, and answers |
| 219 | daemon probes the candidate for `muxd <version>` and this binary answers | 223 | a usage page on stderr with rc 2 and no announce (measured against the |
| 220 | `mux <version>`, so that one migration is `mux d stop` then `mux d start`. | 224 | released v0.0.1-15 tarball). There is no in-place upgrade across the |
| 225 | rename either: a ≤15 daemon probes the candidate for `muxd <version>` and | ||
| 226 | this binary answers `mux <version>`, so that one migration is | ||
| 227 | `mux d stop` then `mux d start`. | ||
| 221 | - **The layout sidecar is derived convenience, not authored intent.** | 228 | - **The layout sidecar is derived convenience, not authored intent.** |
| 222 | `$XDG_STATE_HOME/mux/layout` stores the pane tree on last detach, from | 229 | `$XDG_STATE_HOME/mux/layout` stores the pane tree on last detach, from |
| 223 | every wall left on a TERMINAL (`shared.is_tty`) — a piped `mux` is a wall | 230 | every wall left on a TERMINAL (`shared.is_tty`) — a piped `mux` is a wall |
README.md
| Old | New | ||
|---|---|---|---|
| @@ -189,14 +189,15 @@ mux HOST # attach; Ctrl-\ d detaches, running it again reattaches | |||
| 189 | `mux d start` spawns the daemon detached and waits until it answers, so it | 189 | `mux d start` spawns the daemon detached and waits until it answers, so it |
| 190 | is safe to run every time — if one is already up it says so and exits 0. | 190 | is safe to run every time — if one is already up it says so and exits 0. |
| 191 | 191 | ||
| 192 | `mux HOST` runs `mux d endpoint` on HOST over ssh (finding a `mux` in | 192 | `mux HOST` runs `mux d endpoint --start` on HOST over ssh, ONE run |
| 193 | `~/.local/bin` even though ssh's non-login shell leaves it off PATH). On a | 193 | (finding a `mux` in `~/.local/bin` even though ssh's non-login shell leaves |
| 194 | box with no daemon that verb answers nothing, and `mux HOST` runs `mux d | 194 | it off PATH). That verb ensures a daemon and then announces the |
| 195 | start` there once, over the same ssh, before asking again — so the | 195 | coordinates on the same stdout, so a cold attach costs one ssh login and |
| 196 | `ssh HOST 'mux d start'` line above is optional after the first time. | 196 | the `ssh HOST 'mux d start'` line above is optional after the first time. |
| 197 | Nothing else starts a daemon: the wall's per-host poll, `mux hosts`, every | 197 | Nothing else starts a daemon, because nothing else spells the flag: the |
| 198 | reconnect and `mux --via 'ssh HOST mux d proxy'` all report the box | 198 | wall's per-host poll, `mux hosts`, every reconnect and `mux --via 'ssh HOST |
| 199 | unreachable and leave it alone. Your | 199 | mux d proxy'` run the bare `mux d endpoint`, report the box unreachable and |
| 200 | leave it alone. Your | ||
| 200 | ssh config (aliases, ports, | 201 | ssh config (aliases, ports, |
| 201 | ProxyJump) all keeps working, since `mux` never parses HOST. That ssh | 202 | ProxyJump) all keeps working, since `mux` never parses HOST. That ssh |
| 202 | fetches the daemon's QUIC port and key once and the session moves onto | 203 | fetches the daemon's QUIC port and key once and the session moves onto |
docs/decisions.md
| Old | New | ||
|---|---|---|---|
| @@ -7504,3 +7504,24 @@ Found by the rewritten e2e leg: a `POST` carrying no body and no | |||
| 7504 | `content-length` — what `curl -X POST` sends — reached std's `discardBody` | 7504 | `content-length` — what `curl -X POST` sends — reached std's `discardBody` |
| 7505 | assert and ABORTED the hub, every tile with it. Any answer to a | 7505 | assert and ABORTED the hub, every tile with it. Any answer to a |
| 7506 | body-capable request that declared no length now closes its connection. | 7506 | body-capable request that declared no length now closes its connection. |
| 7507 | |||
| 7508 | ## 2026-08-29 — the cold handoff is one ssh run: `mux d endpoint --start` | ||
| 7509 | |||
| 7510 | - **The three runs.** A cold `mux HOST` cost three ssh logins because the | ||
| 7511 | CLIENT decided to start: `mux d endpoint` exited 1 having written nothing, | ||
| 7512 | the client reaped the child to read that code, ran `ssh HOST 'mux d start'`, | ||
| 7513 | then ran `mux d endpoint` again. Measured on the suite's ssh shim, one line | ||
| 7514 | per run: `e2e_04_handoff.sh`'s cold leg pinned 3 and now pins 1. | ||
| 7515 | - **Why it folds.** The reap existed only to tell the remote's refusal (exit | ||
| 7516 | 1) from ssh's own failure (255), which the announce cannot; the remote | ||
| 7517 | deciding deletes the question, and `mux d start` was already idempotent, so | ||
| 7518 | "ensure, then answer" has no race in it. `mosh-server new`'s shape: one ssh | ||
| 7519 | run whose first stdout line is the coordinates. | ||
| 7520 | - **"A read never starts a daemon" now holds by ARGV**, not by a branch a new | ||
| 7521 | dial path could forget: a poll, `mux hosts` and every redial spell | ||
| 7522 | `mux d endpoint` and start nothing. `--start` is `endpoint`'s alone | ||
| 7523 | (`mux d stats --start` is rc 2, not a flag that vanishes) and its ensure | ||
| 7524 | forwards `--sock`, so the daemon binds the path the announce then probes. | ||
| 7525 | - **What ≤15 remotes read.** Measured against the released v0.0.1-15 tarball: | ||
| 7526 | rc 2, a usage page on stderr, nothing on stdout — so the asked dial reads | ||
| 7527 | `[unreachable]`, the same answer the rename already gave them. | ||