a73x

aa247b30

docs: M14 closed — ssh→QUIC handoff

a73x   2026-08-12 06:34

Commit message
docs: M14 closed — ssh→QUIC handoff

Roadmap: M14 into "Now" (M1–M14) with its own verdict section; the
handoff bullet leaves the candidate list and the section becomes M15,
gaining the two items M14 created (the drain()/ECONNREFUSED fix and the
transport-recipe union).

decisions.md: the M14 section — verdict, the regrade table (3/3, each
catch legible at the check that predicted it), the LAN-box kill
criterion with its numbers, what shipped, the measured deadline and why
2000 stands, six findings, the bde1af5 errata, and what M14 banks.

README: the HOST line described `--via "ssh HOST muxd proxy"`, which has
not been what `mux HOST` does since Task 6. The shell-interpolation
warning stays — it is still true.

e2e.sh: (d)'s timing comment claimed the elapsed time IS the deadline.
It is the deadline plus launch overhead, measured at 26-129ms across 13
runs; the comment now says so, and says why the floor is still safe.

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

README.md
Old New
@@ -53,12 +53,18 @@ mux HOST # attach; Ctrl-\ detaches, running it again reattaches
53 `muxd start` spawns the daemon detached and waits until it answers, so it 53 `muxd start` spawns the daemon detached and waits until it answers, so it
54 is safe to run every time — if one is already up it says so and exits 0. 54 is safe to run every time — if one is already up it says so and exits 0.
55 55
56 `mux HOST` is sugar for `mux --via "ssh HOST muxd proxy"` — ssh config 56 `mux HOST` runs `ssh HOST muxd endpoint` — ssh config (aliases, ports,
57 (aliases, ports, ProxyJump) all keeps working, since `mux` never parses 57 ProxyJump) all keeps working, since `mux` never parses HOST. That ssh
58 HOST. It **is** interpolated into a shell command, so HOST must be trusted: 58 fetches the daemon's QUIC port and key once and the session moves onto
59 feeding it unreviewed from an inventory file or a cloud API is handing that 59 QUIC, leaving no ssh process behind; the coordinates are cached under
60 source a shell. `--via` accepts any command that exposes the daemon's 60 `~/.cache/mux/hosts/`, so later attaches dial QUIC straight away and skip
61 socket on stdio, for custom socket paths or transports. 61 ssh entirely. Where inbound UDP is blocked, the attach lands over ssh one
62 deadline (~2s) later, saying so in a single line, and the session is the
63 same session either way. HOST **is** interpolated into a shell command, so
64 it must be trusted: feeding it unreviewed from an inventory file or a
65 cloud API is handing that source a shell. `--via` accepts any command that
66 exposes the daemon's socket on stdio, for custom socket paths or
67 transports.
62 68
63 If the link drops, the client reconnects and resumes by itself; `Ctrl-\` 69 If the link drops, the client reconnects and resumes by itself; `Ctrl-\`
64 gives up waiting. 70 gives up waiting.
docs/decisions.md
Old New
@@ -2203,6 +2203,287 @@ before and after; no product code was left mutated.
2203 mutation sweep, ASAN/valgrind over the QUIC tests, a `make gate` for 2203 mutation sweep, ASAN/valgrind over the QUIC tests, a `make gate` for
2204 loaded runs, and the M12 pty-scenario timeout notes. 2204 loaded runs, and the M12 pty-scenario timeout notes.
2205 2205
2206 ## 2026-08-11 (M14 — the ssh→QUIC handoff)
2207
2208 **Verdict: cleared, both legs.** `mux HOST` stopped being sugar for an
2209 ssh pipe and became a transport negotiation: one ssh to `muxd endpoint`,
2210 whose first stdout line is a mandatory announce of the daemon's QUIC port
2211 and key, then QUIC carries the session and the coordination ssh is
2212 reaped. The coordinates are cached, so the second attach spawns no ssh at
2213 all. Leg 1: the suite grew to **20 scenario checkpoints over 33
2214 convergence points**, both pinned as literals, with `SOAK_N=10 make soak`
2215 at **10/10 on the code that ships** (d9b17d5). Leg 2: three
2216 resurrections, three catches, each legible at the check that predicted
2217 it. The kill criterion held on the real LAN box (`192.168.0.109`) with
2218 `muxd` on the remote PATH as the one manual step. Spec:
2219 superpowers/specs/2026-08-11-m14-ssh-quic-handoff-design.md.
2220
2221 ### The regrade table
2222
2223 One sitting, one tree, one set of binaries; `make build && make test &&
2224 make e2e` green before, green after, `git status` clean between every
2225 row. Each mutant was required to **compile** — a build failure
2226 masquerading as a kill grades nothing.
2227
2228 | Resurrection | Predicted catch | Verdict |
2229 | --- | --- | --- |
2230 | `serviceObserver`'s `endpoint_req` arm deleted | unit: `Server: endpoint_req binds a listener lazily…` fails first and cheapest; e2e (a) fails too | **CAUGHT** (20s unit, 128s e2e) |
2231 | `endpointCmd`'s announce write deleted | e2e (a) fails at its bounded wait with the scenario's own message, the announce absent from the dump | **CAUGHT** (149s) |
2232 | `openHandoff`'s fallback stderr line deleted | e2e (d) fails its grep while (c)'s absent-grep control stays green in the same run | **CAUGHT** (156s) |
2233
2234 **The observer arm — the unit layer answered first, as predicted, and
2235 named the missing thing itself.** `make test` failed at
2236 `src/server.zig:4634` with `return error.NoEndpointReply`, which is the
2237 whole diagnosis in one identifier, 20 seconds after the mutation. The
2238 e2e corroboration landed too, but **not at the assertion the prediction
2239 named**: (a) died at `e2e FAIL: cold handoff: QUIC took over, so ssh
2240 must be gone: pid 2931354 is still running 2s later`, not at the later
2241 cache/QUIC assertion, because that is the earliest check in (a) the
2242 mutation reaches. The prediction was right about the scenario and wrong
2243 about the line — which is finding 3 below, at assertion granularity
2244 rather than scenario granularity.
2245
2246 **The announce write — the failure says why, not just that.** Both lines
2247 of the scenario's own message printed:
2248
2249 ```
2250 e2e FAIL: cold handoff: no marker in 25s — the attach never converged.
2251 A client that blocks here read no announce line off the ssh pipe.
2252 muxd endpoint: starting…
2253 up (0.1s) pid=2940304
2254 mux: aborted before attaching
2255 ```
2256
2257 The diagnostic dump is what separates this kill from Task 7's
2258 stderr-routing kill, which produces the same first line: here `muxd
2259 endpoint`'s own preamble is present and the `endpoint …` announce is
2260 simply absent from it. The mutation as the plan first wrote it — moving
2261 the write below `return proxy.run(…)` — does not compile: Zig rejects
2262 unreachable code outright, so the sanctioned alternative was taken and
2263 the write deleted. Recorded because "each mutant must compile" and "make
2264 the announce unreachable" were, in this language, incompatible
2265 instructions.
2266
2267 **The fallback line — the control proved it was a control.** (d) failed
2268 with `e2e FAIL: key mismatch printed no fallback line in 15s; its
2269 stderr was:` and an empty capture, in a run whose immediately preceding
2270 line was `e2e OK: a stale cache self-heals: one refetch (2231ms), no
2271 fallback line, real port cached`. (c) asserts that line is *absent* and
2272 (d) asserts it is *present*; deleting the line flipped exactly one of
2273 them. A pair like that is the only way an absent-grep assertion can be
2274 shown to be load-bearing rather than vacuous.
2275
2276 ### The kill criterion, on the LAN box (2026-08-11)
2277
2278 `ubuntu@192.168.0.109`, a static musl `muxd` scp'd to the box and put on
2279 PATH — the one allowed manual step. Both ends zeroed first: remote
2280 daemon stopped, `~/.config/mux/key` removed, local cache entry removed,
2281 every local `mux` run under hermetic `XDG_*` so the developer's own key
2282 and cache were never touched. All three legs then ran with **zero
2283 further manual steps**.
2284
2285 | Leg | Wall | What was observed |
2286 | --- | --- | --- |
2287 | Cold | **244ms** attach → first marker | ssh child (`ssh ubuntu@192.168.0.109 muxd endpoint`, pid 2959493) gone by `kill -0` at the moment the marker painted, while `mux` stayed alive and a second marker sent 8s later still painted |
2288 | Warm | **13ms** attach → marker | **no ssh child at any point** — polled across the whole session, not just the attach |
2289 | Blocked, warm cache | **4264ms** | fallback line printed, session landed over ssh, exit 0 |
2290 | Blocked, cold cache | **2177ms** | same line, one deadline + 177ms |
2291
2292 The cold leg's announce was `endpoint 56407 8560…a307`, written to the
2293 cache verbatim; on the box `ss -uapn` showed `UNCONN 0.0.0.0:56407
2294 users:(("muxd",pid=17958,fd=6))` and `muxd stats` reported `clients=1
2295 deltas=3` — a session being served by deltas over a listener that did
2296 not exist before the attach asked for it. The blocked legs dropped
2297 inbound UDP with `iptables -A INPUT -p udp --dport 56407 -j DROP` and
2298 both printed the exact coordinates they had tried: `mux:
2299 quic://192.168.0.109:56407 unreachable, attaching over ssh`. The rule
2300 was removed on the way out and the removal proved by listing the chain
2301 (`-P INPUT ACCEPT`, nothing matching `--dport 56407`), not by trusting
2302 `iptables -D`'s exit status. Teardown was `muxd stop` over ssh: `muxd:
2303 stopped`, exit 0, then `pgrep -a muxd` empty, no UDP listener, socket
2304 file gone.
2305
2306 **The two blocked numbers are the interesting result, and they are not
2307 the same case.** From a **cold** cache the blackhole costs **one**
2308 deadline, because the ssh that carried the announce is still open and
2309 *is* the fallback transport. From a **warm** cache it costs **two**: the
2310 cached dial spends a deadline, then the refetch over ssh returns the same
2311 (still-blocked) port and spends another before falling back. The spec's
2312 criterion says "within one deadline" and the cold leg meets it as
2313 written; the warm leg is recorded at 4264ms rather than reconciled,
2314 because it is what a user on a UDP-blocked network actually pays on
2315 every attach after their first. It is the direct cost of refusing
2316 negative caching, which remains the right call — network state changes,
2317 and a cached "UDP is blocked" would strand a user on ssh after their
2318 firewall was fixed.
2319
2320 **A stale daemon from M10 had to be killed by pid, not stopped.** The
2321 box was still running the Aug-9 binary, which predates `stop_req`
2322 entirely, so `muxd stop` printed usage and exited 2. It was killed by
2323 its tracked pid and the absence confirmed with `pgrep`. Not part of the
2324 criterion — state zeroing, not attaching — but the shape is worth
2325 knowing: the stop verb is only as available as the binary on the far
2326 side, and a box that has been up across a milestone boundary may not
2327 have it.
2328
2329 ### What shipped
2330
2331 `endpoint_req = 0x08` / `endpoint_reply = 0x89` (`src/protocol.zig`),
2332 with an `encode`/`decode` pair and golden wire bytes pinned as literals,
2333 following the rule M10 extracted — assert the literal, never the
2334 constant the code under test reads.
2335
2336 The daemon's **lazy QUIC bind** (`src/server.zig`). `endpoint_req` is
2337 answered by binding a listener on demand if one is not already up, and
2338 the load-bearing arm is `serviceObserver` for M13's reason: `muxd
2339 endpoint` never attaches, so it is an observer for its whole life. A
2340 `quic_owned` flag records whether the listener was created by this path,
2341 so `deinit` hands back exactly what it took and a daemon started with an
2342 explicit `--quic` keeps ownership of its own.
2343
2344 `src/handoff.zig` — one grammar, one place. Cache read/write, announce
2345 `format`/`parse`, the dial-host strip and the deadline constant all live
2346 in a pure module with no sockets and no processes, so the entire surface
2347 tests without a daemon. **The announce is mandatory in both
2348 directions**: `endpoint none` is an explicit negative rather than
2349 silence. The spec was amended (dd6c511) to say so after the alternative
2350 was found to be unresolvable — at the reading end, "the remote has no
2351 QUIC to offer" and "ssh is being slow" are the same zero bytes, and the
2352 client blocks on one line either way.
2353
2354 `muxd endpoint` (`src/main.zig`) — `ensureForAttach`, then key
2355 resolution matching the daemon's exactly (`MUX_KEY_FILE`, then the
2356 default path, and only the default is ever created), then the announce
2357 as the **first** stdout bytes, then the byte-for-byte `muxd proxy` pump.
2358 Every unhappy path announces `none` and stays on ssh rather than
2359 panicking: a bug in a preamble must not take down a session ssh was
2360 about to carry perfectly well.
2361
2362 Client-side, the handoff sits at the **Transport** layer
2363 (`src/client.zig`), not above it, so a reconnect re-runs it rather than
2364 resurrecting a dead recipe. Cache first, ssh second. The fallback line
2365 is attach-only (`report_fallback`) so a flapping link cannot turn it
2366 into a log. `readAnnounceAbortable` has **no timeout by design** — it is
2367 bounded by ssh itself, the same trust `--via` already extends to the
2368 command it spawns — and it honors the carry contract, holding
2369 non-abort bytes typed during the wait rather than dropping them.
2370
2371 ### The deadline, pinned from measurement (Task 4, 2026-08-11)
2372
2373 **There is no fast-failure case.** A wrong PSK against a live listener
2374 ran 2037ms of a 2000ms budget, 3049 of 3000, 8031 of 8000: mutual auth
2375 means the listener does not answer a peer it cannot authenticate, so
2376 silence is all the client ever gets. A blackholed UDP port did the same
2377 (4001 of 4000, 8001 of 8000). So did an unbound port **on loopback**,
2378 2051ms of 2000 — the ICMP refusal is real, but it lands on the `sendto`
2379 inside `Client.drain`, whose `catch return` discards it, so the
2380 `ConnectionRefused` branch in `readable` never sees it. Every failure
2381 runs the budget out, and the deadline is the only thing that ends any of
2382 them.
2383
2384 That fixes the shape of the trade, and 2000 stands. Below it: a real
2385 handshake is **4ms** local, **6.9ms** cold attach on the M8 LAN, and
2386 **234ms** at 75ms RTT — 3.1× RTT, because every fresh Initial costs a
2387 Retry round trip — so 2000 covers RTT to ~645ms, past any terrestrial
2388 link. Above it: with negative caching refused, every attach on a
2389 UDP-blocked network pays this in full, so it wants to be as low as the
2390 floor allows. 1000 would halve the tax and also halve the RTT ceiling to
2391 ~320ms — buying latency on a degraded network at the price of abandoning
2392 QUIC paths that would have worked. Wrong way round.
2393
2394 ### Findings
2395
2396 - **A wedged `zig test` step prints nothing at all.** Not a truncated
2397 report, not a partial list — nothing, for the whole timeout. So the
2398 ordering rule from M13 ("a regrade catch must be legible") needs a
2399 second clause: a legible catch must also be ordered **before** any
2400 test the same regression can hang. The ownership pin is the case that
2401 taught it. Left where it was, the mutation produced a 600-second empty
2402 wedge with no output to read; moved ahead of the test that hangs, the
2403 same mutation produces 8 named failures in 14 seconds.
2404 - **The suite was running the developer's login shell.** Hermetic
2405 `XDG_*` homes cannot make a suite hermetic while it still executes
2406 arbitrary user rc files: a zsh plugin manager cloning a repository at
2407 shell startup ate a convergence window and failed a scenario that had
2408 nothing to do with it. `SHELL` is now pinned suite-wide. The general
2409 form is that "hermetic" has to include the *code* the environment
2410 causes to run, not only the paths it points at.
2411 - **A mutation only proves the earliest scenario that catches it.**
2412 First-failure exit means every later scenario is untested against that
2413 break, and a campaign that reports "five kills" is silently reporting
2414 five *first* kills. Task 7's five left scenario (e) and (c)'s
2415 self-heal arm unproven until the accounting was audited scenario by
2416 scenario; re-running against each in turn brought the real total to
2417 eight. Kill counts are claims about ordering as much as about
2418 coverage.
2419 - **Review prescriptions remain fallible in both directions, and the
2420 pipeline caught both.** A spec reviewer prescribed a "complementary"
2421 post-hoc liveness check on the reaped ssh; it was **unfalsifiable** —
2422 dead processes do not resurrect, and a reused pid presents as alive —
2423 so it could only ever pass or lie, and an independent review proved
2424 that and deleted it. The same review corrected the lead's own `set
2425 +e` alignment. M13's rule holds: an implementer rejecting a bad
2426 prescription with evidence is the process working in the direction it
2427 is easier to forget it runs.
2428 - **`waitReady` answers the detach chord *during* a dial.** That is
2429 correct behavior — a user who presses Ctrl-\ while waiting asked to
2430 stop — but it means any scenario built to spend a deadline must hold
2431 its detach bytes out past the budget, or the client aborts early and
2432 the scenario passes having tested nothing. A timing assertion whose
2433 subject can be cancelled by its own fixture is not an assertion.
2434 - **A fixed port must dodge the kernel's ephemeral range.** Read
2435 `/proc/sys/net/ipv4/ip_local_port_range` and stay out of it: a port
2436 chosen for being dead can come alive mid-suite when something
2437 unrelated is handed it, and the scenario that depended on nothing
2438 listening there then fails for a reason no one will find by reading
2439 it.
2440
2441 ### Errata
2442
2443 The commit message of bde1af5 states `kill -0`'s polarity backwards. The
2444 code and its comments are correct; only the prose in that message is
2445 wrong. Recorded here rather than fixed by rewriting history, which is
2446 the standing preference — a wrong sentence with a correction beside it
2447 is more honest than a history that never contained it.
2448
2449 ### Process note
2450
2451 Subagent pipeline as M11–M13: one implementer and two reviewers per
2452 task, quality and spec, every new assertion written mutation-first. One
2453 new shape is worth recording. A spec reviewer was blocked by the
2454 permission classifier on `make e2e`, three times, situationally rather
2455 than by rule. It did not work around the block, invent an equivalent
2456 command, or report a verdict it had not earned: it stopped, reverted its
2457 own edit, and handed the exact change to the implementer to run. That is
2458 the correct response to a tool refusal, and it is recorded because the
2459 tempting alternatives all produce a green report that means nothing.
2460
2461 ### Banked by M14
2462
2463 - **`drain()` swallows `ECONNREFUSED`** — the Task 4 incidental, and an
2464 M15 candidate. Fixing it would make unreachable ports fail in ~1 RTT
2465 instead of a full deadline. **Re-pin, do not delete:** e2e (d)'s
2466 `elapsed >= floor` bound and `client.zig`'s dead-coordinates test
2467 bound both currently lean on the quirk, and both would need re-timing
2468 in the same change.
2469 - **The transport recipe as a `union(enum)`** — from Task 6's quality
2470 review. `Transport.open` now takes four mutually-exclusive nullables,
2471 two of them sharing a type; `union(enum) { sock, via, quic, hand }`
2472 makes the invariant unstatable-wrong rather than documented. M15.
2473 - **Per-client keys, certificates and TOFU** — still parked, unchanged
2474 since M10. The handoff hands out the *same* shared key over ssh, which
2475 is exactly the trust model M10 chose and no worse; per-client
2476 credentials remain the thing that would change it.
2477 - **Negative caching of "UDP blocked"** — still refused, by design, and
2478 the LAN box's 4264ms warm-blocked attach is now the measured price of
2479 that refusal rather than an estimate of it.
2480 - **The WAN-box RTT on record disagrees with itself** — ~290ms in one
2481 place against the 15.7–16.5ms baseline at decisions.md:314. Verify
2482 against the live box before the next measurement leans on either.
2483 - **Scenario (e) has two unexercised assertions** — its absent-fallback
2484 grep and its no-budget bound were never driven red. Cheap candidates
2485 for the next regrade, and cheaper than the audit that found them.
2486
2206 ## Open (owed by later milestones) 2487 ## Open (owed by later milestones)
2207 2488
2208 - Scrollback retention *tuning*. The policy itself was decided in M1 and 2489 - Scrollback retention *tuning*. The policy itself was decided in M1 and
docs/roadmap.md
Old New
@@ -5,7 +5,7 @@ The forward view, one item per line, ranked. History and evidence live in
5 this file at each milestone close and whenever the queue reorders; the 5 this file at each milestone close and whenever the queue reorders; the
6 queue's order is set by the user, not by this file. 6 queue's order is set by the user, not by this file.
7 7
8 **Now:** M1–M13 complete; `v0.0.1-2` published as a Linux tarball; in 8 **Now:** M1–M14 complete; `v0.0.1-2` published as a Linux tarball; in
9 field trial on real VMs, and the trial is now producing the queue. 9 field trial on real VMs, and the trial is now producing the queue.
10 Trial feedback outranks everything below — what actually hurts in use is 10 Trial feedback outranks everything below — what actually hurts in use is
11 better data than any of this ranking, and the proof is that the three 11 better data than any of this ranking, and the proof is that the three
@@ -117,18 +117,45 @@ truncation is reserved for `muxd start`; and three review prescriptions
117 were rejected with evidence, which is the adversarial flow running in 117 were rejected with evidence, which is the adversarial flow running in
118 the direction it is easier to forget. 118 the direction it is easier to forget.
119 119
120 ## M14 candidates — and still outranked by trial feedback 120 ## M14 — ssh→QUIC handoff — complete
121 121
122 The trial-friction bundle that used to head this section shipped as M13. 122 **Verdict: cleared, both legs.** `mux HOST` no longer chooses a
123 What remains is what was already ranked behind it, plus two items M13 123 transport; it earns one. An ssh to `muxd endpoint` fetches the daemon's
124 created. Nothing here is a field finding; the next one that arrives 124 QUIC port and key as a mandatory one-line announce, the session moves
125 outranks all of it. 125 onto QUIC and the coordination ssh is reaped, and the coordinates are
126 126 cached so every later attach dials direct with no ssh process at all.
127 - **ssh→QUIC handoff**: `muxd endpoint` prints port+key over ssh; 127 Leg 1 held at **20 scenario checkpoints over 33 convergence points**,
128 client caches both, tries QUIC first under a ~2s deadline, and the 128 both pinned as literals, with `SOAK_N=10 make soak` at **10/10 on the
129 coordination ssh stays alive as the fallback proxy. Full sketch — 129 code that ships** (d9b17d5). Leg 2 is three for three, each catch
130 including the key-outlives-the-daemon model and the stale-cache 130 legible at the check that predicted it. The kill criterion held on the
131 invariant — in the M10 spec's context section. 131 real LAN box with the one allowed manual step: cold attach **244ms**
132 with the ssh child observed dead while the session went on answering,
133 warm attach **13ms** with zero ssh spawned, and a blackholed UDP port
134 still landing the session over ssh, one deadline and one line later
135 (decisions.md, M14, which carries the numbers and the regrade table).
136 En route: the announce had to become **mandatory in both directions**
137 because silence and a slow ssh are indistinguishable at the reading end
138 — `endpoint none` is now an explicit negative — and the deadline was
139 pinned from measurement showing there is **no fast-failure case** at
140 all, not even a refused loopback port.
141
142 ## M15 candidates — and still outranked by trial feedback
143
144 What remains is what was already ranked behind M13 and M14, plus two
145 items M14 created. Nothing here is a field finding; the next one that
146 arrives outranks all of it.
147
148 - **`drain()` swallows `ECONNREFUSED`** — created by M14 and the
149 cheapest win on the list. On a connected UDP socket the queued ICMP
150 refusal is consumed by the `sendto` in `Client.drain`, whose `catch
151 return` discards it, so the `ConnectionRefused` branch never fires and
152 an unreachable port costs the full 2s deadline instead of ~1 RTT.
153 Fixing it re-times two existing bounds rather than deleting them
154 (decisions.md, M14).
155 - **The transport recipe as a `union(enum)`** — also from M14.
156 `Transport.open` takes four mutually-exclusive nullables, two of them
157 sharing a type; `union(enum) { sock, via, quic, hand }` would make the
158 invariant unstatable-wrong instead of merely documented.
132 - **First-backoff tuning**: a reconnect after a tear can pay a 200ms–2s 159 - **First-backoff tuning**: a reconnect after a tear can pay a 200ms–2s
133 backoff step; measured ~4x win available on fast links, ~2x on slow 160 backoff step; measured ~4x win available on fast links, ~2x on slow
134 (decisions.md, M7). 161 (decisions.md, M7).
test/e2e.sh
Old New
@@ -2581,9 +2581,9 @@ HT0=$(date +%s%N)
2581 SHELL=/bin/sh XDG_RUNTIME_DIR="$HRUN2" PATH="$HPATH" timeout 40 \ 2581 SHELL=/bin/sh XDG_RUNTIME_DIR="$HRUN2" PATH="$HPATH" timeout 40 \
2582 "$MUX" "127.0.0.1" > "$OUT.h4" 2> "$OUT.h4.err" & 2582 "$MUX" "127.0.0.1" > "$OUT.h4" 2> "$OUT.h4.err" &
2583 H4PID=$! 2583 H4PID=$!
2584 # Timed to the LINE rather than to the client's exit: the line is printed 2584 # Timed to the LINE rather than to the client's exit, so no session sleeps
2585 # the moment the budget runs out, so the elapsed time here IS the deadline 2585 # are folded in. The elapsed time is the dial's budget plus launch overhead
2586 # the dial spent, with no session sleeps folded into it. 2586 # (measured 26-129ms), which only inflates it — the floor is safe.
2587 # Only the stderr is printed on failure, deliberately: the complaint is 2587 # Only the stderr is printed on failure, deliberately: the complaint is
2588 # about a line that is missing from it, and the capture beside it is a 2588 # about a line that is missing from it, and the capture beside it is a
2589 # screenful of escape sequences that would bury the answer. 2589 # screenful of escape sequences that would bury the answer.