cmux-tui: reads run no write; workspace.list and session.ping read only what they answer
Summary
Under an 80-agent load the session snapshot was the workspace registry lock's longest holder: resource_api.rs:551 held it for 25.7 s in 60 s (fleet step 73e93323, server-stats locks from #28147). It also held mux.state for 23.3 s. Most of its calls were agents' CLI list calls, and two of its parts wrote to the database while holding the lock. Design rule 1 of the lock work (#27698) is that a read never writes under the lock.
- A read runs no write.
- The snapshot no longer deletes read marks of notifications outside the retained window. It skips them, and the registry open deletes them, because the runtime prune queue does not survive a restart.
- It no longer publishes pending terminal directories (a full
terminal.cwdcommit) before it reads. Terminal output, resize, reconnect and every committed graph change still publish them.
- One-collection reads read one collection.
workspace.listbuilds only the workspaces: workspace rows, the topology frame and workspace decorations, under the registry lock alone, without the state lock.session.pingreads only the topology frame for its cursor.- One builder makes the workspace values for both the snapshot and the list, so the list equals the snapshot's
workspaces(a test checks this).
- Adoption publishes the shell's replayed directory (review of this PR, M1). The snapshot used to publish pending directories on its first read; that was the only step after a daemon restart that published an idle adopted shell's replayed directory (#12978).
finish_terminal_adoptionnow publishes the pending directory and progress once the terminal is live, as a reconnect does. Red test:an_idle_shell_presents_its_reported_directory_after_its_host_adopts(red step 3ead7ba355c2aab83e7beeae at 72a5382b70f: "the adopted shell presents its launch directory, not its reported ..."; passes at the head).workspace.listalso holds the registry connection for its whole read, so no late writer commit lands between its rows, frame and decorations (review L1). - #28159 review, Low 1 and Low 3.
lock_state_pinnedsettles the late answers that arrived when it pinned. An answer can still land after that settle, because the writer drops the connection and then sends its receipts. A pinned prepare now refuses while such an answer is unsettled, instead of building on state without the late commit. The next pin settles it.- A rename whose workspace left state after a durable commit raises the in-memory revisions before it reports the error.
An app terminal's first view also publishes the directory and progress its shell reported before the record existed (fresh review finding 1). Red test: the_first_view_publishes_a_directory_the_shell_reported_before_it (red step af09d2f0955b8bfff205602d at 6876e72646f).
Contains #28147 and #28159. Review only the commits after 352210fa9976. #27780 (Austin) touches the same lock. Owner: PL-daemon (session cmux-b6).
Testing
Red, at b870e5cfd36d (tests only):
- Fleet step
0f68ce7b9cfd5e97c628bc68,cmux-tui-rust-check.sh test read_paths. Three tests FAILED for the stated reasons:a_session_snapshot_read_runs_no_write: "session.snapshot wrote {resource_notification_reads}".workspace_list_reads_only_workspace_tables: it readresource_screens,resource_panes,resource_tabs,resource_browsers,resource_terminals,terminal_hosts, the projections andsession_journal.session_ping_reads_only_the_topology_frame: the same tables.
opening_the_registry_drops_read_marks_outside_the_retained_windowpassed on the base too. It guards that the table stays bounded once the read stops deleting; it is not a red test.- Fleet step
d5f923e12bcfe340a4c95b34:a_pinned_prepare_refuses_a_commit_that_landed_after_its_settleFAILED with "a pinned prepare built on a commit its state does not hold".
The tests record the tables each statement reads and writes through a SQLite authorizer on the registry connection.
Green:
- Full run at the head 481cab7733d9: step 38e8e52e01839ea5d499f49e: pending. The previous head 7093b67fd9ef passed with only the two base reds (step 3c9ea216); 481cab7733d9 adds only the merge of #28147's crash ratchet fix (the state guard holds its lock guard directly, with no expect()).
- Earlier full run
27e89f4f2f3e37cef1dd361b(076d306c3d03, Linux): 12710 tests ran and 12707 passed. Failures: two base reds, the CLI noun-first path (#28119) and the acpmux hermetic guard (#28150), plus the acpmux wireguard 1012-close test (failed in the full run, passed alone; acpmux does not depend on cmux-tui-core; listed for a fixer). - Focused run
0107740c10d3fd5023fddd2bat the head 35b4afceebc6 (Linux):named_server_status_stop_alias_and_durable_restart_preserve_topologyPASSED. - Mac 5/5 of that test at the head 481cab7733d9 (after the merge of #28147's crash ratchet fix): 5 of 5 PASSED on the first run with no failure and no retry, all on aws-m4pro-2, each "1 test run: 1 passed": 9afd04923f7758f3891a86e4, b467183076d021d4afe209fa, c3b7f85fbebb739adc2f7441, ca7d72b516494455f772001e, efdb02786c67ea89e94a684c. (Earlier heads also passed 5/5: 35b4afceebc6, 3e5ca9963e33, 7093b67fd9ef.)
Test fix, not a loosened test: named_server_status_stop_alias_and_durable_restart_preserve_topology.
- It FAILED on this branch (step
23b44075, at d1457b2e06d6):server reload-configexited 3 with "the local server is still starting". It PASSED on the base (step6317235aaefaacf34d409974). - Cause: the test sent lifecycle commands right after the socket accepted. The server accepts them only once it is lifecycle-ready (
Mux::mark_server_lifecycle_ready). The test passed only while theworkspace listbefore them was slow, and this PR makes that list fast. - The test now waits, after each start, on the product's own
lifecycle_readysignal inidentify. That wait is level-triggered (R5) on the product's signal, not a sleep for a fixed time: it readsidentifyevery 25 ms untillifecycle_readyis true, bounded at 15 s, and every assertion is unchanged. - The product gap, a lifecycle command right after start fails instead of waiting on that signal, is in the handoff followups, owned by B2. It is not part of this PR.
Observed / Not run
-
Base reds (they fail on the base 352210fa9976 with the same assertion; this PR does not touch them):
flow_trace_bind::the_daemon_bind_path_starts_flow_marks("socket path parent is not a directory: /tmp"; base step d73fc2c9).replace_unresponsive_owner::ensure_reports_a_frozen_owner_unavailable_without_waiting_out_the_restore_bound("not reported unavailable"; base step db1a43e8).- The CLI noun-first path test (#28119) and the acpmux hermetic guard (#28150).
-
The first green (23b44075) also had 9 acpmux reds. This PR cannot reach them: acpmux does not depend on cmux-tui-core. They are listed for a fixer, not cleared as flaky.
-
Not run: Austin's-day numbers, which the harness owner runs and posts. Perf claim: none until then. My diagnostic bench ranked this holder first; it is not the merge evidence.
-
R9 row R-L3 fails, owned by B2 (lead decision 2026-10-30). Both refusals in
drain_unsettledreach clients as an untypedoperation_failedwith no busy code and noretry_after_ms:workspace_registry/registry_intent.rs:244: "an earlier session journal registry commit is still unanswered; this request is retryable: retry now". This one exists on the base.registry_intent.rs:251: "an earlier session journal registry commit landed after this request read state; this request is retryable: retry now". New in this PR. The next pin settles the late answer, so an immediate retry succeeds.
The typed busy error with
retry_after_msis B2's next PR (pa-b2, after #28041): it adds a retryableregistry.busycatalog error withretry_after_ms, and the CLI and hooks back off with jitter. pa-b2 has accepted both sites. Until it opens, the tracking entry is "NEXT PR after #28041, owner pa-b2" in the lead's followups. Until then, both messages say the request is retryable now.
Risk tier
The risk gate labels this PR risk:high: it adds a DELETE FROM (the new data deletion row). That line is prune_stale_notification_reads_at_open. It deletes the same rows the snapshot read deleted before this PR (read marks of notifications outside the retained window): the delete moved from every read to the registry open, and it deletes nothing new. The high tier needs an independent review receipt on the exact head and a green compile. A fresh review at the new head 481cab7733d9 follows its green; the review on 7093b67fd9ef no longer counts.
Performance
No perf numbers: this PR claims no measured speedup yet. My diagnostic bench ranked the snapshot reader first (step 73e93323); it is not merge evidence. The Austin's-day before and after numbers come from the harness owner and will be posted on this PR.
Changelog
Fixed: Agents' cmux workspace list calls no longer make the rest of cmux NEXT wait on a busy Mac.
Proof
No capture: daemon-only change, no UI.