cmux-tui: put-session stores a session's whole capability list, so launches restore machines again
Summary
Since mid-October, the daemon refused every put-session from the app. The capability list a session advertises grew past the fixed 4,096-byte bound on a personal JSON value (5,765 bytes on Austin's 10-29 build). The app records each connected machine with put-session (SessionRegistrar). So no machine reached the session registry, nothing showed this, and at the next launch SSHService / ServerReachService found nothing to restore (P-AUSTIN B7a).
Each personal field now has a bound from its own content or from where the record goes (R9 R-L1):
capabilities: unique tokens of[a-z][a-z0-9._-], 1-64 bytes each. The list is at mostMAX_EVENT_PAYLOAD_BYTES / 2as JSON (512 KiB, derived in code from the journal's budget for one client event payload; the journal does not apply that budget to this record, but it is the size one event is built to carry, and the record's other fields are a few KiB). A larger list is refused, and the error gives its size.transportand browser profilesource: 4096 bytes, as before (small objects; the longest field is a path, and 4096 is LinuxPATH_MAX, where Cloud machines run).proxy: no byte cap of its own.validate_browser_profile_proxyalready bounds it by its structure (about 17 KiB). The 4096 cap refused valid proxies with many bypass rules.- The launch snapshot (8 MiB) leaves out session capabilities, as it already leaves out room env.
The app no longer loses a refused machine silently (R-L4, R-L5). SessionRegistration (CmuxNextDaemonService) sends each record through a SessionRegistrationLedger (CmuxNextDaemon):
- A deterministic refusal (a
bad request:answer not marked retryable: size or validation) is shown once per refused session as a toast that stays until closed: " was not saved for restore: ". The same record is not sent again until it changes or the home store's connection epoch is new (aDaemonConnectionreconnects in place after a daemon restart, so the epoch, not the object, tells a new connection). - No answer, a store error or a retryable refusal is sent again after
RetryPacer's capped, jittered backoff (250 ms to 30 s, 8 timed tries). When the tries are spent, the record waits for an event (a changed record or a new epoch), and the same notice shows with the error. - Each send has a token; an answer to an older send, or from an older connection, changes nothing. Sessions that leave the machine list are forgotten; a refusal already shown is not shown again.
- A notice that no window could show waits and shows when a window becomes key (
WindowManager.didActivate); it is dropped if its record is recorded or its session is gone. - The record holds the machine name (sent again when it resolves or changes) and the capabilities unique and sorted.
SessionRegistrar in CmuxNextApp keeps only the wiring: it builds the records while home is connected, submits them, and shows the toast through CmuxToastCenter. The notice strings are in CmuxNextDaemonService/Resources/SessionRegistration.xcstrings, all 21 languages.
When the list passed 4,096 bytes: a static estimate over feat-cmux-next first-parent commits rises from about 2.3 KB (10-08) to about 4.1 KB (10-18). Many capability additions did it, not one commit; 59d0076c54c (#21322, 10-18, about +200 bytes) is the largest step near 10-19.
Owner of the area: PL-daemon (session cmux-b6), plans/cmux-next/OWNERS.md.
Testing
The red commit 487921ca6df does not compile (a mutating call inside #expect). 3d79af5b7d7 reads each result into a local first; the assertions did not change (reviewer 3 confirmed: every condition and message matches 487921ca6df). The red proofs run at test-only refs of fix/pa-b7a-red-proof (never merge): the head with the ledger set back to its red stub and, for SessionRegistrationTests, SessionRegistration.swift set back to its red commit d629cf35d65. The test files on the red-proof ref are identical to the head's.
Red tests, each committed before its fix: whole_capability_list (492057eab90, Rust), SessionRegistrationLedgerTests (487921ca6df), SessionRegistrationLedgerSendTests (09d19edf968), SessionRegistrationTests (d629cf35d65; the first-window test is added with its fix).
| what | ref | job | result |
|---|---|---|---|
Rust test whole_capability_list red | 492057eab90 | 7c4b39b326f549e1d838a13a | 2 run, 2 failed: capabilities exceeds 4096 bytes |
Rust test whole_capability_list green | 7c734f6290f | 0d1b1df3bf5b94f650e1ef7f | 2 run, 2 passed |
Rust test capabilit | fbbfa5b1840 | bd40c08b9ddb751363cbb89d | 117 run, 117 passed |
Rust test launch_snapshot_carries_personal_state | fbbfa5b1840 | 1f40d6d79fe7df3335ba8d46 | 1 passed |
Rust test a_browser_profile_carries_a_proxy | fbbfa5b1840 | 7fe7fc423ced5860a68c4b8c | 1 passed |
Rust clippy | ef51a99acd4 | 16e512e9addc3d560758576b | passed; later Rust changes are comments only |
| Mac ledger suites red | ae688d14546 | 01004a44e611ad00b08f3f82 | both suites failed on assertions (11 and 6 issues) |
| Mac ledger suites green | b87096b9ddf | 8105cb1f2245af2f9a30d53b | 5/5 and 4/4 passed (rerun: 87684297dcb84ec1f1f92fdd lost its worker lease, no verdict) |
| Swift compile-check, Mac green and red at the head (review rounds 4 and 5) | (this head) | follow in a comment |
Earlier compiles that failed and were fixed: 970a429120583134fefc67a7 (access levels), 605a6a90c0fc930ad9d08b0d (record isolation), f4bbd94d4677e769d47bb067, 2d46b94dfa65133cffadf729 (the connection id type).
Known limits (for review)
- The wiring test (
SessionRegistrarWiringTests) coversSessionRegistrar.homeKeyandcanRegister, not the observation loop that calls them (SessionRegistrar.start,sync). Reason: driving the loop needs aMachineRegistrywith a connected home daemon, which no test helper fakes today. Follow-up: a test that drives a fakeMachineRegistrythrough connect, restart and sign-out and checks the sends. - The observation key has no
connectionEpoch. A reconnect to the same daemon process inside one main-actor transaction gives an equal key, so it does not resync. That daemon still holds every record it accepted:put-sessionwrites them to thesessionstable of the session registry (SQLite,cmux-tui/crates/cmux-tui-core/src/workspace_registry/personal_mutations.rs:418, table atpersonal_store.rs:82), which lives on disk across connections. A daemon restart changespidandgeneration, which the key holds, so a restart always resyncs.
Observed / Not run
- Observed: the fleet results above;
prepush-gate.pypasses;scripts/cmux-next/check-l10n.shis at its ceiling (100), as on the base. - Not run: the toast was not seen in a running app (no capture). Austin's-day numbers: pending harness; the scenario has no restore step, so this fix is not measured on it.
Changelog
Fixed: Connected machines are restored again at launch, and a machine the daemon refuses to save now shows a notice.
Proof
No capture: the only UI is a text toast through the existing toast component, shown only when the daemon refuses a machine; no capture scene reaches it.