971a17e9
docs: M18 decisions, field verification, and what the version string cannot tell you
a73x 2026-08-15 12:22
Commit message
docs/decisions.md
| Old | New | ||
|---|---|---|---|
| @@ -3351,3 +3351,151 @@ was fixed the same day, each fix proven by a test that fails without it. | |||
| 3351 | back is the one the up-line named, so every dial targets what was | 3351 | back is the one the up-line named, so every dial targets what was |
| 3352 | bound. Two suites congruent mod 900, or a tuned ephemeral range, no | 3352 | bound. Two suites congruent mod 900, or a tuned ephemeral range, no |
| 3353 | longer collide. | 3353 | longer collide. |
| 3354 | |||
| 3355 | ## 2026-08-15 (M18 — the multi-session daemon) | ||
| 3356 | |||
| 3357 | One daemon, N sessions, one socket path / QUIC port / key. The design was | ||
| 3358 | locked in conversation on 2026-08-13 and executed as eight tasks; what | ||
| 3359 | follows is the locked set, then the places execution disagreed with it. | ||
| 3360 | |||
| 3361 | - **A connection IS a session.** The attach payload grew an optional | ||
| 3362 | UTF-8 name tail after its fixed 20 bytes, and *nothing after attach | ||
| 3363 | changed*: no frame tagging, no per-frame session id, no QUIC stream | ||
| 3364 | surgery. Same-host tiles are separate connections to the same port. | ||
| 3365 | This is the decision every other one falls out of, and it is why the | ||
| 3366 | diff is a name tail plus a `Session` struct rather than a protocol. | ||
| 3367 | - **Empty name means the default session, and that is the compat | ||
| 3368 | story.** `encodeAttachNamed("")` writes the same twenty bytes | ||
| 3369 | `encodeAttach` always did, so an M17 client works against an M18 | ||
| 3370 | daemon and an M18 client with no `--session` works against an older | ||
| 3371 | one. `status_req`, `await_req` and `debug_dump` took the same tail for | ||
| 3372 | the same reason — one rule across every session-scoped verb. | ||
| 3373 | - **Known limitation, documented rather than fixed — and it is two | ||
| 3374 | different failures, not one.** The plan predicted a single symptom | ||
| 3375 | ("silently ignored by `decodeAttach catch return`, the client hangs"); | ||
| 3376 | the cross-version run measured both arms and they differ by transport, | ||
| 3377 | because a pre-M18 daemon has two attach paths: | ||
| 3378 | - **Unix socket, ssh and `--via`** reach `serviceObserver`, which | ||
| 3379 | DROPS the connection on an unparseable attach. The client says `mux: | ||
| 3380 | connection to muxd lost` and exits 1. A clean, bounded failure. | ||
| 3381 | - **QUIC** reaches `handleFrame`, whose `decodeAttach catch return` | ||
| 3382 | ignores the frame in silence — and there the client really does | ||
| 3383 | hang, measured as an 8s timeout with no output. A plain `quic://` | ||
| 3384 | attach from the same M18 client to the same old daemon works, so it | ||
| 3385 | is the NAME that is fatal, not the transport. | ||
| 3386 | |||
| 3387 | This is the worse half landing on the deployment that most needs it | ||
| 3388 | right: remote boxes are exactly the QUIC ones, so upgrading the client | ||
| 3389 | or the wall ahead of the box turns `--session` into a hang with | ||
| 3390 | nothing printed. Still not worth a capability dance for a flag that | ||
| 3391 | did not exist when that daemon shipped, but a bounded first-snapshot | ||
| 3392 | wait on a NAMED attach — "no answer; that daemon may predate | ||
| 3393 | `--session`" — is a cheap mitigation and is banked as a candidate. | ||
| 3394 | - **Purpose bounds the surface.** The wall showing one host twice is the | ||
| 3395 | whole point, so there is no list verb (the wall names its sessions; | ||
| 3396 | attach-or-create makes discovery unnecessary), no kill verb (a session | ||
| 3397 | ends when its shell exits), no rename, no per-session shell or | ||
| 3398 | scrollback config. `max_sessions = 4`, wall-sized, in the spirit of | ||
| 3399 | `max_clients = 8`. | ||
| 3400 | - **A bare attach joins the zeroth session and never creates one — | ||
| 3401 | seamless handoff is the reason.** The default session is spawned in | ||
| 3402 | `init` (`createSession(..., proto.default_session, ...)`), so `"0"` | ||
| 3403 | exists before any client connects and an empty name always *finds* it. | ||
| 3404 | The alternative — tmux's `new-session` semantics, where an unnamed | ||
| 3405 | invocation makes a fresh one — would break the product's central | ||
| 3406 | trick: the M14 handoff attaches over ssh, re-dials, and re-attaches | ||
| 3407 | over QUIC, and every reconnect and resume path in `client.zig` | ||
| 3408 | re-attaches the same way. All of those carry an empty name, so all of | ||
| 3409 | them must land in the SAME shell. Under create-on-bare-attach a | ||
| 3410 | handoff would be a new login and a network blip would spawn a shell. | ||
| 3411 | The tmux analogy holds once mapped correctly: mux has no separate | ||
| 3412 | "new" verb — `mux` IS the attach verb — so its bare form is `tmux | ||
| 3413 | attach`, not `tmux new`. Naming a session that does not exist is the | ||
| 3414 | explicit request, and that is the arm that creates. | ||
| 3415 | - **An attach is answered, always — there is no wait state.** Both arms | ||
| 3416 | refuse the same way `resolveSession` returns null: `exit_status 1`, | ||
| 3417 | queued on the client path and written-then-dropped on the observer | ||
| 3418 | path. Resolution is synchronous — map `""`→`"0"`, validate, find, else | ||
| 3419 | create, else no. This is worth stating because the pre-M18 hang | ||
| 3420 | documented above looks like a wait and is not one: that daemon's | ||
| 3421 | `decodeAttach catch return` fails to PARSE a named attach and returns | ||
| 3422 | before any resolve-or-refuse logic runs, so it never decides anything. | ||
| 3423 | Silence is not a state this protocol has; it is what a peer that | ||
| 3424 | predates the rule does. | ||
| 3425 | - **Creation requires a real size.** Attach-or-create only creates when | ||
| 3426 | the attach carries nonzero cols×rows. muxa attaches at 0×0 — it makes | ||
| 3427 | no size claim, deliberately — so `muxa send` to a dead session is | ||
| 3428 | refused with `exit_status 1` instead of silently spawning a 0×0 shell. | ||
| 3429 | - **A session name means one session, whichever transport carried the | ||
| 3430 | attach.** The session table belongs to the daemon, not to the listener | ||
| 3431 | that accepted the connection, so `--session a` over QUIC and | ||
| 3432 | `--session a` over the unix socket join the *same* shell — and two | ||
| 3433 | QUIC dials to one port come out as two shells. This is the operator's | ||
| 3434 | half of decision 6 and it is now pinned end to end: the QUIC block | ||
| 3435 | drives two dials into two sessions, then joins one of them from the | ||
| 3436 | socket and asserts both clients' markers on one grid with the session | ||
| 3437 | count unmoved. QUIC is where the claim is least obvious, since a QUIC | ||
| 3438 | client is promoted to a slot when its *handshake* completes — before | ||
| 3439 | any attach — so its slot starts session-less and the name is the only | ||
| 3440 | thing that ever binds it. | ||
| 3441 | - **Command tracking is per-session**, forced by the agent-surface | ||
| 3442 | merge. A tracker fed by two shells would interleave their command | ||
| 3443 | lifecycles into nonsense, so `cmd` and `last_return` live on the | ||
| 3444 | session; `AwaitState` stays on the client slot, because an await is | ||
| 3445 | one client's question. | ||
| 3446 | |||
| 3447 | **Amended in execution — reality disagreeing with the plan.** | ||
| 3448 | |||
| 3449 | - **A `/bin/sh` session has no marks, so the planned `muxa status` phase | ||
| 3450 | assertion could not hold.** The plan's e2e sketch asserted that | ||
| 3451 | `status --session b` names a running command while `--session a` | ||
| 3452 | reports `at_prompt`. `shellint` detects shells by basename and unknown | ||
| 3453 | shells get nothing, so `cmd.phase` in a `/bin/sh` session never leaves | ||
| 3454 | `at_prompt` and the assertion would have been asserting a constant | ||
| 3455 | against itself. Switching the block to bash was rejected for the | ||
| 3456 | reason `e2e.sh` already pins `SHELL=/bin/sh` at the top: `bash_init` | ||
| 3457 | sources `$HOME/.bashrc`, which is arbitrary code on the session under | ||
| 3458 | test. What the e2e asserts instead is what it can honestly observe — | ||
| 3459 | that `status --session` returns a real StatusReply for a named session | ||
| 3460 | and that `capture --session` returns one session's grid and not its | ||
| 3461 | neighbour's. The per-session tracker itself stays pinned where marks | ||
| 3462 | actually exist: `server.zig`'s unit tests, and `agent.sh`'s bash | ||
| 3463 | daemons. | ||
| 3464 | - **`muxa` is now an e2e.sh argument (`$9`).** The plan assumed the | ||
| 3465 | suite could reach it; it could not — muxa went only to `agent.sh`. It | ||
| 3466 | is asked for here rather than there because the verbs under test | ||
| 3467 | address a session *without attaching*, and the sessions they address | ||
| 3468 | can only be CREATED by `mux`, which `agent.sh` has no copy of. The | ||
| 3469 | soak step takes the same argument, since soak *is* the e2e suite run N | ||
| 3470 | times — an argument added to one and not the other makes every soak | ||
| 3471 | run abort on an unbound variable before its first scenario, which is | ||
| 3472 | exactly how it was caught. | ||
| 3473 | - **The scenario detaches before asserting.** The plan's sketch held | ||
| 3474 | both clients open across the assertions; the block as written has each | ||
| 3475 | client plant its marker and leave. A session ends when its shell | ||
| 3476 | exits, not when its last client leaves, so every routing assertion now | ||
| 3477 | runs against sessions with nobody attached — which is also what makes | ||
| 3478 | the answers impossible to explain by a live client holding something | ||
| 3479 | open. | ||
| 3480 | |||
| 3481 | **The one real defect, and the e2e found it rather than review** (see | ||
| 3482 | `fix(pty)`, f25f62c). `forkpty` returns the master without CLOEXEC, so | ||
| 3483 | every session spawned later inherited every earlier session's master — | ||
| 3484 | observed on a three-session daemon as fds 0,1,2,255 in the first shell, | ||
| 3485 | those plus fd 3 in the second, plus fd 3 AND fd 6 in the third. A master | ||
| 3486 | with a second holder never sees its last close, so `Pty.deinit`'s close | ||
| 3487 | stopped hanging up the far side; the shell is interactive and therefore | ||
| 3488 | ignores the SIGTERM that follows; and the blocking `waitpid` after that | ||
| 3489 | never returned. `muxd stop` unlinked the socket and printed `muxd: | ||
| 3490 | stopped` while the daemon sat in `do_wait` forever with every session's | ||
| 3491 | shell alive — and because deinit walks sessions in slot order, wedging on | ||
| 3492 | the first meant none of the others were torn down either. | ||
| 3493 | |||
| 3494 | Two things worth keeping from it. **A fd leak is a shutdown bug, not a | ||
| 3495 | tidiness bug** — the daemon's own sockets were already CLOEXEC, and the | ||
| 3496 | pty master was the single oversight in an otherwise consistent | ||
| 3497 | convention. And **the bug was strictly unreachable at N=1**: nothing in | ||
| 3498 | the unit layer, the review rounds, or fifteen prior milestones could have | ||
| 3499 | surfaced it, because none of them ever spawned a second pty from a | ||
| 3500 | process that already held one. The first test that ran two sessions on | ||
| 3501 | one daemon found it on its first execution. | ||
docs/roadmap.md
| Old | New | ||
|---|---|---|---|
| @@ -283,6 +283,112 @@ behaviour is pinned through the dead-daemon paths, an old binary is not). | |||
| 283 | And `muxa capture` is **the one verb with no automated coverage at all** — | 283 | And `muxa capture` is **the one verb with no automated coverage at all** — |
| 284 | `test/agent.sh` drives run, await, send and status; capture is where | 284 | `test/agent.sh` drives run, await, send and status; capture is where |
| 285 | `--diff-since` would land, so the test and the flag are owed together. | 285 | `--diff-since` would land, so the test and the flag are owed together. |
| 286 | *(Closed in M18: the multi-session e2e block drives `capture --session` | ||
| 287 | and asserts it returns one session's grid and not its neighbour's. | ||
| 288 | `--diff-since` is still owed.)* | ||
| 289 | |||
| 290 | ## M18 — multi-session daemon — complete | ||
| 291 | |||
| 292 | Shipped 2026-08-15 (e7285d9..2b1c9bb + close-out). One daemon now holds | ||
| 293 | up to **four** independent sessions — engine, pty, tracker, epoch, | ||
| 294 | command tracker and name each — over **one socket path, one QUIC port, | ||
| 295 | one key**. The wall can finally show the same host twice, which is what | ||
| 296 | the milestone was for. | ||
| 297 | |||
| 298 | **A connection is a session.** The attach payload grew an optional UTF-8 | ||
| 299 | name tail after its fixed 20 bytes and nothing after attach changed: no | ||
| 300 | frame tagging, no QUIC stream surgery, the transport still one bidi | ||
| 301 | stream per connection. Same-host tiles are separate connections to the | ||
| 302 | same port. Attach-or-create is tmux-style; a session ends when its shell | ||
| 303 | exits and the daemon exits with the last one. `status_req`, `await_req` | ||
| 304 | and `debug_dump` grew the same tail, which is what lets `muxa` and `muxd | ||
| 305 | dump` address one session of several without attaching to it. | ||
| 306 | |||
| 307 | **Field-verified 2026-08-15 on both real boxes.** They had been on | ||
| 308 | **0.0.1-3** — behind the agent surface as well as M18 — and a site power | ||
| 309 | cut had taken them down, which made the upgrade window a clean one: no | ||
| 310 | daemon running on either, so nothing to tear down and no session to lose. | ||
| 311 | Static musl `muxd` (stripped, 23MB → 8.6MB) installed to | ||
| 312 | `/usr/local/bin`, identical sha256 on both boxes and on the build host, | ||
| 313 | then `muxd start --quic` on each. On the LAN box over real QUIC, and on | ||
| 314 | the WAN box over QUIC through the LAN-side UDP forward (the gate carries | ||
| 315 | TCP only): two dials, `--session a` and `--session b`, produced | ||
| 316 | `sessions=3 session 0 / session a / session b` on **both** boxes, with | ||
| 317 | each session holding its own marker and neither holding its neighbour's, | ||
| 318 | and the default session holding neither. The milestone's central claim, | ||
| 319 | on real hardware over a real link. | ||
| 320 | |||
| 321 | Two deployment notes worth keeping. `muxd --version` still prints | ||
| 322 | **0.0.1-5** after the upgrade — M18 is untagged, so the version string | ||
| 323 | cannot tell you whether a box has it; `muxd stats | grep sessions=` is | ||
| 324 | the check that can, which is the same discriminator the banked | ||
| 325 | capability probe uses. And the trial PSK survived the power cycle | ||
| 326 | identical on both boxes (`ba046148…`), so the M17 key-drift failure did | ||
| 327 | not repeat — fingerprint before blaming the network stays the rule. | ||
| 328 | |||
| 329 | **One name, one session, either door.** The session table is the | ||
| 330 | daemon's, not the accepting listener's, so `--session a` reaches the same | ||
| 331 | shell over QUIC as over the unix socket, and two QUIC dials to one port | ||
| 332 | are two shells. Pinned end to end by the QUIC block: two dials into two | ||
| 333 | sessions, then a socket client joins one of them and both clients' | ||
| 334 | markers land on one grid with the session count unmoved. That also makes | ||
| 335 | a mixed-transport wall of a single host work — `muxweb HOST#a | ||
| 336 | quic://HOST:8787#b` is tile 0 over ssh and tile 1 over QUIC against one | ||
| 337 | daemon. (`muxd run --sock PATH --quic ADDR` binds both; ssh needs no | ||
| 338 | listener of its own, since `muxd proxy` pumps into the same socket.) | ||
| 339 | |||
| 340 | **The compat story held**, measured against main @ 5d41e10 (the branch | ||
| 341 | point) in both directions. An empty name is the default session and | ||
| 342 | encodes to byte-identical pre-M18 payloads: an M18 client drove a | ||
| 343 | pre-M18 daemon's default session; a pre-M18 client drove an M18 | ||
| 344 | daemon's and created nothing (`sessions=1`, `session 0`); and a pre-M18 | ||
| 345 | `muxa`'s empty `status_req` still answered. The known limitation is | ||
| 346 | documented rather than fixed, and turns out to be **two** failures | ||
| 347 | split by transport — socket/ssh get a clean exit 1 (the old | ||
| 348 | `serviceObserver` drops the connection), **QUIC hangs** (the old | ||
| 349 | `handleFrame` ignores the frame in silence). Remote boxes are the QUIC | ||
| 350 | ones, so a client upgraded ahead of its box hangs with nothing printed; | ||
| 351 | a bounded first-snapshot wait on a named attach is banked as the cheap | ||
| 352 | mitigation. See decisions.md. | ||
| 353 | |||
| 354 | **One real defect, found by the new e2e and not by review** (f25f62c). | ||
| 355 | `forkpty` returns the master without CLOEXEC, so every session spawned | ||
| 356 | later inherited every earlier session's master — one spare `/dev/ptmx` | ||
| 357 | per predecessor, riding through exec into a shell that had no idea it | ||
| 358 | held it. A master with a second holder never sees its last close, so | ||
| 359 | `Pty.deinit`'s close stopped hanging up, the interactive shell ignored | ||
| 360 | the SIGTERM that followed, and the blocking `waitpid` never returned: | ||
| 361 | `muxd stop` unlinked the socket, printed `muxd: stopped`, and left the | ||
| 362 | daemon in `do_wait` forever with every session's shell alive. Strictly | ||
| 363 | unreachable at one session per daemon. The pin closes a master while a | ||
| 364 | later session is live and polls for the hangup with a deadline — the bug | ||
| 365 | is a hang, and a hung `zig build test` step prints nothing at all. | ||
| 366 | |||
| 367 | **The cross-version check stopped being hand-rolled** (`test/xversion.sh`, | ||
| 368 | `make xversion`). This tree's client against a previous version's daemon | ||
| 369 | and the reverse, each daemon in a container of its own, both sides built | ||
| 370 | static musl so one artifact runs in the container and on the host. Eight | ||
| 371 | legs: both directions on the default session, both halves of the | ||
| 372 | `--session` limitation, an old `muxa`'s empty `status_req`, the | ||
| 373 | `stats_req` version discriminator the banked mitigation would need, and a | ||
| 374 | teardown observed rather than claimed. It exists because M18's by-hand | ||
| 375 | run found a *documentation* error days after the sentence was written; a | ||
| 376 | gate finds that on the next run. | ||
| 377 | |||
| 378 | Two things about it stated rather than left implied. **Containers share | ||
| 379 | the host kernel**, so nothing there validates pty, poll or fd semantics | ||
| 380 | against a different one — most of what muxd is. That needs VMs and is not | ||
| 381 | claimed. And the rig is **falsifiable on purpose**: point `XVER_OLD_BIN` | ||
| 382 | at the new binaries and legs must fail. Doing exactly that caught a false | ||
| 383 | pass in the gate's own first draft — the QUIC arm keyed on the client's | ||
| 384 | exit code, and a daemon that HONOURS the name also leaves the client | ||
| 385 | attached until `timeout` kills it, so 124 meant both things. It now asks | ||
| 386 | the daemon whether a session called `zz` appeared, which only a daemon | ||
| 387 | that understood the name can produce. | ||
| 388 | |||
| 389 | `muxa capture` is **no longer the one verb with no automated coverage**: | ||
| 390 | the M18 block drives it by name and asserts it returns one session's | ||
| 391 | grid and not its neighbour's. `--diff-since` is still owed. | ||
| 286 | 392 | ||
| 287 | ## M16 candidates — and still outranked by trial feedback | 393 | ## M16 candidates — and still outranked by trial feedback |
| 288 | 394 | ||
| @@ -290,6 +396,57 @@ What remains is what was ranked behind M13 and M14 and survived M15, | |||
| 290 | plus the seven items M15 created. Nothing here is a field finding; the | 396 | plus the seven items M15 created. Nothing here is a field finding; the |
| 291 | next one that arrives outranks all of it. | 397 | next one that arrives outranks all of it. |
| 292 | 398 | ||
| 399 | - **Probe for named-session support before sending a named attach** — | ||
| 400 | M18's measured cross-version gap. `--session NAME` against a pre-M18 | ||
| 401 | daemon is dropped cleanly on the socket path but ignored in **silence | ||
| 402 | over QUIC**, where the client hangs with nothing printed. The two | ||
| 403 | differ because a pre-M18 QUIC connection takes a client slot at | ||
| 404 | `quicOnOpen`, so its attach reaches `handleFrame`'s silent | ||
| 405 | `decodeAttach catch return`, while a socket connection is an observer | ||
| 406 | until it attaches and `serviceObserver` closes on a bad one. | ||
| 407 | |||
| 408 | A `hello`/version frame cannot fix this: `MsgType` is non-exhaustive | ||
| 409 | and both switch arms end in `else => {}`, so an old daemon ignores an | ||
| 410 | unknown frame in exactly the silence that is the bug. What works is a | ||
| 411 | probe using a verb old daemons ALREADY answer — `stats_req`, which | ||
| 412 | pre-M18 `handleFrame` serves without an attach, on QUIC included — and | ||
| 413 | reading the reply: M18's stats text carries `sessions=`, pre-M18's | ||
| 414 | does not. Zero protocol change, effective against shipped binaries, | ||
| 415 | and it yields a true message rather than a timeout's guess. | ||
| 416 | |||
| 417 | Gate it on the capability and refuse only the ATTACH, never the | ||
| 418 | connection: an unnamed attach is byte-identical to pre-M18 and works | ||
| 419 | today, so a version-equality check would break configurations that are | ||
| 420 | currently fine. Ranks above the rest of this list for anyone running a | ||
| 421 | wall against boxes that upgrade on their own schedule, which is every | ||
| 422 | wall. A `hello` frame is still worth adding for its own sake — free, | ||
| 423 | safe, and it makes the NEXT version pair a negotiation instead of | ||
| 424 | another archaeology exercise. | ||
| 425 | - **A refused attach should say which refusal it was** — M18 review | ||
| 426 | finding. `resolveSession` returning null is answered with | ||
| 427 | `exit_status 1` for four different causes: a name that failed | ||
| 428 | validation, a full session table, an unknown name with no size to | ||
| 429 | create at, and a spawn that failed. That byte is also what a shell | ||
| 430 | exiting 1 sends, so five distinct outcomes arrive identical. The | ||
| 431 | full-table case was the model — "the same honest no a full client | ||
| 432 | table gives" — and it is honest there, where there is genuinely one | ||
| 433 | answer; it stopped being honest once four causes started sharing it. | ||
| 434 | |||
| 435 | The evidence that this costs something is that **the tests cannot read | ||
| 436 | it either**. `test/e2e.sh` has to disambiguate through a side channel | ||
| 437 | (`muxd stats | grep sessions=`), and `test/xversion.sh` had to invent | ||
| 438 | `assert_name_not_honoured` to ask "did a session appear?" because the | ||
| 439 | reply itself could not say. When a protocol's own suite has to go | ||
| 440 | around the answer to find out what happened, an operator staring at a | ||
| 441 | bare exit code has strictly less to work with. | ||
| 442 | |||
| 443 | Not worth a protocol change on its own — bank it with the probe above, | ||
| 444 | which is the other half of the same story and would land in the same | ||
| 445 | place. The cheap client-side half needs no wire change at all: on a | ||
| 446 | **named** attach that never produced a snapshot, `mux` can print "the | ||
| 447 | daemon refused the attach or the shell exited 1" instead of exiting | ||
| 448 | silently. That one line covers the case an operator actually hits, | ||
| 449 | which is a typo in `--session`. | ||
| 293 | - ~~**`drain()` swallows `ECONNREFUSED`**~~ — paid by M15 (Task 4). Both | 450 | - ~~**`drain()` swallows `ECONNREFUSED`**~~ — paid by M15 (Task 4). Both |
| 294 | socket paths now route the refusal through one verdict, and an | 451 | socket paths now route the refusal through one verdict, and an |
| 295 | unreachable port fails in **1ms** instead of a full deadline | 452 | unreachable port fails in **1ms** instead of a full deadline |
| @@ -380,7 +537,14 @@ next one that arrives outranks all of it. | |||
| 380 | 6. **`addCSourceFiles` for the QUIC deps**, replacing the build script. | 537 | 6. **`addCSourceFiles` for the QUIC deps**, replacing the build script. |
| 381 | 7. **Autotools wolfSSL + ticket resumption** — the only route to 0-RTT. | 538 | 7. **Autotools wolfSSL + ticket resumption** — the only route to 0-RTT. |
| 382 | 8. **Certificates / TOFU · QUIC datagrams for input · multi-session per | 539 | 8. **Certificates / TOFU · QUIC datagrams for input · multi-session per |
| 383 | connection · NAT traversal** — parked as a group. | 540 | *connection* · NAT traversal** — parked as a group. Multi-session |
| 541 | **shipped in M18, but per *daemon***: one socket path / port / key | ||
| 542 | holding N sessions, each its own connection. What stays banked here | ||
| 543 | is the narrower thing this line originally meant — several sessions | ||
| 544 | multiplexed inside ONE connection, which needs QUIC stream surgery | ||
| 545 | the M18 design deliberately refused (a connection IS a session). | ||
| 546 | Worth doing only if the connection count per host ever becomes the | ||
| 547 | cost, which on a four-tile wall it is not. | ||
| 384 | 9. **Scrollback retention tuning** — policy decided (engine-native 10k | 548 | 9. **Scrollback retention tuning** — policy decided (engine-native 10k |
| 385 | ring); making the limit configurable is what remains, and nobody has | 549 | ring); making the limit configurable is what remains, and nobody has |
| 386 | hit the ceiling. | 550 | hit the ceiling. |