cmux-tui: put-session stores a session's whole capability list, so launches restore machines again

Merged
LA
6d ago
+1604-32

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 most MAX_EVENT_PAYLOAD_BYTES / 2 as 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.
  • transport and browser profile source: 4096 bytes, as before (small objects; the longest field is a path, and 4096 is Linux PATH_MAX, where Cloud machines run).
  • proxy: no byte cap of its own. validate_browser_profile_proxy already 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 (a DaemonConnection reconnects 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).

whatrefjobresult
Rust test whole_capability_list red492057eab907c4b39b326f549e1d838a13a2 run, 2 failed: capabilities exceeds 4096 bytes
Rust test whole_capability_list green7c734f6290f0d1b1df3bf5b94f650e1ef7f2 run, 2 passed
Rust test capabilitfbbfa5b1840bd40c08b9ddb751363cbb89d117 run, 117 passed
Rust test launch_snapshot_carries_personal_statefbbfa5b18401f40d6d79fe7df3335ba8d461 passed
Rust test a_browser_profile_carries_a_proxyfbbfa5b18407fe7fc423ced5860a68c4b8c1 passed
Rust clippyef51a99acd416e512e9addc3d560758576bpassed; later Rust changes are comments only
Mac ledger suites redae688d1454601004a44e611ad00b08f3f82both suites failed on assertions (11 and 6 issues)
Mac ledger suites greenb87096b9ddf8105cb1f2245af2f9a30d53b5/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) covers SessionRegistrar.homeKey and canRegister, not the observation loop that calls them (SessionRegistrar.start, sync). Reason: driving the loop needs a MachineRegistry with a connected home daemon, which no test helper fakes today. Follow-up: a test that drives a fake MachineRegistry through 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-session writes them to the sessions table of the session registry (SQLite, cmux-tui/crates/cmux-tui-core/src/workspace_registry/personal_mutations.rs:418, table at personal_store.rs:82), which lives on disk across connections. A daemon restart changes pid and generation, which the key holds, so a restart always resyncs.

Observed / Not run

  • Observed: the fleet results above; prepush-gate.py passes; scripts/cmux-next/check-l10n.sh is 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.