fa0eb22a
docs: M10 record and roadmap close — cleared on the real box
a73x 2026-08-09 15:48
Commit message
docs/decisions.md
| Old | New | ||
|---|---|---|---|
| @@ -1369,6 +1369,131 @@ Engine, over a socket. The full render-vs-dump harness stays banked. | |||
| 1369 | It still fails structurally on fast links; the form that fixes it is now | 1369 | It still fails structurally on fast links; the form that fixes it is now |
| 1370 | standard and written down. | 1370 | standard and written down. |
| 1371 | 1371 | ||
| 1372 | ## 2026-08-09 (M10 — QUIC ergonomics) | ||
| 1373 | |||
| 1374 | **Verdict: cleared, on the real box.** The whole QUIC story is four | ||
| 1375 | commands — `muxd keygen`; one ssh line placing the key; `ssh HOST 'muxd | ||
| 1376 | start --quic 0.0.0.0'`; `mux quic://HOST` — with no port or key path | ||
| 1377 | typed anywhere and the trust posture unchanged (32-byte PSK, permissive | ||
| 1378 | files refused, no unauthenticated mode). Kill criterion held on the LAN | ||
| 1379 | box from a clean slate: the commands as written landed a session on the | ||
| 1380 | first attach; a marker survived full ssh logout (the spawning session | ||
| 1381 | died while daemon pid 8829 lived on — noting the box had `Linger=yes`, | ||
| 1382 | which governs `systemd --user` and not a setsid daemon, so | ||
| 1383 | `KillUserProcesses` at its default `no` is the operative mechanism and | ||
| 1384 | the one the README's survival sentence claims); ten attach/detach | ||
| 1385 | cycles never | ||
| 1386 | started a second daemon (enumeration matched the tracked up-line pid | ||
| 1387 | exactly); the `start` rerun no-op'd with exit 0; `dump` carried the | ||
| 1388 | marker. Spec: superpowers/specs/2026-08-09-m10-quic-ergonomics-design.md | ||
| 1389 | (re-cut mid-design from "daemon auto-start" when the user pointed at the | ||
| 1390 | lower-hanging fruit; the auto-start and ssh→QUIC-handoff stages are | ||
| 1391 | banked there with full sketches). | ||
| 1392 | |||
| 1393 | Shipped: `muxd keygen` (0600 key in a 0700 dir, refusal is atomic at the | ||
| 1394 | syscall and leaves bytes untouched); default key path | ||
| 1395 | `$XDG_CONFIG_HOME/mux/key` with resolution `--key` > `MUX_KEY_FILE` > | ||
| 1396 | default-if-present on BOTH binaries (`pickKey` pure and unit-pinned, its | ||
| 1397 | call site pinned separately by e2e — see the third gate below; the | ||
| 1398 | daemon reading the env var is new); default port 4433 both parsers, | ||
| 1399 | single constant in quic_server.zig; `muxd start` (probe → fork/setsid → | ||
| 1400 | poll; already-running is a no-op exit 0; failure names the log; | ||
| 1401 | `~/.local/state/mux/muxd.log` truncated per spawn); honest `--via` | ||
| 1402 | failure (`lostMsg`: new wording iff `via != null` and no frame ever | ||
| 1403 | arrived — for any other transport it would be a different lie); | ||
| 1404 | `--version` on both binaries from one build.zig constant; systemd fully | ||
| 1405 | deleted (contrib/ units, `LISTEN_FDS` socket activation — verified | ||
| 1406 | working same-day before removal, this commit range is the resurrection | ||
| 1407 | reference — and `owns_sock_file`, whose dev/ino teardown check stays and | ||
| 1408 | is still mutation-covered both ways). | ||
| 1409 | |||
| 1410 | **Defects found and fixed en route:** | ||
| 1411 | - **`muxd start` panicked (exit 134) whenever its child died young** — | ||
| 1412 | the poll loop's second `waitpid(NOHANG)` after its own reap gets | ||
| 1413 | ECHILD, which the stdlib maps to `unreachable`. Fired on exactly the | ||
| 1414 | first-run mistake the failure line exists for (`--quic` before the key | ||
| 1415 | was placed); neither test layer covered a dying child (the unit stub | ||
| 1416 | slept forever, every e2e start succeeded). Fixed 9bd43fc: a pid owes | ||
| 1417 | exactly one reap; pinned at both layers by a stub that exits | ||
| 1418 | immediately. | ||
| 1419 | - **Three plan mutation gates could not fail.** The port tests asserted | ||
| 1420 | `expectEqual(quic.default_port, …)` — the constant against itself, a | ||
| 1421 | tautology that stayed green under mutation; the race scenario typed | ||
| 1422 | its marker *after* the race through the same socket path, so a stolen | ||
| 1423 | socket answered indistinguishably; and the key-resolution order, | ||
| 1424 | swapped at its CALL SITE (`envKey() orelse o.key` in `run()`), | ||
| 1425 | passed both layers while a live daemon authenticated with the wrong | ||
| 1426 | key — until 144f6af the daemon's env path had zero automated coverage | ||
| 1427 | anywhere. Two rules now standing. First: **assert the literal, never | ||
| 1428 | the constant the code under test reads** — the shape is a defect | ||
| 1429 | whenever the constant is a contract with something outside the | ||
| 1430 | process (a second binary, a user), fine when private. Second, and the | ||
| 1431 | likelier to recur in a codebase that rightly extracts for | ||
| 1432 | testability: **a pure function's unit test pins the function, not its | ||
| 1433 | call site** — extraction can move the risk into the argument list | ||
| 1434 | rather than removing it (`pickKey`'s three parameters are all | ||
| 1435 | `?[]const u8`, so a swap compiles silently), and the extraction needs | ||
| 1436 | its own end-to-end pin where it is called. Replaced with literal | ||
| 1437 | pins, a head-on stolen-socket e2e, and the call-site key scenario. | ||
| 1438 | - **Third instance of cross-binary drift**: two independent 15_000 | ||
| 1439 | idle-ms constants, each self-asserted only. Single-sourced beside | ||
| 1440 | `default_port` and pinned by literal (abae058). | ||
| 1441 | - **Two silent-side-effect test bugs**: the keygen refusal test re-read | ||
| 1442 | through a kept fd (follows the inode — an unlink-and-replace of the | ||
| 1443 | credential passed); and the spawn test truncated the developer's real | ||
| 1444 | `muxd.log` (post-`muxd start`, that is a live daemon's log NUL-holed | ||
| 1445 | by `make test`). Both proven by control before and after fixing | ||
| 1446 | (ce7a921, 35ae0b2). | ||
| 1447 | - **`std.posix.exit` in a fork child under link_libc is exit(3)** — | ||
| 1448 | atexit handlers plus the *parent's* inherited stdio buffers flushed | ||
| 1449 | twice. `exit_group` is the only safe child exit (7b4208f). | ||
| 1450 | - **M9 burst flake, 1-in-3 under load (review runs at 715f71b)**: | ||
| 1451 | `confirmed 5` was exact-equality on a timing-dependent counter; the | ||
| 1452 | M9 WAN record itself shows the counter legitimately short (churn | ||
| 1453 | flush, `abandoned=1`). Now `>= 4` via a separate `want_stat_ge` | ||
| 1454 | helper — separate so weakening a *correctness* counter (contradicted, | ||
| 1455 | expired: still exact) never becomes a habit. Four consecutive green | ||
| 1456 | suites after (implementer's Task 10 runs at d587aa2), plus the | ||
| 1457 | reviewer's two at the same commit. | ||
| 1458 | |||
| 1459 | **Accepted, with rationale in place:** the e2e start scenarios leak a | ||
| 1460 | daemon if they fail inside the two assertions between spawn and pid | ||
| 1461 | capture (closing it would mean parsing the pid before asserting the | ||
| 1462 | lines that prove it is there); `muxd start --version` is an unknown | ||
| 1463 | argument (`--version` is recognized at argv[1] only — a command, not a | ||
| 1464 | flag, and with `keygen`/`start` present that stays a rule, not an | ||
| 1465 | exception). | ||
| 1466 | |||
| 1467 | **Banked by M10:** attach auto-start (`muxd proxy`/local `mux` call the | ||
| 1468 | spawn helper; unix-socket only; silence stays meaningful) and the | ||
| 1469 | ssh→QUIC handoff (`muxd endpoint` prints port+key over ssh; client-side | ||
| 1470 | cache with a ~2s attach deadline; the coordination ssh stays alive as | ||
| 1471 | the fallback proxy; key belongs to the host-user on disk, outliving | ||
| 1472 | daemons — restarts invalidate only cached ports; rotation = delete + | ||
| 1473 | restart; measure the wrong-key failure mode before trusting any | ||
| 1474 | deadline number) — both fully sketched in the spec. Also banked: an | ||
| 1475 | absolute-plus-relative bound pattern where a guard's threshold is | ||
| 1476 | derived from a path it does not bound (the `refuse` helper carries the | ||
| 1477 | worked example); a decision on `quic://[NAME]` — the portless-bracket | ||
| 1478 | branch now DNS-resolves any bracketed string where it was refused | ||
| 1479 | before M10, a behavior that fell out of the IPv6 handling rather than | ||
| 1480 | being chosen; and a Zig plan-writing trap that bit twice: an inferred | ||
| 1481 | error set silently loses a member when its only `return error.X` is | ||
| 1482 | removed (breaking distant switch arms), and an error *set* return like | ||
| 1483 | `execveZ`'s cannot be `_ =` discarded. | ||
| 1484 | |||
| 1485 | Provenance worth keeping honest: the 0700 key directory (b405645) and | ||
| 1486 | the cross-binary version-drift e2e pin (a33238a) were review findings | ||
| 1487 | that amended the spec mid-milestone, not design foresight. | ||
| 1488 | |||
| 1489 | Process note for the record: subagent pipeline as in M9 (implementer + | ||
| 1490 | reviewer in isolated worktrees, mutations written first). The reviewer | ||
| 1491 | re-fired mutations independently every round and found the waitpid HIGH | ||
| 1492 | the suite was structurally blind to; the implementer twice refused to | ||
| 1493 | tick a mutation checkbox that could not fire and said so instead of | ||
| 1494 | proceeding. One review round was NOT-APPROVED; everything else landed | ||
| 1495 | without rework cycles. | ||
| 1496 | |||
| 1372 | ## Open (owed by later milestones) | 1497 | ## Open (owed by later milestones) |
| 1373 | 1498 | ||
| 1374 | - Scrollback retention *tuning*. The policy itself was decided in M1 and | 1499 | - Scrollback retention *tuning*. The policy itself was decided in M1 and |
docs/roadmap.md
| Old | New | ||
|---|---|---|---|
| @@ -5,9 +5,9 @@ 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:** `v0.0.1-1` tagged; M1–M9 complete; in field trial on real VMs. | 8 | **Now:** M1–M10 complete; `v0.0.1-2` published as a Linux tarball; in |
| 9 | Trial feedback outranks everything below — what actually hurts in use is | 9 | field trial on real VMs. Trial feedback outranks everything below — |
| 10 | better data than any of this ranking. | 10 | what actually hurts in use is better data than any of this ranking. |
| 11 | 11 | ||
| 12 | ## M9 — prediction (speculative local echo) — complete | 12 | ## M9 — prediction (speculative local echo) — complete |
| 13 | 13 | ||
| @@ -31,11 +31,33 @@ failure found ngtcp2 being re-entered on **every** QUIC exchange — | |||
| 31 | constant, with a rare consequence — which is fixed and, unusually, was | 31 | constant, with a rare consequence — which is fixed and, unusually, was |
| 32 | proven so by a mutation that fired immediately. | 32 | proven so by a mutation that fired immediately. |
| 33 | 33 | ||
| 34 | ## M10 — QUIC ergonomics — complete | ||
| 35 | |||
| 36 | **Verdict: cleared on the real box.** The whole QUIC story is four | ||
| 37 | commands — `muxd keygen`, one ssh line placing the key, `ssh HOST 'muxd | ||
| 38 | start --quic 0.0.0.0'`, `mux quic://HOST` — no port or key path typed | ||
| 39 | anywhere, trust posture unchanged. Kill criterion held on the LAN box: | ||
| 40 | first-attach success from a clean slate, a marker surviving full ssh | ||
| 41 | logout, ten attach/detach cycles with exactly one daemon counted by | ||
| 42 | tracked pid (decisions.md, M10). systemd is deleted, not deferred: the | ||
| 43 | contrib/ units and `LISTEN_FDS` activation are gone, with the removal | ||
| 44 | commit as the resurrection reference. En route: a panic on `muxd | ||
| 45 | start`'s own first-run-mistake path, two plan mutation gates that could | ||
| 46 | not fail, and a third cross-binary constant drift — all fixed, all with | ||
| 47 | the rule recorded ("assert the literal, never the constant the code | ||
| 48 | under test reads"). | ||
| 49 | |||
| 34 | ## Trial friction, likely next — and still outranked by trial feedback | 50 | ## Trial friction, likely next — and still outranked by trial feedback |
| 35 | 51 | ||
| 36 | - **Daemon lifecycle**: attach does not auto-start `muxd run`; no install | 52 | - **Attach auto-start**: `muxd proxy` and local `mux` call the spawn |
| 37 | script; systemd user units exist in `contrib/` but are not the deployed | 53 | helper `muxd start` already uses, so `mux user@host` works on a box |
| 38 | recipe. First annoyance any trial hits. | 54 | where nothing is running. Sketched in the M10 spec; the helper is |
| 55 | built and shipping. | ||
| 56 | - **ssh→QUIC handoff**: `muxd endpoint` prints port+key over ssh; | ||
| 57 | client caches both, tries QUIC first under a ~2s deadline, and the | ||
| 58 | coordination ssh stays alive as the fallback proxy. Full sketch — | ||
| 59 | including the key-outlives-the-daemon model and the stale-cache | ||
| 60 | invariant — in the M10 spec's context section. | ||
| 39 | - **First-backoff tuning**: a reconnect after a tear can pay a 200ms–2s | 61 | - **First-backoff tuning**: a reconnect after a tear can pay a 200ms–2s |
| 40 | backoff step; measured ~4x win available on fast links, ~2x on slow | 62 | backoff step; measured ~4x win available on fast links, ~2x on slow |
| 41 | (decisions.md, M7). | 63 | (decisions.md, M7). |