Skip to content

fix(cmux-tui-core): the surface admission budget reserves with atomics on the input lane (R-DAEMON-STATE S1 step 3) - #34146

Merged
lawrencecchen merged 10 commits into
feat-cmux-nextfrom
fix/p0-typing-s1-admission
Nov 4, 2026
Merged

lawrencecchen merged 10 commits into
feat-cmux-nextfrom
fix/p0-typing-s1-admission

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

Summary

R-DAEMON-STATE stage S1 step 3 (handoff/rewrites/R-DAEMON-STATE/DESIGN.md, section 3 and Appendix A.2 step 11). #34014 (stage S1 step 1) merged at 21:52:58Z (squash 4bb12b8); this PR now targets feat-cmux-next directly. Head e2a052e merges feat 8880dae (through ff04cf67b63, then a no-change merge that records the squash, whose content the branch already had from 590cb11). The PR's own diff is unchanged: the same patch-id (be5d75f4106) against the review head's base and against feat now.

Every request of every connection, the person's key included, reserves its retained bytes from one daemon-wide budget (ServerSurfaceOperationAdmission) on the connection's reader thread before it is queued, and releases them after it ran. The reservation took one mutex that all connections share, so a key waited for any other connection's reservation or release. The S1 census measured a full wait: 300.3 ms while another thread held it 300 ms (hosted run 41648095321).

With the stage switch on (CMUX_TUI_RDS_INPUT=1, per mux, default off), the two counters are AtomicUsize with a compare-and-swap reservation (fetch_update with checked_add and the same capacity filter: 16 workers, 16 MiB), so a reservation never waits. A permit records the arm it reserved from and releases to the same one. With the switch off, the old mutex counts run unchanged; they go with the switch when the stage passes.

Testing

a_key_waits_for_no_surface_admission_lock (cmux-tui/crates/cmux-tui-core/src/server/tests/rds_input_lane.rs): two keys through the real line path while another thread holds the old arm's lock 300 ms. A control first proves the hold reaches the old path (switch off: the key must wait), so the lane's pass is not a test that holds nothing. The step-1 control (a_key_waits_for_no_client_registry_lock) gets the same control arm.

Focused command: ./scripts/verify-cmux-tui-hosted.sh --filter rds_input_lane.

Both runs also show base reds that this branch's older base still has: lint (linux) and the macOS build fail on acpmux/src/login_env/watch.rs unused imports (fixed on feat by #33664 and #33690), and macOS acpmux --test web_image_attachments aborts at tests/support/daemon_env.rs:464 (node on the test's PATH). This PR touches none of those files.

Observed / Not run

  • Observed: the two hosted runs above (Linux test job).
  • Observed: fleet compile job 189bde14160772147cb9455f at 8e5c8a2 passed (CmuxNext 344 s, app 114 s, cmux-tui 61 s). At e2a052e: compile job 1548c3c24c244948c7b8f356 and the focused hosted run (below) are running.
  • Not run: macOS execution of the test (the macOS job stops at the acpmux base red before it); the real-app A/B (after S1 lands, with the measurement owner).

Review

Review (claude-reviewer-rds-s13): SHIP at 8e5c8a21191de2f7c594631d172687ae6133e638 (comment 6613345630). Its notes are taken in #34261's branch (831686b): the budget tests on both arms, a lost-key control, private fields, the doc wording.

Changelog

none

Proof

No capture: daemon-only change behind a default-off switch, no UI.

…MON-STATE S1 red)

A key line from an attached view goes through the real connection path (the
line handler, the connection's dispatcher, the send handler) while another
thread holds one shared lock for 300 ms. #31940 took Mux.state off the key;
the key still waits for the client registry lock (five takes per line), the
link peers lock, the host proxy authors lock, the surface admission lock, the
creating-input lock, the client sizing lock and the activity lock. Their other
holders are every connect, attach, resize and request of every client.
Mux.state and the workspace registry rows are controls (pass on the base).

Two keys go in a row, so a lock taken after the PTY write (sizing, activity)
shows as a delay of the second key on the connection's dispatcher.

Test-only hold hooks (cfg(test)) in creating_input.rs, terminal_sizing.rs,
activity.rs and agent_session_attach.rs. No product change.

Design: handoff/rewrites/R-DAEMON-STATE/DESIGN.md section 2.6 (stage S1).
…N-STATE S1 step 1 red)

The previous commit's census test held nine shared locks in turn; this commit
splits it by fix step. Step 1 is the client registry lock (ClientRegistry.state),
which a key line takes about nine times (handoff checks, remote-client and
remote-device checks, the request's actor, the kept terminal, the reply's
incognito filter) while every connect, disconnect, attach and detach of every
client holds it.

The test turns on the stage switch CMUX_TUI_RDS_INPUT (rds_switch.rs, inert in
this commit) and drives two keys through the real connection path while another
thread holds the lock for 300 ms. A second test keeps Mux.state and the
workspace registry off the key on the new path (#31940).

The hold hooks of the other locks leave with this commit; they come back with
their fix steps.
…ished facts, never its lock (R-DAEMON-STATE S1 step 1)

A key line took the client registry lock (ClientRegistry.state) about nine
times: the daemon handoff checks, the remote-client and remote-device checks,
the request's actor, the kept terminal of the key and the reply's incognito
filter. Every connect, disconnect, attach, detach and capability change of every
client holds that lock, so an agent storm held back the person's keys (red at
2472251: 300.4 ms behind a 300 ms hold; STATUS 9131).

The registry is now a Published value (published.rs): writers still serialize
on its mutex, and a guard that changed the value publishes an immutable
ClientFacts projection (handoff state; per connection its transport, remote
device flag, base actor and kept input terminals) with arc-swap when it drops,
under the mutex, so projections follow the order of the changes. With the stage
switch CMUX_TUI_RDS_INPUT=1, the line and key paths read the projection with
one atomic load: daemon_handoff_in_progress/committed, transport_of (so
is_remote_client), is_remote_device (refuse_line, line_to_recheck,
prune_reply), connection_actor and attached_input_surface. A poisoned registry
still fails closed (the projection comes in the error, as Mutex::lock gives
its guard). With the switch off nothing changes.

A projection costs O(connections) under the registry lock per change, bounded
by the 64-connection cap; stage S2 makes it O(changed).

arc-swap 1.9 is already in the lockfile and the shipped notices (through
iroh-dns); cmux-tui-core now names it.

Tests: rds_input_lane (a key waits for no client registry lock; the session and
workspace registry locks stay off; a respawn under the same id sends the next
key to the new shell), published.rs unit tests (a change publishes on drop, a
read publishes nothing, a reader never waits for a writer, a poisoned value
fails closed). The two remote-relay poison tests poison through the new type.
… stage switch per mux (R-DAEMON-STATE S1 step 1)

The hosted run of f1ea1b6 (https://github.com/manaflow-ai/cmux/actions/runs/41655768031) still failed
both lane tests: the key waited 300 ms for the client registry and, new, for Mux.state. Two causes:

- The key still used the kept terminal of #31940. That cache is validated by process-wide route
  generations, and cargo test runs the tests of one binary as threads of one process: another test's
  mux moved the same generation slot (ids start at 1 in every mux), the kept terminal looked stale, and
  the key fell back to the locked lookup and then to remember_attached_input (registry lock). The
  stage switch was a process-wide static too.
- So the lane must not depend on a cache with process-wide invalidation at all.

Now, with CMUX_TUI_RDS_INPUT=1:
- The session's three routing tables (surfaces, terminal_catalog, terminal_catalog_by_runtime) are
  copy-on-write behind an Arc. A state guard that changed one publishes a RouteSnapshot (three Arc
  clones) with arc-swap when it drops, under the lock, so publications follow the changes; the
  snapshot it replaced is freed after the lock is released. Mux::surface reads the snapshot with one
  atomic load. A respawn, close, move, rollback or projection is seen once its guard is released, as a
  reader that waited for the lock would see it. Nothing is kept per client, so nothing is invalidated.
- input_surface on the lane: the published routes give the terminal, the registry's published facts
  say whether the client types into it (types_into: the person's input QoS mark). attached_input_surface
  and remember_attached_input stay only on the old path.
- The switch is per mux (rds_switch::InputLaneSwitch, read once from the environment when the mux is
  built; Mux::set_input_lane_for_test in tests), and it also decides whether anything is published: off,
  the registry builds no projection and the state holds no route snapshot, so a route write never
  copies a shared table and the off arm costs what the base costs.

Cost on the lane: a guard's first route write copies that table once (O(routes)), bounded by one copy
per changing guard; State::clone no longer deep-copies the three tables. Stage S2 makes both O(log n).
…an old-path control (R-DAEMON-STATE S1 step 3 red)

Every request of every connection reserves its retained bytes from one
daemon-wide budget (ServerSurfaceOperationAdmission) under one mutex before it
is queued, and releases them under it after it ran. The S1 census measured a
key waiting 300.3 ms behind a 300 ms hold of that lock (hosted run 41648095321).

The lane test now has a control: the same two keys with stage S1 off must
wait for the held lock, so a lane result can never come from a hold that does
not reach the key path (a vacuous green). The control is added to the client
registry test too. A test-only hook (cfg(test)) holds the admission lock.

Red here: with the lane on, the key still reserves through the mutex.
…s on the input lane (R-DAEMON-STATE S1 step 3)

Every request of every connection reserves its retained bytes (and every
clear-history its worker) from one daemon-wide budget before it is queued and
releases after it ran. Under one mutex, a key waited for any other
connection's reservation or release (red at 5d0e4a0: 300 ms behind a
300 ms hold).

With CMUX_TUI_RDS_INPUT=1 the two counters are atomics with compare-and-swap
reservations (fetch_update: add only while the sum stays within the capacity;
release saturates at zero), so nothing waits. The limits and the refusals are
the same as the mutex arm's: a worker is refused at 16 reserved, bytes when
the sum would pass 16 MiB. Each permit releases to the arm it reserved from.
With the switch off the mutex arm runs as before; it goes with the switch when
the stage passes (S1.8). The arm is fixed when the mux is built
(ServerSurfaceOperationAdmission::new(publish_routes)); tests set it with
Mux::set_input_lane_for_test before any request.

The two scheduler tests that counted reserved workers through the mutex read
reserved_workers() (both arms).
@lawrencecchen

Copy link
Copy Markdown
Contributor Author

Review (claude-reviewer-rds-s13): SHIP at 8e5c8a2
reviewer-session: 6c5437c8-e82e-476c-885f-699e0a8d26a1
author-session: ae25c6d154f94c01a
R9: pass | no row of the layer-ownership table changes: the surface-operation budget is daemon-internal Rust state (section 1 item 1, rule L2). Its one owner stays the mux's ServerSurfaceOperationAdmission. The switch picks one arm per mux when the mux is built, and each permit releases only to the arm it reserved from, so no count gets a second owner or a copy in Swift or TS.
perf claim: none

I found no defect that must be fixed before merge. I read the diff (590cb11..8e5c8a2), every caller and reader of the budget in cmux-tui/, and the logs of the red and green hosted runs. I did not build or run anything.

What I checked

  1. Capacity. reserve (worker_admission.rs:176-182) admits exactly what the mutex arm admits in every state that either arm can reach (held <= capacity). A 17th worker is refused at 16. Bytes are admitted while held + n <= 16 MiB. A zero-byte reservation is admitted at a full budget on both arms. A reservation larger than the capacity, or one whose sum overflows, is refused (checked_add gives None; on the mutex arm n > CAP - held). Each successful CAS writes a value that passed the filter, so no interleaving of reserve and release takes a counter above its capacity. fetch_update retries a failed CAS with the new value, so contention causes no false refusal.
  2. Release. A permit is not Clone, and code outside worker_admission.rs cannot construct one (the new private lock_free field). Drop runs once, so there is no double release. A release goes to the arm that the permit recorded. saturating_sub cannot underflow (the same as the mutex arm). If the switch changes while permits are live, each arm's count stays correct, but the two arms together can hold up to 2x the capacity. Only set_lock_free_for_test (cfg(test)) can change it, and its only caller (TypingConnection::open_with, rds_input_lane.rs:63) sets it right after test_mux(), before any reservation.
  3. Callers and readers. Production reservations: ConnectionSurfaceScheduler::dispatch (bytes, on the reader thread, connection_scheduler.rs:157) and run_connection_surface_dispatcher (clear-history worker, :396). Each production scheduler (line_connection.rs:78, websocket_listener.rs:402, agent_session_proxy.rs:631 and :811) takes the mux's admission, so one mux uses one arm. No stats line and no drain check reads the counts. The only readers left are worker_admission.rs itself and reserved_workers() (both arms) in the two scheduler tests. Those tests use default() (the mutex arm), so they read the same values as before.
  4. Off arm. new(false) is the same as default(). The off arm adds one relaxed load per reservation and one bool per permit, and runs the base's mutex code unchanged.
  5. Memory ordering. AcqRel/Acquire is sufficient. The counters guard no other data. The code relies on one order: the clear worker releases its permit before ActiveClearGuard wakes the next request (connection_scheduler.rs:418-421). This stays true on the atomic arm. The release RMW comes before the worker unlocks the scheduler mutex in finish_clear, and the dispatcher's next try_reserve_worker comes after it locks that mutex, so it reads the released count. The relaxed lock_free load is safe: the value is set before the Arc is shared, or (tests) before any thread that reads it starts.
  6. Tests and evidence. The control is real: hold_for_test holds the same mutex that try_reserve_bytes takes on the old arm. Red run 41685706056 at 5d0e4a0, test (linux): control 300.259716 ms, lane 300.274746 ms, a_key_waits_for_no_surface_admission_lock FAILED at rds_input_lane.rs:212; the other 3 passed. Green run 41685706038, checkout at 8e5c8a2: 4 passed, 0 failed. The 300 ms hold and the 150 ms budget leave about 150 ms on each side. The control's wait cannot go below the hold minus the gap between the holder's signal and started. Keys that take no held lock measured 0.018-0.105 ms in the S1 census (run 41648095321). Two keys also catch a lock taken after the PTY write, because the bytes permit drops at the end of run_pending_request (connection_scheduler.rs:378). The other non-green jobs are not caused by this PR. lint (linux) and this PR's clippy (changed cmux-tui packages) fail on crates/acpmux/src/login_env/watch.rs:15 (unused notify::Watcher, fixed on feat by base red: the notify Watcher import is macOS-only (unused on Linux after #33406) #33664). macOS fails on watch.rs:227 (unused super::*, base red: acpmux's login-env watcher test module is Linux-only, so macOS has no unused import (clippy, after #33406) #33690), and acpmux --test web_image_attachments aborts at tests/support/daemon_env.rs:464 while it lists the filter, so macOS did not run rds_input_lane (the PR body says so).
  7. Compile. Fleet job 189bde14160772147cb9455f at 8e5c8a2 is done: CmuxNext ok (344 s), app ok (114 s), cmux-tui ok (61 s). In the green lint (linux) job, clippy -D warnings --all-targets checked cmux-tui-core (it does not depend on acpmux) with no finding; only acpmux failed.
  8. Panic and crash ratchets. The product code adds no unwrap, expect, indexing or arithmetic: reserve uses checked_add, release uses saturating_sub, and the old arm's += lines are base lines. The .unwrap() and .expect( counts of the touched product files are unchanged (the test file outbound_and_scheduler.rs has 2 fewer unwrap). The ratchet RED in this PR's clippy job names only acpmux (indexing_slicing 693 > 686, string_slice 95 > 94) and cmux-host/src/cloud/github_status.rs:112. No cmux-tui-core deny lint is red.

The base branch moved (#34014 is now at 17de87c, a merge of feat). None of this PR's five files changed there after 590cb11, so the diff against the new base is the same.

Non-blocking notes

  1. No test covers the atomic arm's limits. Every budget test runs on the mutex arm. surface_worker_limit_is_mux_wide_across_connections and active_surface_request_bytes_count_toward_mux_budget (outbound_and_scheduler.rs:1020, :1028) build ServerSurfaceOperationAdmission::default() (tests.rs:450). independent_muxes_do_not_share_surface_operation_admission (:972) uses test_mux(), which CI builds with the switch off. The lane tests send only small send lines, so the atomic try_reserve_worker and both atomic refusal branches never run. An atomic arm that admitted everything would pass every test. The code is correct as I read it. Run those tests on both arms (ServerSurfaceOperationAdmission::new(lock_free)), as S1.2 does for the poison tests, before an A/B turns the switch on.
  2. The control passes on a lost key. key_wait_while_held returns Duration::MAX when a key never reaches the PTY (rds_input_lane.rs:182-185). assert_old_path_waits_for accepts that as proof that the hold reached the old path (:227-231). Also require waited < Duration::MAX, so that a lost key fails the control.
  3. The mutex counts stay pub(super). After this PR, ServerSurfaceOperationAdmission::state and ServerSurfaceOperationState::workers (worker_admission.rs:127, :114) have no reader outside worker_admission.rs. Private fields would stop later code in server from reading the mutex count while the atomic arm is on; reserved_workers() already serves the tests.
  4. The doc overstates who reserves. "Every request of every connection reserves here" (worker_admission.rs:119) is not exact: ping and plain identify (answers_before_queue), v2 resource lines and the try_handle chains in connection_frame.rs do not reach ConnectionSurfaceScheduler::dispatch. "Every request queued on a connection's surface scheduler" is exact. The key's path does reserve, so the PR's claim about the key is correct.

Base automatically changed from fix/p0-typing-s1-shared-locks to feat-cmux-next November 3, 2026 21:53
@github-actions github-actions Bot added risk:high Risk tier (docs/ci/risk-tiers.md) and removed risk:normal Risk tier (docs/ci/risk-tiers.md) labels Nov 3, 2026
Merge-main commit by scripts/merge-main.sh.
Merged by scripts/merge-main.sh: origin/feat-cmux-next at 8880dae.

Merge-main-previous-head: c4b9ddb
Merge-main-base: 8880dae
@github-actions

github-actions Bot commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

cmux status (one comment, edited in place; each line is updated by the job that owns it)

Review e2a052e: HQ feed card · gallery before/after runs · checks

App of this head (app.zip, kept 1 day)

gh workflow run reload-build.yml -R manaflow-ai/cmux --ref feat-cmux-next -f tag=pr34146 -f ref=fix/p0-typing-s1-admission -f platform=macos -f backend_mode=local

web and size checks: green

Shipped in:

@github-actions github-actions Bot added risk:normal Risk tier (docs/ci/risk-tiers.md) and removed risk:high Risk tier (docs/ci/risk-tiers.md) labels Nov 3, 2026
@blacksmith-sh

blacksmith-sh Bot commented Nov 3, 2026 •

Copy link
Copy Markdown

Found 1 test failure on Blacksmith runners:

Failure

Test View Logs
test_live_typed_catalog_matches_every_registry (main.ContractRegistryTests.test_liv
e_typed_catalog_matches_every_registry)/
test_live_typed_catalog_matches_every_registry (main.ContractRegistryTests.test_liv
e_typed_catalog_matches_every_registry)
View Logs

Fix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.

@lawrencecchen

Copy link
Copy Markdown
Contributor Author

Review (claude-reviewer-rds-s13): SHIP at e2a052e
reviewer-session: 6c5437c8-e82e-476c-885f-699e0a8d26a1
author-session: ae25c6d154f94c01a
R9: pass | no row of the layer-ownership table changes: the surface-operation budget is daemon-internal Rust state (section 1 item 1, rule L2). Its one owner stays the mux's ServerSurfaceOperationAdmission. The switch picks one arm per mux when the mux is built, and each permit releases only to the arm it reserved from, so no count gets a second owner or a copy in Swift or TS.
perf claim: none
This re-reviews only the merges with feat-cmux-next after my SHIP at 8e5c8a2 (4672321 with ff04cf6, c4b9ddb with the #34014 squash 4bb12b8, e2a052e with feat 8880dae). The PR's own code did not change, so my findings at 8e5c8a2 stay as they are.

What I checked, all read-only (no build, no test run on this machine):

  1. The PR's diff did not change. git diff 590cb119161 8e5c8a21191 and git diff 8880dae3652 e2a052e6059 have the same patch-id (stable be5d75f4106, verbatim e537fd0680a). Apart from hunk line numbers and index lines, the diff text is the same. gh pr diff 34146 (base feat-cmux-next, merge base 8880dae) also has patch-id be5d75f4106 and the same 5 files.
  2. The -s ours merge dropped nothing. The tree of c4b9ddb (dad325b36cb) is the tree of its first parent 4672321. The squash has the same content as S1.1: git diff ff04cf67b63 4bb12b86ecd and git diff 7cdc87323aa 590cb119161 both have patch-id 211b88cb40f (18 files). git diff 4bb12b86ecd c4b9ddb0eaf --name-only lists only this PR's 5 files, with patch-id be5d75f4106.
  3. No feat change interacts with the budget. Between 590cb11's base (7cdc873) and 8880dae, only the cmux-tui: the person's key takes no client registry or session lock (R-DAEMON-STATE S1 step 1, behind CMUX_TUI_RDS_INPUT) #34014 squash touched worker_admission.rs, connection_scheduler.rs, rds_input_lane.rs, outbound_and_scheduler.rs, mux.rs, mux/construct.rs or rds_switch.rs. These 7 files and 7 neighbours (terminal_create.rs, connection_wait.rs, signaled_mutex.rs, cmd_terminal_io.rs, line_connection.rs, websocket_listener.rs, server/tests.rs) are byte-identical at 590cb11 and 8880dae, and at 8e5c8a2 and e2a052e. Every try_reserve_*, ServerSurfaceOperationAdmission, ConnectionSurfaceScheduler construction, surface_operation_admission and capacity-constant site in cmux-tui/ is the same at 8e5c8a2 and e2a052e (146 sites; only line numbers moved). The other feat commits in neighbouring files add no reservation and no queue path:
  4. Evidence at e2a052e.
    • Fleet compile job 1548c3c24c244948c7b8f356 is done: CmuxNext ok (160 s), app ok (27 s), cmux-tui ok (14 s).
    • Hosted run 41728033905, test (linux): the checkout is at e2a052e, TEST_FILTER: rds_input_lane, and the result is 4 passed, 0 failed (a_key_waits_for_no_surface_admission_lock included).
    • This PR's clippy (changed cmux-tui packages) is now green (run 41727618036).
  5. The other reds are not this PR's.
    • Hosted lint (linux): workspace clippy -D warnings finished; the panic ratchet is red only on the cmux-tui crate (indexing_slicing 835 > 831, crates/cmux-tui/src/...). The PR changes only cmux-tui-core.
    • Hosted macOS: acpmux --test web_image_attachments aborts at tests/support/daemon_env.rs:464 while it lists the filter, so macOS did not run the lane tests (the same as before).
    • PR cmux-next checks, web bundles, page strings generate and release-notary: the failures are a Swift AsyncStream in FakeChromiumHosting.swift, scrollbars (12 > 10), missing settings.cloud.tunnel.* translations, the perf gate's debug.tab_switch and NightlyStepCoverageTests. The same jobs fail on feat-cmux-next at 8f405d7, which contains 8880dae (runs 41733592178 and 41733592458).

@lawrencecchen lawrencecchen added the ready-to-land Reviewed at this head; next-autoland merges it into feat-cmux-next when its only reds are base reds label Nov 4, 2026
@cmux-bot
cmux-bot Bot temporarily deployed to ci-routing-agent November 4, 2026 02:25 Inactive
@lawrencecchen
lawrencecchen merged commit dfcfac6 into feat-cmux-next Nov 4, 2026
110 of 112 checks passed
@lawrencecchen
lawrencecchen deleted the fix/p0-typing-s1-admission branch November 4, 2026 02:29
lawrencecchen added a commit that referenced this pull request Nov 4, 2026
lawrencecchen added a commit that referenced this pull request Nov 4, 2026
lawrencecchen added a commit that referenced this pull request Nov 4, 2026
lawrencecchen added a commit that referenced this pull request Nov 4, 2026
…settled newest; the input time is an atomic (R-DAEMON-STATE S1 step 5) (#34261)

* test(cmux-tui-core): the person's key waits for no shared lock (R-DAEMON-STATE S1 red)

A key line from an attached view goes through the real connection path (the
line handler, the connection's dispatcher, the send handler) while another
thread holds one shared lock for 300 ms. #31940 took Mux.state off the key;
the key still waits for the client registry lock (five takes per line), the
link peers lock, the host proxy authors lock, the surface admission lock, the
creating-input lock, the client sizing lock and the activity lock. Their other
holders are every connect, attach, resize and request of every client.
Mux.state and the workspace registry rows are controls (pass on the base).

Two keys go in a row, so a lock taken after the PTY write (sizing, activity)
shows as a delay of the second key on the connection's dispatcher.

Test-only hold hooks (cfg(test)) in creating_input.rs, terminal_sizing.rs,
activity.rs and agent_session_attach.rs. No product change.

Design: handoff/rewrites/R-DAEMON-STATE/DESIGN.md section 2.6 (stage S1).

* test(cmux-tui-core): a key waits for no client registry lock (R-DAEMON-STATE S1 step 1 red)

The previous commit's census test held nine shared locks in turn; this commit
splits it by fix step. Step 1 is the client registry lock (ClientRegistry.state),
which a key line takes about nine times (handoff checks, remote-client and
remote-device checks, the request's actor, the kept terminal, the reply's
incognito filter) while every connect, disconnect, attach and detach of every
client holds it.

The test turns on the stage switch CMUX_TUI_RDS_INPUT (rds_switch.rs, inert in
this commit) and drives two keys through the real connection path while another
thread holds the lock for 300 ms. A second test keeps Mux.state and the
workspace registry off the key on the new path (#31940).

The hold hooks of the other locks leave with this commit; they come back with
their fix steps.

* fix(cmux-tui-core): the person's key reads the client registry's published facts, never its lock (R-DAEMON-STATE S1 step 1)

A key line took the client registry lock (ClientRegistry.state) about nine
times: the daemon handoff checks, the remote-client and remote-device checks,
the request's actor, the kept terminal of the key and the reply's incognito
filter. Every connect, disconnect, attach, detach and capability change of every
client holds that lock, so an agent storm held back the person's keys (red at
2472251: 300.4 ms behind a 300 ms hold; STATUS 9131).

The registry is now a Published value (published.rs): writers still serialize
on its mutex, and a guard that changed the value publishes an immutable
ClientFacts projection (handoff state; per connection its transport, remote
device flag, base actor and kept input terminals) with arc-swap when it drops,
under the mutex, so projections follow the order of the changes. With the stage
switch CMUX_TUI_RDS_INPUT=1, the line and key paths read the projection with
one atomic load: daemon_handoff_in_progress/committed, transport_of (so
is_remote_client), is_remote_device (refuse_line, line_to_recheck,
prune_reply), connection_actor and attached_input_surface. A poisoned registry
still fails closed (the projection comes in the error, as Mutex::lock gives
its guard). With the switch off nothing changes.

A projection costs O(connections) under the registry lock per change, bounded
by the 64-connection cap; stage S2 makes it O(changed).

arc-swap 1.9 is already in the lockfile and the shipped notices (through
iroh-dns); cmux-tui-core now names it.

Tests: rds_input_lane (a key waits for no client registry lock; the session and
workspace registry locks stay off; a respawn under the same id sends the next
key to the new shell), published.rs unit tests (a change publishes on drop, a
read publishes nothing, a reader never waits for a writer, a poisoned value
fails closed). The two remote-relay poison tests poison through the new type.

* fix(cmux-tui-core): the key reads the session's published routes; one stage switch per mux (R-DAEMON-STATE S1 step 1)

The hosted run of f1ea1b6 (https://github.com/manaflow-ai/cmux/actions/runs/41655768031) still failed
both lane tests: the key waited 300 ms for the client registry and, new, for Mux.state. Two causes:

- The key still used the kept terminal of #31940. That cache is validated by process-wide route
  generations, and cargo test runs the tests of one binary as threads of one process: another test's
  mux moved the same generation slot (ids start at 1 in every mux), the kept terminal looked stale, and
  the key fell back to the locked lookup and then to remember_attached_input (registry lock). The
  stage switch was a process-wide static too.
- So the lane must not depend on a cache with process-wide invalidation at all.

Now, with CMUX_TUI_RDS_INPUT=1:
- The session's three routing tables (surfaces, terminal_catalog, terminal_catalog_by_runtime) are
  copy-on-write behind an Arc. A state guard that changed one publishes a RouteSnapshot (three Arc
  clones) with arc-swap when it drops, under the lock, so publications follow the changes; the
  snapshot it replaced is freed after the lock is released. Mux::surface reads the snapshot with one
  atomic load. A respawn, close, move, rollback or projection is seen once its guard is released, as a
  reader that waited for the lock would see it. Nothing is kept per client, so nothing is invalidated.
- input_surface on the lane: the published routes give the terminal, the registry's published facts
  say whether the client types into it (types_into: the person's input QoS mark). attached_input_surface
  and remember_attached_input stay only on the old path.
- The switch is per mux (rds_switch::InputLaneSwitch, read once from the environment when the mux is
  built; Mux::set_input_lane_for_test in tests), and it also decides whether anything is published: off,
  the registry builds no projection and the state holds no route snapshot, so a route write never
  copies a shared table and the off arm costs what the base costs.

Cost on the lane: a guard's first route write copies that table once (O(routes)), bounded by one copy
per changing guard; State::clone no longer deep-copies the three tables. Stage S2 makes both O(log n).

* test(cmux-tui-core): a key waits for no surface admission lock, with an old-path control (R-DAEMON-STATE S1 step 3 red)

Every request of every connection reserves its retained bytes from one
daemon-wide budget (ServerSurfaceOperationAdmission) under one mutex before it
is queued, and releases them under it after it ran. The S1 census measured a
key waiting 300.3 ms behind a 300 ms hold of that lock (hosted run 41648095321).

The lane test now has a control: the same two keys with stage S1 off must
wait for the held lock, so a lane result can never come from a hold that does
not reach the key path (a vacuous green). The control is added to the client
registry test too. A test-only hook (cfg(test)) holds the admission lock.

Red here: with the lane on, the key still reserves through the mutex.

* fix(cmux-tui-core): the surface admission budget reserves with atomics on the input lane (R-DAEMON-STATE S1 step 3)

Every request of every connection reserves its retained bytes (and every
clear-history its worker) from one daemon-wide budget before it is queued and
releases after it ran. Under one mutex, a key waited for any other
connection's reservation or release (red at 5d0e4a0: 300 ms behind a
300 ms hold).

With CMUX_TUI_RDS_INPUT=1 the two counters are atomics with compare-and-swap
reservations (fetch_update: add only while the sum stays within the capacity;
release saturates at zero), so nothing waits. The limits and the refusals are
the same as the mutex arm's: a worker is refused at 16 reserved, bytes when
the sum would pass 16 MiB. Each permit releases to the arm it reserved from.
With the switch off the mutex arm runs as before; it goes with the switch when
the stage passes (S1.8). The arm is fixed when the mux is built
(ServerSurfaceOperationAdmission::new(publish_routes)); tests set it with
Mux::set_input_lane_for_test before any request.

The two scheduler tests that counted reserved workers through the mutex read
reserved_workers() (both arms).

* test(cmux-tui-core): a key waits for neither the link peers nor the proxy authors lock (R-DAEMON-STATE S1 step 2 red)

Two more shared locks on the person's key line (DESIGN.md Appendix A.2 steps 3, 7, 12 and 13): the link peers map
(RemoteRelayState.peers: the remote-client check and the request's actor) and the host proxy authors
(AgentSessions.proxy_authors: the proxy-link check on the reader thread). The S1 census measured a full 300 ms wait
on each (hosted run 41648095321: 300.5 ms and 300.4 ms).

The test holds each lock 300 ms on another thread while two keys go through the real line path. It first proves
the hold reaches the old path (stage switch off: the key waits), then expects the stage's path not to wait. Red on
this commit: the stage does not publish either value yet.

* test(cmux-tui-core): a change cut short by a panic is never published (R-DAEMON-STATE S1 red)

A published value's guard publishes its projection in Drop::drop, which also runs while a panic unwinds, before
its MutexGuard field drops and marks the mutex poisoned. A writer that panics in the middle of a change so
publishes the half-made value, and in the moment between the publication and the poisoning a reader gets it as a
good value: a fail-open window for readers that fail closed on a poisoned value (transport_of, peer_checked).

Red on this commit: the test sees the half-made value 9 in the poisoned projection.

* test(cmux-tui-core): a reservation unshares a published route map; a panicked change publishes no routes (R-DAEMON-STATE S1 red)

Two defects the #34014 review found on the input lane (both only with CMUX_TUI_RDS_INPUT on):

- RouteTable::try_reserve returns early when the map has room, also when a published route snapshot shares the
  map. The insert after the durable commit then copies the whole table (Arc::make_mut), an allocation that
  ResourceMutationPlan::apply forbids ("Plan builders reserve all needed capacities before returning").
- The session state guard publishes the routes of a change that a panic cut short: Drop::drop runs during the
  unwind, before the MutexGuard field poisons the mutex, so the lane routes by half-made tables.

Red on this commit: the insert changes the map's pointer, and the routes snapshot is replaced.

* fix(cmux-tui-core): link peers and host proxy authors are published; the S1.1 review follow-ups (R-DAEMON-STATE S1 step 2)

A person's key line asked two more shared locks (DESIGN.md Appendix A.2 steps 3, 7, 12, 13): the link peers map
(the remote-client check and the request's actor) and the host proxy authors (the proxy-link check on the reader
thread). Every remote bind, disconnect and revocation sweep holds the first; every proxy bind and every client's
disconnect holds the second.

- published.rs moves to cmux-tui-util (daemon primitives, no daemon state) so cmux-tui-remote-access can use it.
  RemoteRelayState.peers becomes a Published copy of the records; peer_checked reads it with no lock when the
  mux's stage switch is on (Published::publishing), and a poisoned map still fails closed. AgentSessions.proxy_authors
  publishes the same way; proxy::author reads it. A disconnect of a client without a record publishes nothing.
- Review follow-ups of #34014 (review comment 6612546773):
  - RouteTable::try_reserve copies a map a published snapshot shares, before the durable commit, so the insert
    after the commit allocates nothing (ResourceMutationPlan::apply).
  - A change a panic cuts short is never published, by the state guard (routes) or by a PublishGuard: Drop skips
    the publication when a panic began while the guard held the lock (std's own poisoning test), so no reader
    sees half-made tables, not even before the mutex is poisoned.
  - A PublishGuard frees the projection it replaced after the unlock (as the state guard does).
  - The two poison tests of the relay run on both arms.
  - ClientFacts and types_into move to server/client_registry/facts.rs (god-file warning).

Same switch, CMUX_TUI_RDS_INPUT, default off: with it off nothing is published and every read takes its lock as
on the base.

* test(cmux-tui-core): a key waits for no creating-input lock, and for no session lock behind one (R-DAEMON-STATE S1 step 4 red)

Every key to a live terminal first asks whether creating input of its terminal must go first
(wait_for_creating_input_flush, DESIGN.md Appendix A.2 step 16). It takes the daemon-wide creating-input lock even
when no terminal is being created, and that lock's holders (begin_creating_input, knows_terminal_input_target,
finish_creating_input) look up the live terminal under it, which takes the session lock. The S1 census measured a
full 300 ms wait on the creating-input lock (hosted run 41648095321: 300.3 ms).

Two holders, each with an old-path control: the creating-input lock itself, and the session lock held while a
reader waits for it under the creating-input lock (with another terminal being created, so the key cannot skip the
lock). Red on this commit: the stage does not change these paths yet.

* fix(cmux-tui-core): a key skips the creating-input lock while nothing is creating; the lock becomes a leaf (R-DAEMON-STATE S1 step 4)

On the input lane (CMUX_TUI_RDS_INPUT, per mux, default off):

- CreatingInputs keeps the number of creating terminals in an atomic, stored under its lock after every insert and
  removal (Release). A key reads it (Acquire) and returns at once when it is zero: no queue can be ahead of it,
  because a terminal's queue is added before the terminal goes live and removed only after every queued byte was
  written to it.
- The creating-input lock's holders look up the live terminal in the published routes instead of under the session
  lock, so no holder waits for the session lock while it holds the creating-input lock, and a key that takes it
  never waits behind one. The routes carry the host-id catalog index for this: terminal_catalog_by_host becomes a
  copy-on-write route table in the RouteSnapshot, and one lookup (route_table::live_terminal) serves the session
  state and the snapshot. A commit publishes its routes before it releases the session lock, and finish runs after
  the commit, so begin, knows and finish give the answers they gave under the session lock.

Off, every path takes the locks it took on the base; the by-host index is copy-on-write on both arms, as the other
three route tables are since step 1.

* test(cmux-tui-core): the surface budget's limits on both arms; a lock control fails on a lost key (#34146 review follow-ups)

The #34146 review (comment 6613345630) noted:
- Every budget test used the mutex arm, so an atomic arm that admitted everything would have passed. The two
  mux-wide limit tests now run on both arms, and a new test checks the same limits on both: the 17th worker, the
  whole byte budget, one byte over, zero bytes, a sum that overflows, and released reservations coming back.
- The old-path control passed when a key never arrived (Duration::MAX >= BUDGET); it now fails on a lost key.
- ServerSurfaceOperationState and the admission's mutex are private (nothing outside the file reads them), and
  the doc says which requests reserve (pings, identify, v2 resource lines and reader-thread frame handlers do not).

* fix(cmux-tui-core): drop an unnecessary path qualification in a test (clippy unused_qualifications)

The lint (linux) job's clippy -D warnings failed on mux/tests/signaled_mutex.rs:120 (TerminalPublicId is in scope);
the #34174 review found it behind the base's acpmux red in the same job.

* test(cmux-tui-core): a poisoned session state never routes by the published tables (R-DAEMON-STATE S1 red)

The #34171 review noted: after a panic poisons Mux::state, the next guard that recovers the poison may publish the
half-made route tables, and the lane's Mux::surface reads the published routes with no poison check, where the old
path fails (it unwraps the lock). Red on this commit: with the lane on, a surface read after the poisoning returns
from the published routes instead of failing.

The relay poison tests now poison published values through a helper in the test file.

* fix(cmux-tui-core): the lane's surface read takes the old path on a poisoned state; the poison helper is test code (#34171 review)

Review comment 6614044759 (FIX at 8f7d405):
- published.rs: poison_for_test moved out of the test module with its panic! respelled as panic_any, which hides a
  hit from the crash ratchet's count. It is test code again: a private helper in the published tests and one in
  the relay tests, with plain panic!.
- Mux::surface on the lane reads the published routes only while the session state is not poisoned; a poisoned
  state takes the old read and fails as it does (StateMutex::is_poisoned). A guard that recovers the poison may
  publish the half-made tables of the change that panicked.
- Published::load debug-asserts that the value publishes: only a publishing value has a current projection.
- The module doc says what load gives on a poisoned value; the caller decides whether to fail closed.

The unused_qualifications error at mux/tests/signaled_mutex.rs:120 is fixed in 10f1669.

* fix(cmux-tui-core): the creating-input live check reads the routes only on an unpoisoned state; its comment states the lane's linearization (#34174 review)

Review comment 6613998735 (SHIP, notes): the begin_creating_input comment still said the check takes the state
under the creating lock; on the lane it reads the published routes, and its answer is one the old path can give
(a commit in progress has not made the id live, as when the old path takes the state before it). The lane's live
lookup now takes the old read on a poisoned state, as Mux::surface does since bb0265f.

* test(cmux-tui-core): a key waits for neither the sizing nor the activity lock; a key takes the grid back after another view's click (R-DAEMON-STATE S1 step 5 red)

After its PTY write a key notes activity in the terminal's sizing (the newest view sets the grid under the default
policy): it takes the daemon-wide client sizing lock twice (the note and the size-state publication), then the
VM activity stream's lock. Every attach, detach, resize and size report of every terminal holds the sizing lock,
and a PTY resize runs under it. The S1 census measured a full 300 ms wait on each (hosted run 41648095321: 300.2
and 300.3 ms).

The first test holds each lock 300 ms (with an old-path control each). The second keeps the fix honest: two views
of one terminal, the default policy; the other view's focus click takes the grid, and the next key from the
typing view must take it back, on both arms. Red on this commit: the first test (the stage does not change these
paths yet).

* fix(cmux-tui-util): a test dereferences the published u64 once (E0614 in bb0265f)

* fix(cmux-tui-core): a key skips its sizing note when its view is the settled newest; the person's input time is an atomic (R-DAEMON-STATE S1 step 5)

On the input lane (CMUX_TUI_RDS_INPUT, per mux, default off):

- Each terminal's sizing entry publishes, on the terminal's runtime (shared by every placement), the view a key may
  skip its sizing note for: the sole newest participant, a client's own view, only while the entry is settled (no
  grid hold, no handheld with a viewport, and the engine holds the grid or the applied grid and participants are its
  decision). Noting activity for that view changes no decision under any policy, and the size state carries no
  activity, so the key compares (client, placement) with one atomic load and takes no sizing lock.
- The publication is automatic: Mux::client_sizing is a SizingMutex whose guard republishes, on drop and under the
  lock, each entry its holder read or wrote (SizingEntries marks them; an apply changes Cells through a shared
  borrow, so reads count). A removed entry clears its slot; a change a panic cuts short publishes nothing.
- Another view's activity makes that view the newest and republishes, so the next key from the first view takes the
  slow path once and takes the grid back (the second test pins this on both arms).
- ActivityStream::note_user_input stores the time in an atomic; it takes the stream lock only while a subscriber
  exists, once per event the worker sends (a wake_pending flag the worker clears before it reads the time).
- TerminalSizingEngine::sole_latest (cmux-terminal-sizing) with a unit test; AppliedSeat::of shares the seat rule
  between the apply and the settled check; set_workspace_size_policy reads its state before the note (no index).

Off, every key takes the sizing and activity locks as on the base; the marks and the guard check one bool.

* fix(cmux-tui-core): the sizing test hook went in above another test hook's cfg(test) (dead code and 2 expect hits in production)

dfe81bf put hold_client_sizing_for_test between pin_latest_size_policy_for_test's doc and its #[cfg(test)], so
pin_latest_size_policy_for_test compiled into production: clippy -D warnings dead_code, and the panic ratchet's
cmux-tui-core expect_used 175 > 173 (hosted lint job 126202464363).

* fix(cmux-tui-core): the lane's surface read loads the routes before its poison check; two test doc comments on their items (#34171 review 2)

Review comment 6615927915 (SHIP at 0fb7c43, notes): Mux::surface checked is_poisoned() and then loaded the
routes; between the two another thread could poison the state and a third, recovering the poison, publish half-made
tables. Poison never clears on the session state, so routes loaded before an unpoisoned check were published before
any poisoning: load first, then check. Two doc comments in remote_relay/tests.rs sat above the wrong item.

* test(cmux-tui-core): after a respawn, keys skip the sizing lock again; the lock tests report a viewport (#34261 review red)

Review comment 6619473751 (FIX at 846d412):
1. A respawn under the same runtime id keeps the sizing entry, which keeps the dead runtime's fast-path slot: the
   new runtime's slot is never published, so every later key takes Mux::client_sizing twice. The new test respawns
   the typing terminal, sends one key, then holds the sizing lock: the next keys must not wait. Red on this
   commit.
2. The lock tests' view never reported a size, so the engine held the grid and the skip went only through that
   branch. The typing view now reports 100x30 as the app's view does, so its keys skip through the settled-grid
   check.

* fix(cmux-tui-core): a sizing entry follows its terminal across a respawn; the input time never goes back (#34261 review)

Review comment 6619473751 (FIX at 846d412):
1. A respawn under the same runtime id keeps the sizing entry but builds a new terminal runtime with a new
   fast-path slot. TerminalSizingEntry::follow_runtime_of re-points the entry at the slot of the runtime its keys
   reach, under the sizing lock, and clears the old slot: on the key's slow path (note_terminal_activity_of has the
   key's surface) and on every terminal_sizing_entry call (attach, claim, size report). The red test
   after_a_respawn_keys_wait_for_no_sizing_lock_again (4bf5af5) pins it.
3. The entry takes or follows a slot only from a surface whose runtime id is the entry's, so two entries never
   share one slot.
4. The lock-free input time is fetch_max, so a key preempted after its clock read never moves it back.
7. The poison comment says what happens: a later holder that recovers the poison republishes what it touches.

Lock order is unchanged: terminal_sizing_entry now looks the surface up on each call (it did at creation, and
apply_terminal_grid does), under the sizing lock, the established sizing-then-session order.

* test(cmux-tui-core): a settled size-state read takes the session lock once with the switch off (#34261 review 2 red)

The sizing entry looks its surface up on every access since 5fe770d, so with the switch off a size-state read
takes the session lock twice (once under the sizing lock): expected red at this commit (lane off: 2, not 1).

* fix(cmux-tui-core): a sizing entry looks its surface up only when it is made; the respawn follow runs only on the lane (#34261 review 2)

5fe770d looked the surface up on every sizing-entry access, so with the switch off (the default) every attach,
size report, claim and size-state read took the session lock under the sizing lock. The surface is looked up only to
make the entry again; a respawn's new runtime is followed by its first key's sizing note, which holds the surface,
and only while the switch is on. An unchanged fast path is not stored again. The respawn test pins the old slot's
clear.

---------

Co-authored-by: cmux-bot[bot] <348531757+cmux-bot[bot]@users.noreply.github.com>
lawrencecchen added a commit that referenced this pull request Nov 4, 2026
…iter (R-DAEMON-STATE S1.6a, input port, default off) (#34544)

* test(cmux-tui-core): the person's key waits for no shared lock (R-DAEMON-STATE S1 red)

A key line from an attached view goes through the real connection path (the
line handler, the connection's dispatcher, the send handler) while another
thread holds one shared lock for 300 ms. #31940 took Mux.state off the key;
the key still waits for the client registry lock (five takes per line), the
link peers lock, the host proxy authors lock, the surface admission lock, the
creating-input lock, the client sizing lock and the activity lock. Their other
holders are every connect, attach, resize and request of every client.
Mux.state and the workspace registry rows are controls (pass on the base).

Two keys go in a row, so a lock taken after the PTY write (sizing, activity)
shows as a delay of the second key on the connection's dispatcher.

Test-only hold hooks (cfg(test)) in creating_input.rs, terminal_sizing.rs,
activity.rs and agent_session_attach.rs. No product change.

Design: handoff/rewrites/R-DAEMON-STATE/DESIGN.md section 2.6 (stage S1).

* test(cmux-tui-core): a key waits for no client registry lock (R-DAEMON-STATE S1 step 1 red)

The previous commit's census test held nine shared locks in turn; this commit
splits it by fix step. Step 1 is the client registry lock (ClientRegistry.state),
which a key line takes about nine times (handoff checks, remote-client and
remote-device checks, the request's actor, the kept terminal, the reply's
incognito filter) while every connect, disconnect, attach and detach of every
client holds it.

The test turns on the stage switch CMUX_TUI_RDS_INPUT (rds_switch.rs, inert in
this commit) and drives two keys through the real connection path while another
thread holds the lock for 300 ms. A second test keeps Mux.state and the
workspace registry off the key on the new path (#31940).

The hold hooks of the other locks leave with this commit; they come back with
their fix steps.

* fix(cmux-tui-core): the person's key reads the client registry's published facts, never its lock (R-DAEMON-STATE S1 step 1)

A key line took the client registry lock (ClientRegistry.state) about nine
times: the daemon handoff checks, the remote-client and remote-device checks,
the request's actor, the kept terminal of the key and the reply's incognito
filter. Every connect, disconnect, attach, detach and capability change of every
client holds that lock, so an agent storm held back the person's keys (red at
2472251: 300.4 ms behind a 300 ms hold; STATUS 9131).

The registry is now a Published value (published.rs): writers still serialize
on its mutex, and a guard that changed the value publishes an immutable
ClientFacts projection (handoff state; per connection its transport, remote
device flag, base actor and kept input terminals) with arc-swap when it drops,
under the mutex, so projections follow the order of the changes. With the stage
switch CMUX_TUI_RDS_INPUT=1, the line and key paths read the projection with
one atomic load: daemon_handoff_in_progress/committed, transport_of (so
is_remote_client), is_remote_device (refuse_line, line_to_recheck,
prune_reply), connection_actor and attached_input_surface. A poisoned registry
still fails closed (the projection comes in the error, as Mutex::lock gives
its guard). With the switch off nothing changes.

A projection costs O(connections) under the registry lock per change, bounded
by the 64-connection cap; stage S2 makes it O(changed).

arc-swap 1.9 is already in the lockfile and the shipped notices (through
iroh-dns); cmux-tui-core now names it.

Tests: rds_input_lane (a key waits for no client registry lock; the session and
workspace registry locks stay off; a respawn under the same id sends the next
key to the new shell), published.rs unit tests (a change publishes on drop, a
read publishes nothing, a reader never waits for a writer, a poisoned value
fails closed). The two remote-relay poison tests poison through the new type.

* fix(cmux-tui-core): the key reads the session's published routes; one stage switch per mux (R-DAEMON-STATE S1 step 1)

The hosted run of f1ea1b6 (https://github.com/manaflow-ai/cmux/actions/runs/41655768031) still failed
both lane tests: the key waited 300 ms for the client registry and, new, for Mux.state. Two causes:

- The key still used the kept terminal of #31940. That cache is validated by process-wide route
  generations, and cargo test runs the tests of one binary as threads of one process: another test's
  mux moved the same generation slot (ids start at 1 in every mux), the kept terminal looked stale, and
  the key fell back to the locked lookup and then to remember_attached_input (registry lock). The
  stage switch was a process-wide static too.
- So the lane must not depend on a cache with process-wide invalidation at all.

Now, with CMUX_TUI_RDS_INPUT=1:
- The session's three routing tables (surfaces, terminal_catalog, terminal_catalog_by_runtime) are
  copy-on-write behind an Arc. A state guard that changed one publishes a RouteSnapshot (three Arc
  clones) with arc-swap when it drops, under the lock, so publications follow the changes; the
  snapshot it replaced is freed after the lock is released. Mux::surface reads the snapshot with one
  atomic load. A respawn, close, move, rollback or projection is seen once its guard is released, as a
  reader that waited for the lock would see it. Nothing is kept per client, so nothing is invalidated.
- input_surface on the lane: the published routes give the terminal, the registry's published facts
  say whether the client types into it (types_into: the person's input QoS mark). attached_input_surface
  and remember_attached_input stay only on the old path.
- The switch is per mux (rds_switch::InputLaneSwitch, read once from the environment when the mux is
  built; Mux::set_input_lane_for_test in tests), and it also decides whether anything is published: off,
  the registry builds no projection and the state holds no route snapshot, so a route write never
  copies a shared table and the off arm costs what the base costs.

Cost on the lane: a guard's first route write copies that table once (O(routes)), bounded by one copy
per changing guard; State::clone no longer deep-copies the three tables. Stage S2 makes both O(log n).

* test(cmux-tui-core): a key waits for no surface admission lock, with an old-path control (R-DAEMON-STATE S1 step 3 red)

Every request of every connection reserves its retained bytes from one
daemon-wide budget (ServerSurfaceOperationAdmission) under one mutex before it
is queued, and releases them under it after it ran. The S1 census measured a
key waiting 300.3 ms behind a 300 ms hold of that lock (hosted run 41648095321).

The lane test now has a control: the same two keys with stage S1 off must
wait for the held lock, so a lane result can never come from a hold that does
not reach the key path (a vacuous green). The control is added to the client
registry test too. A test-only hook (cfg(test)) holds the admission lock.

Red here: with the lane on, the key still reserves through the mutex.

* fix(cmux-tui-core): the surface admission budget reserves with atomics on the input lane (R-DAEMON-STATE S1 step 3)

Every request of every connection reserves its retained bytes (and every
clear-history its worker) from one daemon-wide budget before it is queued and
releases after it ran. Under one mutex, a key waited for any other
connection's reservation or release (red at 5d0e4a0: 300 ms behind a
300 ms hold).

With CMUX_TUI_RDS_INPUT=1 the two counters are atomics with compare-and-swap
reservations (fetch_update: add only while the sum stays within the capacity;
release saturates at zero), so nothing waits. The limits and the refusals are
the same as the mutex arm's: a worker is refused at 16 reserved, bytes when
the sum would pass 16 MiB. Each permit releases to the arm it reserved from.
With the switch off the mutex arm runs as before; it goes with the switch when
the stage passes (S1.8). The arm is fixed when the mux is built
(ServerSurfaceOperationAdmission::new(publish_routes)); tests set it with
Mux::set_input_lane_for_test before any request.

The two scheduler tests that counted reserved workers through the mutex read
reserved_workers() (both arms).

* test(cmux-tui-core): a key waits for neither the link peers nor the proxy authors lock (R-DAEMON-STATE S1 step 2 red)

Two more shared locks on the person's key line (DESIGN.md Appendix A.2 steps 3, 7, 12 and 13): the link peers map
(RemoteRelayState.peers: the remote-client check and the request's actor) and the host proxy authors
(AgentSessions.proxy_authors: the proxy-link check on the reader thread). The S1 census measured a full 300 ms wait
on each (hosted run 41648095321: 300.5 ms and 300.4 ms).

The test holds each lock 300 ms on another thread while two keys go through the real line path. It first proves
the hold reaches the old path (stage switch off: the key waits), then expects the stage's path not to wait. Red on
this commit: the stage does not publish either value yet.

* test(cmux-tui-core): a change cut short by a panic is never published (R-DAEMON-STATE S1 red)

A published value's guard publishes its projection in Drop::drop, which also runs while a panic unwinds, before
its MutexGuard field drops and marks the mutex poisoned. A writer that panics in the middle of a change so
publishes the half-made value, and in the moment between the publication and the poisoning a reader gets it as a
good value: a fail-open window for readers that fail closed on a poisoned value (transport_of, peer_checked).

Red on this commit: the test sees the half-made value 9 in the poisoned projection.

* test(cmux-tui-core): a reservation unshares a published route map; a panicked change publishes no routes (R-DAEMON-STATE S1 red)

Two defects the #34014 review found on the input lane (both only with CMUX_TUI_RDS_INPUT on):

- RouteTable::try_reserve returns early when the map has room, also when a published route snapshot shares the
  map. The insert after the durable commit then copies the whole table (Arc::make_mut), an allocation that
  ResourceMutationPlan::apply forbids ("Plan builders reserve all needed capacities before returning").
- The session state guard publishes the routes of a change that a panic cut short: Drop::drop runs during the
  unwind, before the MutexGuard field poisons the mutex, so the lane routes by half-made tables.

Red on this commit: the insert changes the map's pointer, and the routes snapshot is replaced.

* fix(cmux-tui-core): link peers and host proxy authors are published; the S1.1 review follow-ups (R-DAEMON-STATE S1 step 2)

A person's key line asked two more shared locks (DESIGN.md Appendix A.2 steps 3, 7, 12, 13): the link peers map
(the remote-client check and the request's actor) and the host proxy authors (the proxy-link check on the reader
thread). Every remote bind, disconnect and revocation sweep holds the first; every proxy bind and every client's
disconnect holds the second.

- published.rs moves to cmux-tui-util (daemon primitives, no daemon state) so cmux-tui-remote-access can use it.
  RemoteRelayState.peers becomes a Published copy of the records; peer_checked reads it with no lock when the
  mux's stage switch is on (Published::publishing), and a poisoned map still fails closed. AgentSessions.proxy_authors
  publishes the same way; proxy::author reads it. A disconnect of a client without a record publishes nothing.
- Review follow-ups of #34014 (review comment 6612546773):
  - RouteTable::try_reserve copies a map a published snapshot shares, before the durable commit, so the insert
    after the commit allocates nothing (ResourceMutationPlan::apply).
  - A change a panic cuts short is never published, by the state guard (routes) or by a PublishGuard: Drop skips
    the publication when a panic began while the guard held the lock (std's own poisoning test), so no reader
    sees half-made tables, not even before the mutex is poisoned.
  - A PublishGuard frees the projection it replaced after the unlock (as the state guard does).
  - The two poison tests of the relay run on both arms.
  - ClientFacts and types_into move to server/client_registry/facts.rs (god-file warning).

Same switch, CMUX_TUI_RDS_INPUT, default off: with it off nothing is published and every read takes its lock as
on the base.

* test(cmux-tui-core): a key waits for no creating-input lock, and for no session lock behind one (R-DAEMON-STATE S1 step 4 red)

Every key to a live terminal first asks whether creating input of its terminal must go first
(wait_for_creating_input_flush, DESIGN.md Appendix A.2 step 16). It takes the daemon-wide creating-input lock even
when no terminal is being created, and that lock's holders (begin_creating_input, knows_terminal_input_target,
finish_creating_input) look up the live terminal under it, which takes the session lock. The S1 census measured a
full 300 ms wait on the creating-input lock (hosted run 41648095321: 300.3 ms).

Two holders, each with an old-path control: the creating-input lock itself, and the session lock held while a
reader waits for it under the creating-input lock (with another terminal being created, so the key cannot skip the
lock). Red on this commit: the stage does not change these paths yet.

* fix(cmux-tui-core): a key skips the creating-input lock while nothing is creating; the lock becomes a leaf (R-DAEMON-STATE S1 step 4)

On the input lane (CMUX_TUI_RDS_INPUT, per mux, default off):

- CreatingInputs keeps the number of creating terminals in an atomic, stored under its lock after every insert and
  removal (Release). A key reads it (Acquire) and returns at once when it is zero: no queue can be ahead of it,
  because a terminal's queue is added before the terminal goes live and removed only after every queued byte was
  written to it.
- The creating-input lock's holders look up the live terminal in the published routes instead of under the session
  lock, so no holder waits for the session lock while it holds the creating-input lock, and a key that takes it
  never waits behind one. The routes carry the host-id catalog index for this: terminal_catalog_by_host becomes a
  copy-on-write route table in the RouteSnapshot, and one lookup (route_table::live_terminal) serves the session
  state and the snapshot. A commit publishes its routes before it releases the session lock, and finish runs after
  the commit, so begin, knows and finish give the answers they gave under the session lock.

Off, every path takes the locks it took on the base; the by-host index is copy-on-write on both arms, as the other
three route tables are since step 1.

* test(cmux-tui-core): the surface budget's limits on both arms; a lock control fails on a lost key (#34146 review follow-ups)

The #34146 review (comment 6613345630) noted:
- Every budget test used the mutex arm, so an atomic arm that admitted everything would have passed. The two
  mux-wide limit tests now run on both arms, and a new test checks the same limits on both: the 17th worker, the
  whole byte budget, one byte over, zero bytes, a sum that overflows, and released reservations coming back.
- The old-path control passed when a key never arrived (Duration::MAX >= BUDGET); it now fails on a lost key.
- ServerSurfaceOperationState and the admission's mutex are private (nothing outside the file reads them), and
  the doc says which requests reserve (pings, identify, v2 resource lines and reader-thread frame handlers do not).

* fix(cmux-tui-core): drop an unnecessary path qualification in a test (clippy unused_qualifications)

The lint (linux) job's clippy -D warnings failed on mux/tests/signaled_mutex.rs:120 (TerminalPublicId is in scope);
the #34174 review found it behind the base's acpmux red in the same job.

* test(cmux-tui-core): a poisoned session state never routes by the published tables (R-DAEMON-STATE S1 red)

The #34171 review noted: after a panic poisons Mux::state, the next guard that recovers the poison may publish the
half-made route tables, and the lane's Mux::surface reads the published routes with no poison check, where the old
path fails (it unwraps the lock). Red on this commit: with the lane on, a surface read after the poisoning returns
from the published routes instead of failing.

The relay poison tests now poison published values through a helper in the test file.

* fix(cmux-tui-core): the lane's surface read takes the old path on a poisoned state; the poison helper is test code (#34171 review)

Review comment 6614044759 (FIX at 8f7d405):
- published.rs: poison_for_test moved out of the test module with its panic! respelled as panic_any, which hides a
  hit from the crash ratchet's count. It is test code again: a private helper in the published tests and one in
  the relay tests, with plain panic!.
- Mux::surface on the lane reads the published routes only while the session state is not poisoned; a poisoned
  state takes the old read and fails as it does (StateMutex::is_poisoned). A guard that recovers the poison may
  publish the half-made tables of the change that panicked.
- Published::load debug-asserts that the value publishes: only a publishing value has a current projection.
- The module doc says what load gives on a poisoned value; the caller decides whether to fail closed.

The unused_qualifications error at mux/tests/signaled_mutex.rs:120 is fixed in 10f1669.

* fix(cmux-tui-core): the creating-input live check reads the routes only on an unpoisoned state; its comment states the lane's linearization (#34174 review)

Review comment 6613998735 (SHIP, notes): the begin_creating_input comment still said the check takes the state
under the creating lock; on the lane it reads the published routes, and its answer is one the old path can give
(a commit in progress has not made the id live, as when the old path takes the state before it). The lane's live
lookup now takes the old read on a poisoned state, as Mux::surface does since bb0265f.

* test(cmux-tui-core): a key waits for neither the sizing nor the activity lock; a key takes the grid back after another view's click (R-DAEMON-STATE S1 step 5 red)

After its PTY write a key notes activity in the terminal's sizing (the newest view sets the grid under the default
policy): it takes the daemon-wide client sizing lock twice (the note and the size-state publication), then the
VM activity stream's lock. Every attach, detach, resize and size report of every terminal holds the sizing lock,
and a PTY resize runs under it. The S1 census measured a full 300 ms wait on each (hosted run 41648095321: 300.2
and 300.3 ms).

The first test holds each lock 300 ms (with an old-path control each). The second keeps the fix honest: two views
of one terminal, the default policy; the other view's focus click takes the grid, and the next key from the
typing view must take it back, on both arms. Red on this commit: the first test (the stage does not change these
paths yet).

* fix(cmux-tui-util): a test dereferences the published u64 once (E0614 in bb0265f)

* fix(cmux-tui-core): a key skips its sizing note when its view is the settled newest; the person's input time is an atomic (R-DAEMON-STATE S1 step 5)

On the input lane (CMUX_TUI_RDS_INPUT, per mux, default off):

- Each terminal's sizing entry publishes, on the terminal's runtime (shared by every placement), the view a key may
  skip its sizing note for: the sole newest participant, a client's own view, only while the entry is settled (no
  grid hold, no handheld with a viewport, and the engine holds the grid or the applied grid and participants are its
  decision). Noting activity for that view changes no decision under any policy, and the size state carries no
  activity, so the key compares (client, placement) with one atomic load and takes no sizing lock.
- The publication is automatic: Mux::client_sizing is a SizingMutex whose guard republishes, on drop and under the
  lock, each entry its holder read or wrote (SizingEntries marks them; an apply changes Cells through a shared
  borrow, so reads count). A removed entry clears its slot; a change a panic cuts short publishes nothing.
- Another view's activity makes that view the newest and republishes, so the next key from the first view takes the
  slow path once and takes the grid back (the second test pins this on both arms).
- ActivityStream::note_user_input stores the time in an atomic; it takes the stream lock only while a subscriber
  exists, once per event the worker sends (a wake_pending flag the worker clears before it reads the time).
- TerminalSizingEngine::sole_latest (cmux-terminal-sizing) with a unit test; AppliedSeat::of shares the seat rule
  between the apply and the settled check; set_workspace_size_policy reads its state before the note (no index).

Off, every key takes the sizing and activity locks as on the base; the marks and the guard check one bool.

* fix(cmux-tui-core): the sizing test hook went in above another test hook's cfg(test) (dead code and 2 expect hits in production)

dfe81bf put hold_client_sizing_for_test between pin_latest_size_policy_for_test's doc and its #[cfg(test)], so
pin_latest_size_policy_for_test compiled into production: clippy -D warnings dead_code, and the panic ratchet's
cmux-tui-core expect_used 175 > 173 (hosted lint job 126202464363).

* fix(cmux-tui-core): the lane's surface read loads the routes before its poison check; two test doc comments on their items (#34171 review 2)

Review comment 6615927915 (SHIP at 0fb7c43, notes): Mux::surface checked is_poisoned() and then loaded the
routes; between the two another thread could poison the state and a third, recovering the poison, publish half-made
tables. Poison never clears on the session state, so routes loaded before an unpoisoned check were published before
any poisoning: load first, then check. Two doc comments in remote_relay/tests.rs sat above the wrong item.

* test(cmux-tui-core): a key does not wait for another writer's blocked host socket (R-DAEMON-STATE S1 step 6 red)

A key to a hosted terminal holds the terminal's runtime lock and its HostAttachment.writer across a blocking write
to the host socket (surface/input.rs, attachment.rs send_host_frame). Another client's large paste to the same
terminal holds both for its whole write, bounded only by the socket's send timeout, so the person's key waits for
automation's work on the same terminal (DESIGN.md section 2.6.1, Appendix A.2 steps 20-22).

The test pastes 8 MiB while the fake host reads nothing for 600 ms, then sends one key. A control first proves the
old path waits (the key takes at least half the stall); the lane must return the key in < 100 ms, and the host must
then read the paste before the key, and the key once. Red on this commit: the lane has no input port yet.

* fix(cmux-tui-core): a person's key to a hosted terminal goes to the terminal's input port and never waits for a socket write (R-DAEMON-STATE S1 step 6)

On the input lane (CMUX_TUI_RDS_INPUT, per mux, default off), interactive input to a hosted terminal
(Surface::write_bytes, Surface::write_paste) goes to the terminal's own input port (surface/input_port.rs): one
queue per terminal, drained in order by the terminal's writer thread (terminal-input-port, started on the first
input, ended after 30 s idle). The request returns once the port took the bytes; it takes neither the terminal's
runtime lock nor the host writer, so another writer of the same terminal (a large paste, receipted input, a
cell-pixel change waiting for its acknowledgement) and a host that does not read its socket no longer hold the
person's key or the connection's dispatcher.

- The writer writes on the route of the current host connection (HostInputRoute: the connection's writer), which
  the terminal publishes whenever its runtime changes: the hosted and exited constructors, and
  PtyTerminalRuntime::replace_runtime, which the reconnect install and the host exit now use. So the writer never
  takes the runtime lock either.
- A lost connection is retried as write_bytes did (cx-6so.49), on the port's thread: wait for the reader's next
  stream change, write to the new host, until HOST_INPUT_RECONNECT_WAIT.
- A full port (32 MiB queued) refuses at once (terminal.input_busy, WouldBlock); a write larger than one host frame
  is refused as before; the exited final screen drops input as before.
- Input the port took and then could not write is lost as a failed write was, with a log line: the request was
  already answered (the reply now means "accepted by the terminal's port", DESIGN.md 2.6.1).
- Not in this step (S1.6b, before the default flip): the connection's other frames still write directly, so while
  the port is behind they can go before a key it still holds.

Off, every write takes the path it took on the base; the route is published on both arms (one Arc per change).

* test(cmux-tui-core): after a respawn, keys skip the sizing lock again; the lock tests report a viewport (#34261 review red)

Review comment 6619473751 (FIX at 846d412):
1. A respawn under the same runtime id keeps the sizing entry, which keeps the dead runtime's fast-path slot: the
   new runtime's slot is never published, so every later key takes Mux::client_sizing twice. The new test respawns
   the typing terminal, sends one key, then holds the sizing lock: the next keys must not wait. Red on this
   commit.
2. The lock tests' view never reported a size, so the engine held the grid and the skip went only through that
   branch. The typing view now reports 100x30 as the app's view does, so its keys skip through the settled-grid
   check.

* fix(cmux-tui-core): a sizing entry follows its terminal across a respawn; the input time never goes back (#34261 review)

Review comment 6619473751 (FIX at 846d412):
1. A respawn under the same runtime id keeps the sizing entry but builds a new terminal runtime with a new
   fast-path slot. TerminalSizingEntry::follow_runtime_of re-points the entry at the slot of the runtime its keys
   reach, under the sizing lock, and clears the old slot: on the key's slow path (note_terminal_activity_of has the
   key's surface) and on every terminal_sizing_entry call (attach, claim, size report). The red test
   after_a_respawn_keys_wait_for_no_sizing_lock_again (4bf5af5) pins it.
3. The entry takes or follows a slot only from a surface whose runtime id is the entry's, so two entries never
   share one slot.
4. The lock-free input time is fetch_max, so a key preempted after its clock read never moves it back.
7. The poison comment says what happens: a later holder that recovers the poison republishes what it touches.

Lock order is unchanged: terminal_sizing_entry now looks the surface up on each call (it did at creation, and
apply_terminal_grid does), under the sizing lock, the established sizing-then-session order.

* fix(cmux-tui-core): name the input port module without super:: where surface::* is in scope (clippy unused_qualifications)

* test(cmux-tui-core): a settled size-state read takes the session lock once with the switch off (#34261 review 2 red)

The sizing entry looks its surface up on every access since 5fe770d, so with the switch off a size-state read
takes the session lock twice (once under the sizing lock): expected red at this commit (lane off: 2, not 1).

* fix(cmux-tui-core): a sizing entry looks its surface up only when it is made; the respawn follow runs only on the lane (#34261 review 2)

5fe770d looked the surface up on every sizing-entry access, so with the switch off (the default) every attach,
size report, claim and size-state read took the session lock under the sizing lock. The surface is looked up only to
make the entry again; a respawn's new runtime is followed by its first key's sizing note, which holds the surface,
and only while the switch is on. An unchanged fast path is not stored again. The respawn test pins the old slot's
clear.

* fix(cmux-tui-core): the input port's order test can fail; the port copies outside its lock, counts each frame, recovers from a writer panic and refuses a failed link (#34544 review)

- The test compared two Options (None < Some), so a key that passed the paste still passed; both positions are
  required now. A new test sends 26 keys on the lane and checks the host reads each once, in order.
- enqueue copies the bytes before the port lock, and each queued frame counts 64 bytes of bookkeeping against the
  32 MiB capacity, so many tiny frames cannot grow the queue without bound.
- A writer that unwinds drops the frames after the one it lost (logged) and marks itself stopped, so the next input
  starts a new writer instead of queueing behind a writer that is gone.
- A host connection the reader gave up on (Failed) refuses input at once, as the old path's failed write did.
- The deadlines use checked_add (no new arithmetic_side_effects hits); the thread name fits Linux's 15 bytes.

* fix(cmux-tui-core): the input port refuses only when the host reader ended, not while a Failed link still retries (#34544 review 2)

A Failed link keeps retrying its host, and a successful retry installs the live replacement before the state
leaves Failed; the old path wrote to it, the lane refused. The lane now refuses only when the reader ended (no
connection will come back), where the old path's write fails too. The capacity holds two of the largest frames
with their per-frame cost; the 26-key test also checks that no key is written twice.

* fix(cmux-tui-core): a key to a failed host link is refused at once on the input port, as on the old path (#34544 review 3)

When the reader moves a link to Failed it shuts the connection down (and keeps retrying); the old path's write then
failed and `cmux send` exited non-zero, while the port took the key and only logged its failure. The reader now
publishes InputRoute::Failed under the runtime lock when it shuts the connection down; the port refuses it with
NotConnected, and the replacement's route (replace_runtime) is accepted as soon as it is installed. Also the 26-key
test's tail loop is a while-let (clippy::while_let_loop).

Test: a_key_to_a_failed_link_is_refused_until_a_live_route_is_published.

* fix(cmux-tui-core): a failed reconnect completion on a failed link also makes the input port refuse (#34544 review 4)

A retry that installed a replacement and then failed to complete it (terminal_host_reconnected false) shuts the
replacement down while the link stays Failed; the route was still the shut replacement, so the port took keys and
only logged them. That path now publishes InputRoute::Failed under the runtime lock too, as the Failed transition
does. Not driven by a test: the hosted fixture's live reader thread reacts to the disconnect itself; the route
behavior is covered by a_key_to_a_failed_link_is_refused_until_a_live_route_is_published.

---------

Co-authored-by: cmux-bot[bot] <348531757+cmux-bot[bot]@users.noreply.github.com>
lawrencecchen added a commit that referenced this pull request Nov 4, 2026
…the person's keys answer before the write (R-DAEMON-STATE S1.6b) (#35082)

* test(cmux-tui-core): the person's key waits for no shared lock (R-DAEMON-STATE S1 red)

A key line from an attached view goes through the real connection path (the
line handler, the connection's dispatcher, the send handler) while another
thread holds one shared lock for 300 ms. #31940 took Mux.state off the key;
the key still waits for the client registry lock (five takes per line), the
link peers lock, the host proxy authors lock, the surface admission lock, the
creating-input lock, the client sizing lock and the activity lock. Their other
holders are every connect, attach, resize and request of every client.
Mux.state and the workspace registry rows are controls (pass on the base).

Two keys go in a row, so a lock taken after the PTY write (sizing, activity)
shows as a delay of the second key on the connection's dispatcher.

Test-only hold hooks (cfg(test)) in creating_input.rs, terminal_sizing.rs,
activity.rs and agent_session_attach.rs. No product change.

Design: handoff/rewrites/R-DAEMON-STATE/DESIGN.md section 2.6 (stage S1).

* test(cmux-tui-core): a key waits for no client registry lock (R-DAEMON-STATE S1 step 1 red)

The previous commit's census test held nine shared locks in turn; this commit
splits it by fix step. Step 1 is the client registry lock (ClientRegistry.state),
which a key line takes about nine times (handoff checks, remote-client and
remote-device checks, the request's actor, the kept terminal, the reply's
incognito filter) while every connect, disconnect, attach and detach of every
client holds it.

The test turns on the stage switch CMUX_TUI_RDS_INPUT (rds_switch.rs, inert in
this commit) and drives two keys through the real connection path while another
thread holds the lock for 300 ms. A second test keeps Mux.state and the
workspace registry off the key on the new path (#31940).

The hold hooks of the other locks leave with this commit; they come back with
their fix steps.

* fix(cmux-tui-core): the person's key reads the client registry's published facts, never its lock (R-DAEMON-STATE S1 step 1)

A key line took the client registry lock (ClientRegistry.state) about nine
times: the daemon handoff checks, the remote-client and remote-device checks,
the request's actor, the kept terminal of the key and the reply's incognito
filter. Every connect, disconnect, attach, detach and capability change of every
client holds that lock, so an agent storm held back the person's keys (red at
2472251: 300.4 ms behind a 300 ms hold; STATUS 9131).

The registry is now a Published value (published.rs): writers still serialize
on its mutex, and a guard that changed the value publishes an immutable
ClientFacts projection (handoff state; per connection its transport, remote
device flag, base actor and kept input terminals) with arc-swap when it drops,
under the mutex, so projections follow the order of the changes. With the stage
switch CMUX_TUI_RDS_INPUT=1, the line and key paths read the projection with
one atomic load: daemon_handoff_in_progress/committed, transport_of (so
is_remote_client), is_remote_device (refuse_line, line_to_recheck,
prune_reply), connection_actor and attached_input_surface. A poisoned registry
still fails closed (the projection comes in the error, as Mutex::lock gives
its guard). With the switch off nothing changes.

A projection costs O(connections) under the registry lock per change, bounded
by the 64-connection cap; stage S2 makes it O(changed).

arc-swap 1.9 is already in the lockfile and the shipped notices (through
iroh-dns); cmux-tui-core now names it.

Tests: rds_input_lane (a key waits for no client registry lock; the session and
workspace registry locks stay off; a respawn under the same id sends the next
key to the new shell), published.rs unit tests (a change publishes on drop, a
read publishes nothing, a reader never waits for a writer, a poisoned value
fails closed). The two remote-relay poison tests poison through the new type.

* fix(cmux-tui-core): the key reads the session's published routes; one stage switch per mux (R-DAEMON-STATE S1 step 1)

The hosted run of f1ea1b6 (https://github.com/manaflow-ai/cmux/actions/runs/41655768031) still failed
both lane tests: the key waited 300 ms for the client registry and, new, for Mux.state. Two causes:

- The key still used the kept terminal of #31940. That cache is validated by process-wide route
  generations, and cargo test runs the tests of one binary as threads of one process: another test's
  mux moved the same generation slot (ids start at 1 in every mux), the kept terminal looked stale, and
  the key fell back to the locked lookup and then to remember_attached_input (registry lock). The
  stage switch was a process-wide static too.
- So the lane must not depend on a cache with process-wide invalidation at all.

Now, with CMUX_TUI_RDS_INPUT=1:
- The session's three routing tables (surfaces, terminal_catalog, terminal_catalog_by_runtime) are
  copy-on-write behind an Arc. A state guard that changed one publishes a RouteSnapshot (three Arc
  clones) with arc-swap when it drops, under the lock, so publications follow the changes; the
  snapshot it replaced is freed after the lock is released. Mux::surface reads the snapshot with one
  atomic load. A respawn, close, move, rollback or projection is seen once its guard is released, as a
  reader that waited for the lock would see it. Nothing is kept per client, so nothing is invalidated.
- input_surface on the lane: the published routes give the terminal, the registry's published facts
  say whether the client types into it (types_into: the person's input QoS mark). attached_input_surface
  and remember_attached_input stay only on the old path.
- The switch is per mux (rds_switch::InputLaneSwitch, read once from the environment when the mux is
  built; Mux::set_input_lane_for_test in tests), and it also decides whether anything is published: off,
  the registry builds no projection and the state holds no route snapshot, so a route write never
  copies a shared table and the off arm costs what the base costs.

Cost on the lane: a guard's first route write copies that table once (O(routes)), bounded by one copy
per changing guard; State::clone no longer deep-copies the three tables. Stage S2 makes both O(log n).

* test(cmux-tui-core): a key waits for no surface admission lock, with an old-path control (R-DAEMON-STATE S1 step 3 red)

Every request of every connection reserves its retained bytes from one
daemon-wide budget (ServerSurfaceOperationAdmission) under one mutex before it
is queued, and releases them under it after it ran. The S1 census measured a
key waiting 300.3 ms behind a 300 ms hold of that lock (hosted run 41648095321).

The lane test now has a control: the same two keys with stage S1 off must
wait for the held lock, so a lane result can never come from a hold that does
not reach the key path (a vacuous green). The control is added to the client
registry test too. A test-only hook (cfg(test)) holds the admission lock.

Red here: with the lane on, the key still reserves through the mutex.

* fix(cmux-tui-core): the surface admission budget reserves with atomics on the input lane (R-DAEMON-STATE S1 step 3)

Every request of every connection reserves its retained bytes (and every
clear-history its worker) from one daemon-wide budget before it is queued and
releases after it ran. Under one mutex, a key waited for any other
connection's reservation or release (red at 5d0e4a0: 300 ms behind a
300 ms hold).

With CMUX_TUI_RDS_INPUT=1 the two counters are atomics with compare-and-swap
reservations (fetch_update: add only while the sum stays within the capacity;
release saturates at zero), so nothing waits. The limits and the refusals are
the same as the mutex arm's: a worker is refused at 16 reserved, bytes when
the sum would pass 16 MiB. Each permit releases to the arm it reserved from.
With the switch off the mutex arm runs as before; it goes with the switch when
the stage passes (S1.8). The arm is fixed when the mux is built
(ServerSurfaceOperationAdmission::new(publish_routes)); tests set it with
Mux::set_input_lane_for_test before any request.

The two scheduler tests that counted reserved workers through the mutex read
reserved_workers() (both arms).

* test(cmux-tui-core): a key waits for neither the link peers nor the proxy authors lock (R-DAEMON-STATE S1 step 2 red)

Two more shared locks on the person's key line (DESIGN.md Appendix A.2 steps 3, 7, 12 and 13): the link peers map
(RemoteRelayState.peers: the remote-client check and the request's actor) and the host proxy authors
(AgentSessions.proxy_authors: the proxy-link check on the reader thread). The S1 census measured a full 300 ms wait
on each (hosted run 41648095321: 300.5 ms and 300.4 ms).

The test holds each lock 300 ms on another thread while two keys go through the real line path. It first proves
the hold reaches the old path (stage switch off: the key waits), then expects the stage's path not to wait. Red on
this commit: the stage does not publish either value yet.

* test(cmux-tui-core): a change cut short by a panic is never published (R-DAEMON-STATE S1 red)

A published value's guard publishes its projection in Drop::drop, which also runs while a panic unwinds, before
its MutexGuard field drops and marks the mutex poisoned. A writer that panics in the middle of a change so
publishes the half-made value, and in the moment between the publication and the poisoning a reader gets it as a
good value: a fail-open window for readers that fail closed on a poisoned value (transport_of, peer_checked).

Red on this commit: the test sees the half-made value 9 in the poisoned projection.

* test(cmux-tui-core): a reservation unshares a published route map; a panicked change publishes no routes (R-DAEMON-STATE S1 red)

Two defects the #34014 review found on the input lane (both only with CMUX_TUI_RDS_INPUT on):

- RouteTable::try_reserve returns early when the map has room, also when a published route snapshot shares the
  map. The insert after the durable commit then copies the whole table (Arc::make_mut), an allocation that
  ResourceMutationPlan::apply forbids ("Plan builders reserve all needed capacities before returning").
- The session state guard publishes the routes of a change that a panic cut short: Drop::drop runs during the
  unwind, before the MutexGuard field poisons the mutex, so the lane routes by half-made tables.

Red on this commit: the insert changes the map's pointer, and the routes snapshot is replaced.

* fix(cmux-tui-core): link peers and host proxy authors are published; the S1.1 review follow-ups (R-DAEMON-STATE S1 step 2)

A person's key line asked two more shared locks (DESIGN.md Appendix A.2 steps 3, 7, 12, 13): the link peers map
(the remote-client check and the request's actor) and the host proxy authors (the proxy-link check on the reader
thread). Every remote bind, disconnect and revocation sweep holds the first; every proxy bind and every client's
disconnect holds the second.

- published.rs moves to cmux-tui-util (daemon primitives, no daemon state) so cmux-tui-remote-access can use it.
  RemoteRelayState.peers becomes a Published copy of the records; peer_checked reads it with no lock when the
  mux's stage switch is on (Published::publishing), and a poisoned map still fails closed. AgentSessions.proxy_authors
  publishes the same way; proxy::author reads it. A disconnect of a client without a record publishes nothing.
- Review follow-ups of #34014 (review comment 6612546773):
  - RouteTable::try_reserve copies a map a published snapshot shares, before the durable commit, so the insert
    after the commit allocates nothing (ResourceMutationPlan::apply).
  - A change a panic cuts short is never published, by the state guard (routes) or by a PublishGuard: Drop skips
    the publication when a panic began while the guard held the lock (std's own poisoning test), so no reader
    sees half-made tables, not even before the mutex is poisoned.
  - A PublishGuard frees the projection it replaced after the unlock (as the state guard does).
  - The two poison tests of the relay run on both arms.
  - ClientFacts and types_into move to server/client_registry/facts.rs (god-file warning).

Same switch, CMUX_TUI_RDS_INPUT, default off: with it off nothing is published and every read takes its lock as
on the base.

* test(cmux-tui-core): a key waits for no creating-input lock, and for no session lock behind one (R-DAEMON-STATE S1 step 4 red)

Every key to a live terminal first asks whether creating input of its terminal must go first
(wait_for_creating_input_flush, DESIGN.md Appendix A.2 step 16). It takes the daemon-wide creating-input lock even
when no terminal is being created, and that lock's holders (begin_creating_input, knows_terminal_input_target,
finish_creating_input) look up the live terminal under it, which takes the session lock. The S1 census measured a
full 300 ms wait on the creating-input lock (hosted run 41648095321: 300.3 ms).

Two holders, each with an old-path control: the creating-input lock itself, and the session lock held while a
reader waits for it under the creating-input lock (with another terminal being created, so the key cannot skip the
lock). Red on this commit: the stage does not change these paths yet.

* fix(cmux-tui-core): a key skips the creating-input lock while nothing is creating; the lock becomes a leaf (R-DAEMON-STATE S1 step 4)

On the input lane (CMUX_TUI_RDS_INPUT, per mux, default off):

- CreatingInputs keeps the number of creating terminals in an atomic, stored under its lock after every insert and
  removal (Release). A key reads it (Acquire) and returns at once when it is zero: no queue can be ahead of it,
  because a terminal's queue is added before the terminal goes live and removed only after every queued byte was
  written to it.
- The creating-input lock's holders look up the live terminal in the published routes instead of under the session
  lock, so no holder waits for the session lock while it holds the creating-input lock, and a key that takes it
  never waits behind one. The routes carry the host-id catalog index for this: terminal_catalog_by_host becomes a
  copy-on-write route table in the RouteSnapshot, and one lookup (route_table::live_terminal) serves the session
  state and the snapshot. A commit publishes its routes before it releases the session lock, and finish runs after
  the commit, so begin, knows and finish give the answers they gave under the session lock.

Off, every path takes the locks it took on the base; the by-host index is copy-on-write on both arms, as the other
three route tables are since step 1.

* test(cmux-tui-core): the surface budget's limits on both arms; a lock control fails on a lost key (#34146 review follow-ups)

The #34146 review (comment 6613345630) noted:
- Every budget test used the mutex arm, so an atomic arm that admitted everything would have passed. The two
  mux-wide limit tests now run on both arms, and a new test checks the same limits on both: the 17th worker, the
  whole byte budget, one byte over, zero bytes, a sum that overflows, and released reservations coming back.
- The old-path control passed when a key never arrived (Duration::MAX >= BUDGET); it now fails on a lost key.
- ServerSurfaceOperationState and the admission's mutex are private (nothing outside the file reads them), and
  the doc says which requests reserve (pings, identify, v2 resource lines and reader-thread frame handlers do not).

* fix(cmux-tui-core): drop an unnecessary path qualification in a test (clippy unused_qualifications)

The lint (linux) job's clippy -D warnings failed on mux/tests/signaled_mutex.rs:120 (TerminalPublicId is in scope);
the #34174 review found it behind the base's acpmux red in the same job.

* test(cmux-tui-core): a poisoned session state never routes by the published tables (R-DAEMON-STATE S1 red)

The #34171 review noted: after a panic poisons Mux::state, the next guard that recovers the poison may publish the
half-made route tables, and the lane's Mux::surface reads the published routes with no poison check, where the old
path fails (it unwraps the lock). Red on this commit: with the lane on, a surface read after the poisoning returns
from the published routes instead of failing.

The relay poison tests now poison published values through a helper in the test file.

* fix(cmux-tui-core): the lane's surface read takes the old path on a poisoned state; the poison helper is test code (#34171 review)

Review comment 6614044759 (FIX at 8f7d405):
- published.rs: poison_for_test moved out of the test module with its panic! respelled as panic_any, which hides a
  hit from the crash ratchet's count. It is test code again: a private helper in the published tests and one in
  the relay tests, with plain panic!.
- Mux::surface on the lane reads the published routes only while the session state is not poisoned; a poisoned
  state takes the old read and fails as it does (StateMutex::is_poisoned). A guard that recovers the poison may
  publish the half-made tables of the change that panicked.
- Published::load debug-asserts that the value publishes: only a publishing value has a current projection.
- The module doc says what load gives on a poisoned value; the caller decides whether to fail closed.

The unused_qualifications error at mux/tests/signaled_mutex.rs:120 is fixed in 10f1669.

* fix(cmux-tui-core): the creating-input live check reads the routes only on an unpoisoned state; its comment states the lane's linearization (#34174 review)

Review comment 6613998735 (SHIP, notes): the begin_creating_input comment still said the check takes the state
under the creating lock; on the lane it reads the published routes, and its answer is one the old path can give
(a commit in progress has not made the id live, as when the old path takes the state before it). The lane's live
lookup now takes the old read on a poisoned state, as Mux::surface does since bb0265f.

* test(cmux-tui-core): a key waits for neither the sizing nor the activity lock; a key takes the grid back after another view's click (R-DAEMON-STATE S1 step 5 red)

After its PTY write a key notes activity in the terminal's sizing (the newest view sets the grid under the default
policy): it takes the daemon-wide client sizing lock twice (the note and the size-state publication), then the
VM activity stream's lock. Every attach, detach, resize and size report of every terminal holds the sizing lock,
and a PTY resize runs under it. The S1 census measured a full 300 ms wait on each (hosted run 41648095321: 300.2
and 300.3 ms).

The first test holds each lock 300 ms (with an old-path control each). The second keeps the fix honest: two views
of one terminal, the default policy; the other view's focus click takes the grid, and the next key from the
typing view must take it back, on both arms. Red on this commit: the first test (the stage does not change these
paths yet).

* fix(cmux-tui-util): a test dereferences the published u64 once (E0614 in bb0265f)

* fix(cmux-tui-core): a key skips its sizing note when its view is the settled newest; the person's input time is an atomic (R-DAEMON-STATE S1 step 5)

On the input lane (CMUX_TUI_RDS_INPUT, per mux, default off):

- Each terminal's sizing entry publishes, on the terminal's runtime (shared by every placement), the view a key may
  skip its sizing note for: the sole newest participant, a client's own view, only while the entry is settled (no
  grid hold, no handheld with a viewport, and the engine holds the grid or the applied grid and participants are its
  decision). Noting activity for that view changes no decision under any policy, and the size state carries no
  activity, so the key compares (client, placement) with one atomic load and takes no sizing lock.
- The publication is automatic: Mux::client_sizing is a SizingMutex whose guard republishes, on drop and under the
  lock, each entry its holder read or wrote (SizingEntries marks them; an apply changes Cells through a shared
  borrow, so reads count). A removed entry clears its slot; a change a panic cuts short publishes nothing.
- Another view's activity makes that view the newest and republishes, so the next key from the first view takes the
  slow path once and takes the grid back (the second test pins this on both arms).
- ActivityStream::note_user_input stores the time in an atomic; it takes the stream lock only while a subscriber
  exists, once per event the worker sends (a wake_pending flag the worker clears before it reads the time).
- TerminalSizingEngine::sole_latest (cmux-terminal-sizing) with a unit test; AppliedSeat::of shares the seat rule
  between the apply and the settled check; set_workspace_size_policy reads its state before the note (no index).

Off, every key takes the sizing and activity locks as on the base; the marks and the guard check one bool.

* fix(cmux-tui-core): the sizing test hook went in above another test hook's cfg(test) (dead code and 2 expect hits in production)

dfe81bf put hold_client_sizing_for_test between pin_latest_size_policy_for_test's doc and its #[cfg(test)], so
pin_latest_size_policy_for_test compiled into production: clippy -D warnings dead_code, and the panic ratchet's
cmux-tui-core expect_used 175 > 173 (hosted lint job 126202464363).

* fix(cmux-tui-core): the lane's surface read loads the routes before its poison check; two test doc comments on their items (#34171 review 2)

Review comment 6615927915 (SHIP at 0fb7c43, notes): Mux::surface checked is_poisoned() and then loaded the
routes; between the two another thread could poison the state and a third, recovering the poison, publish half-made
tables. Poison never clears on the session state, so routes loaded before an unpoisoned check were published before
any poisoning: load first, then check. Two doc comments in remote_relay/tests.rs sat above the wrong item.

* test(cmux-tui-core): a key does not wait for another writer's blocked host socket (R-DAEMON-STATE S1 step 6 red)

A key to a hosted terminal holds the terminal's runtime lock and its HostAttachment.writer across a blocking write
to the host socket (surface/input.rs, attachment.rs send_host_frame). Another client's large paste to the same
terminal holds both for its whole write, bounded only by the socket's send timeout, so the person's key waits for
automation's work on the same terminal (DESIGN.md section 2.6.1, Appendix A.2 steps 20-22).

The test pastes 8 MiB while the fake host reads nothing for 600 ms, then sends one key. A control first proves the
old path waits (the key takes at least half the stall); the lane must return the key in < 100 ms, and the host must
then read the paste before the key, and the key once. Red on this commit: the lane has no input port yet.

* fix(cmux-tui-core): a person's key to a hosted terminal goes to the terminal's input port and never waits for a socket write (R-DAEMON-STATE S1 step 6)

On the input lane (CMUX_TUI_RDS_INPUT, per mux, default off), interactive input to a hosted terminal
(Surface::write_bytes, Surface::write_paste) goes to the terminal's own input port (surface/input_port.rs): one
queue per terminal, drained in order by the terminal's writer thread (terminal-input-port, started on the first
input, ended after 30 s idle). The request returns once the port took the bytes; it takes neither the terminal's
runtime lock nor the host writer, so another writer of the same terminal (a large paste, receipted input, a
cell-pixel change waiting for its acknowledgement) and a host that does not read its socket no longer hold the
person's key or the connection's dispatcher.

- The writer writes on the route of the current host connection (HostInputRoute: the connection's writer), which
  the terminal publishes whenever its runtime changes: the hosted and exited constructors, and
  PtyTerminalRuntime::replace_runtime, which the reconnect install and the host exit now use. So the writer never
  takes the runtime lock either.
- A lost connection is retried as write_bytes did (cx-6so.49), on the port's thread: wait for the reader's next
  stream change, write to the new host, until HOST_INPUT_RECONNECT_WAIT.
- A full port (32 MiB queued) refuses at once (terminal.input_busy, WouldBlock); a write larger than one host frame
  is refused as before; the exited final screen drops input as before.
- Input the port took and then could not write is lost as a failed write was, with a log line: the request was
  already answered (the reply now means "accepted by the terminal's port", DESIGN.md 2.6.1).
- Not in this step (S1.6b, before the default flip): the connection's other frames still write directly, so while
  the port is behind they can go before a key it still holds.

Off, every write takes the path it took on the base; the route is published on both arms (one Arc per change).

* test(cmux-tui-core): after a respawn, keys skip the sizing lock again; the lock tests report a viewport (#34261 review red)

Review comment 6619473751 (FIX at 846d412):
1. A respawn under the same runtime id keeps the sizing entry, which keeps the dead runtime's fast-path slot: the
   new runtime's slot is never published, so every later key takes Mux::client_sizing twice. The new test respawns
   the typing terminal, sends one key, then holds the sizing lock: the next keys must not wait. Red on this
   commit.
2. The lock tests' view never reported a size, so the engine held the grid and the skip went only through that
   branch. The typing view now reports 100x30 as the app's view does, so its keys skip through the settled-grid
   check.

* fix(cmux-tui-core): a sizing entry follows its terminal across a respawn; the input time never goes back (#34261 review)

Review comment 6619473751 (FIX at 846d412):
1. A respawn under the same runtime id keeps the sizing entry but builds a new terminal runtime with a new
   fast-path slot. TerminalSizingEntry::follow_runtime_of re-points the entry at the slot of the runtime its keys
   reach, under the sizing lock, and clears the old slot: on the key's slow path (note_terminal_activity_of has the
   key's surface) and on every terminal_sizing_entry call (attach, claim, size report). The red test
   after_a_respawn_keys_wait_for_no_sizing_lock_again (4bf5af5) pins it.
3. The entry takes or follows a slot only from a surface whose runtime id is the entry's, so two entries never
   share one slot.
4. The lock-free input time is fetch_max, so a key preempted after its clock read never moves it back.
7. The poison comment says what happens: a later holder that recovers the poison republishes what it touches.

Lock order is unchanged: terminal_sizing_entry now looks the surface up on each call (it did at creation, and
apply_terminal_grid does), under the sizing lock, the established sizing-then-session order.

* fix(cmux-tui-core): name the input port module without super:: where surface::* is in scope (clippy unused_qualifications)

* test(cmux-tui-core): a settled size-state read takes the session lock once with the switch off (#34261 review 2 red)

The sizing entry looks its surface up on every access since 5fe770d, so with the switch off a size-state read
takes the session lock twice (once under the sizing lock): expected red at this commit (lane off: 2, not 1).

* fix(cmux-tui-core): a sizing entry looks its surface up only when it is made; the respawn follow runs only on the lane (#34261 review 2)

5fe770d looked the surface up on every sizing-entry access, so with the switch off (the default) every attach,
size report, claim and size-state read took the session lock under the sizing lock. The surface is looked up only to
make the entry again; a respawn's new runtime is followed by its first key's sizing note, which holds the surface,
and only while the switch is on. An unchanged fast path is not stored again. The respawn test pins the old slot's
clear.

* fix(cmux-tui-core): the input port's order test can fail; the port copies outside its lock, counts each frame, recovers from a writer panic and refuses a failed link (#34544 review)

- The test compared two Options (None < Some), so a key that passed the paste still passed; both positions are
  required now. A new test sends 26 keys on the lane and checks the host reads each once, in order.
- enqueue copies the bytes before the port lock, and each queued frame counts 64 bytes of bookkeeping against the
  32 MiB capacity, so many tiny frames cannot grow the queue without bound.
- A writer that unwinds drops the frames after the one it lost (logged) and marks itself stopped, so the next input
  starts a new writer instead of queueing behind a writer that is gone.
- A host connection the reader gave up on (Failed) refuses input at once, as the old path's failed write did.
- The deadlines use checked_add (no new arithmetic_side_effects hits); the thread name fits Linux's 15 bytes.

* fix(cmux-tui-core): the input port refuses only when the host reader ended, not while a Failed link still retries (#34544 review 2)

A Failed link keeps retrying its host, and a successful retry installs the live replacement before the state
leaves Failed; the old path wrote to it, the lane refused. The lane now refuses only when the reader ended (no
connection will come back), where the old path's write fails too. The capacity holds two of the largest frames
with their per-frame cost; the 26-key test also checks that no key is written twice.

* fix(cmux-tui-core): a key to a failed host link is refused at once on the input port, as on the old path (#34544 review 3)

When the reader moves a link to Failed it shuts the connection down (and keeps retrying); the old path's write then
failed and `cmux send` exited non-zero, while the port took the key and only logged its failure. The reader now
publishes InputRoute::Failed under the runtime lock when it shuts the connection down; the port refuses it with
NotConnected, and the replacement's route (replace_runtime) is accepted as soon as it is installed. Also the 26-key
test's tail loop is a while-let (clippy::while_let_loop).

Test: a_key_to_a_failed_link_is_refused_until_a_live_route_is_published.

* fix(cmux-tui-core): a failed reconnect completion on a failed link also makes the input port refuse (#34544 review 4)

A retry that installed a replacement and then failed to complete it (terminal_host_reconnected false) shuts the
replacement down while the link stays Failed; the route was still the shut replacement, so the port took keys and
only logged them. That path now publishes InputRoute::Failed under the runtime lock too, as the Failed transition
does. Not driven by a test: the hosted fixture's live reader thread reacts to the disconnect itself; the route
behavior is covered by a_key_to_a_failed_link_is_refused_until_a_live_route_is_published.

* test(cmux-tui-core): a frame sent after a key the input port holds must reach the host after it (R-DAEMON-STATE S1.6b red)

The port counts the frames it took and finished and offers PortOrder::barrier; nothing calls it yet, so a direct
frame (here an Activate under the runtime lock, as viewer sizes and the other direct writers send) passes a key
the paused port still holds: expected red in a_frame_sent_after_a_key_the_port_holds_reaches_the_host_after_it.

* fix(cmux-tui-core): every other frame to a hosted terminal's host goes after the keys its input port took (R-DAEMON-STATE S1.6b order)

HostAttachment gets the terminal's PortOrder (set by the hosted constructor and replace_runtime, so it follows
every connection change); each frame it writes besides the port's own (send, receipted input, viewer size, cell
pixels, Kitty limits, control requests such as clear history and terminate, and the clipboard replier's replies)
first waits until the port finished every frame it had taken (PortOrder::barrier, at most 2 s, as one frame write
may block). The wait returns at once while the port's writer waits for a reconnect, since the install it waits
for may need the runtime lock the caller holds. With the switch off the port takes nothing and the wait is one
uncontended lock. The 9 test literals of HostAttachment take input_order: None.

Test: a_frame_sent_after_a_key_the_port_holds_reaches_the_host_after_it (red at 8b9f34a).

* test(cmux-tui-core): an automation write answers after the input port wrote it, with its frame's error (R-DAEMON-STATE S1.6b red)

On the lane every hosted write answered when the port took the bytes, so the send handler, the creating-input
flush (its give-back) and the resource input effect (its indeterminate mark) never saw a write error. Expected red:
an_automation_write_answers_after_the_port_wrote_it (it answers while the port is paused) and
an_automation_write_gets_the_error_of_its_frame (ok for bytes never written).

* fix(cmux-tui-core): on the input lane only the person's keys answer before the write; every other write answers with its frame's result (R-DAEMON-STATE S1.6b)

S1.6a answered every hosted write when the port took it, so the send handler for scripts, the creating-input
flush (its give-back), the resource input effect (its indeterminate mark) and the image paste (its reservation)
never saw a write error. Now Surface::write_bytes and write_paste answer when the port wrote the frame
(InputReply::Written: the port reports each frame's result on a channel, bounded at 15 s); only the person's key
(the send and send-key handlers, when the client is attached to the terminal it types into) passes
InputReply::Accepted and returns at once. The port also keeps one reconnect wait for its queue: frames queued
behind a host that does not come back fail together, not 5 s each. The give-back notice of the plan is dropped
(DESIGN 2.6.1: the app reads it only on its control connection, and it never retried a failed key).

Tests: an_automation_write_answers_after_the_port_wrote_it and an_automation_write_gets_the_error_of_its_frame
(red at 189ba58); the_persons_keys_answer_when_taken_and_other_writes_when_written; the person-key tests pass
InputReply::Accepted.

* test(cmux-tui-core): the input port's reconnect and reply rules from review 1 of #35082 (R-DAEMON-STATE S1.6b red)

Red:
- a_frame_sent_after_a_replacement_install_waits_for_the_keys_the_port_held (must-fix 1): after replace_runtime and
  before the reader's stream notify, a direct frame on the new connection passes the key the port held.
- a_later_host_loss_waits_for_its_own_reconnect (must-fix 2): the first loss's deadline outlives it, so a later
  loss's write fails at once.
- a_write_whose_wait_ends_before_the_port_takes_it_is_never_written, a_write_whose_wait_ends_during_its_write_may_be_written
  and the_creating_input_flush_keeps_input_that_may_have_been_written (item 6): a timed-out write stays queued and
  is written later, and the flush gives back bytes that may still be written.
Guards (pass before and after): a_frame_sent_while_the_port_waits_for_a_reconnect_does_not_wait_for_it,
writes_queued_behind_a_lost_host_fail_together, receipted_input_sent_after_a_key_the_port_holds_reaches_the_host_after_it.
an_automation_write_gets_the_error_of_its_frame waits for the port's count instead of sleeping 100 ms.

Test seams: the port's reply and reconnect waits (test overrides), its reconnecting/accepted/queued counts, and the
InputMayBeWritten marker with its classifier (nothing produces the marker yet).

* fix(cmux-tui-core): a route publication ends the input port's reconnect wait; each frame waits for a reconnect from when the port took it; a timed-out write is cancelled or marked possibly written (#35082 review 1)

- Must-fix 1: replace_runtime and input_route_failed call InputPort::route_changed. A port-owned route epoch ends
  the writer's reconnect wait and clears reconnecting under the port lock, so after the reader installs a live
  replacement the port writes the keys it holds to it before any other frame (before, the writer slept on the
  stream revision until the reader's notify after the VT replay, and the barrier skipped meanwhile).
- Must-fix 2: each frame waits for a reconnect until its own deadline, HOST_INPUT_RECONNECT_WAIT after the port took
  it, as the old path's write waited from its call. The queue-wide reconnect_until outlived its loss, so a later
  loss's writes failed at once. Writes queued behind a host that does not come back still fail together.
- Item 6: InputPort::write cancels a frame its waiter gave up on before the writer took it (the error says not
  written, and it never is); a wait that ends during the write answers InputMayBeWritten, and the creating-input
  flush does not give those bytes back (that typed them twice).
- Item 5: the order barrier ends by the caller's own deadline (control requests, cell pixels).
- Item 3: the order-wait comment states the bound; the clipboard replier uses the same deadline.
- Item 8: the port and input docs describe the current rules.

Tests (red at 2133c10): a_frame_sent_after_a_replacement_install_waits_for_the_keys_the_port_held,
a_later_host_loss_waits_for_its_own_reconnect, a_write_whose_wait_ends_before_the_port_takes_it_is_never_written,
a_write_whose_wait_ends_during_its_write_may_be_written, the_creating_input_flush_keeps_input_that_may_have_been_written.

* test(cmux-tui-core): the review-1 port tests name Frame and input_may_be_written through the module scope (clippy unused_qualifications)

lint (linux) at bbc44c2 (runs 41806402684, 41806407494) failed clippy -D warnings on four qualified paths in
surface/tests/hosted.rs; both names are in scope through use super::*.

---------

Co-authored-by: cmux-bot[bot] <348531757+cmux-bot[bot]@users.noreply.github.com>
lawrencecchen added a commit that referenced this pull request Nov 4, 2026
…end the input port's reconnect wait (R-DAEMON-STATE S1.6c) (#35280)

* test(cmux-tui-core): the person's key waits for no shared lock (R-DAEMON-STATE S1 red)

A key line from an attached view goes through the real connection path (the
line handler, the connection's dispatcher, the send handler) while another
thread holds one shared lock for 300 ms. #31940 took Mux.state off the key;
the key still waits for the client registry lock (five takes per line), the
link peers lock, the host proxy authors lock, the surface admission lock, the
creating-input lock, the client sizing lock and the activity lock. Their other
holders are every connect, attach, resize and request of every client.
Mux.state and the workspace registry rows are controls (pass on the base).

Two keys go in a row, so a lock taken after the PTY write (sizing, activity)
shows as a delay of the second key on the connection's dispatcher.

Test-only hold hooks (cfg(test)) in creating_input.rs, terminal_sizing.rs,
activity.rs and agent_session_attach.rs. No product change.

Design: handoff/rewrites/R-DAEMON-STATE/DESIGN.md section 2.6 (stage S1).

* test(cmux-tui-core): a key waits for no client registry lock (R-DAEMON-STATE S1 step 1 red)

The previous commit's census test held nine shared locks in turn; this commit
splits it by fix step. Step 1 is the client registry lock (ClientRegistry.state),
which a key line takes about nine times (handoff checks, remote-client and
remote-device checks, the request's actor, the kept terminal, the reply's
incognito filter) while every connect, disconnect, attach and detach of every
client holds it.

The test turns on the stage switch CMUX_TUI_RDS_INPUT (rds_switch.rs, inert in
this commit) and drives two keys through the real connection path while another
thread holds the lock for 300 ms. A second test keeps Mux.state and the
workspace registry off the key on the new path (#31940).

The hold hooks of the other locks leave with this commit; they come back with
their fix steps.

* fix(cmux-tui-core): the person's key reads the client registry's published facts, never its lock (R-DAEMON-STATE S1 step 1)

A key line took the client registry lock (ClientRegistry.state) about nine
times: the daemon handoff checks, the remote-client and remote-device checks,
the request's actor, the kept terminal of the key and the reply's incognito
filter. Every connect, disconnect, attach, detach and capability change of every
client holds that lock, so an agent storm held back the person's keys (red at
2472251: 300.4 ms behind a 300 ms hold; STATUS 9131).

The registry is now a Published value (published.rs): writers still serialize
on its mutex, and a guard that changed the value publishes an immutable
ClientFacts projection (handoff state; per connection its transport, remote
device flag, base actor and kept input terminals) with arc-swap when it drops,
under the mutex, so projections follow the order of the changes. With the stage
switch CMUX_TUI_RDS_INPUT=1, the line and key paths read the projection with
one atomic load: daemon_handoff_in_progress/committed, transport_of (so
is_remote_client), is_remote_device (refuse_line, line_to_recheck,
prune_reply), connection_actor and attached_input_surface. A poisoned registry
still fails closed (the projection comes in the error, as Mutex::lock gives
its guard). With the switch off nothing changes.

A projection costs O(connections) under the registry lock per change, bounded
by the 64-connection cap; stage S2 makes it O(changed).

arc-swap 1.9 is already in the lockfile and the shipped notices (through
iroh-dns); cmux-tui-core now names it.

Tests: rds_input_lane (a key waits for no client registry lock; the session and
workspace registry locks stay off; a respawn under the same id sends the next
key to the new shell), published.rs unit tests (a change publishes on drop, a
read publishes nothing, a reader never waits for a writer, a poisoned value
fails closed). The two remote-relay poison tests poison through the new type.

* fix(cmux-tui-core): the key reads the session's published routes; one stage switch per mux (R-DAEMON-STATE S1 step 1)

The hosted run of f1ea1b6 (https://github.com/manaflow-ai/cmux/actions/runs/41655768031) still failed
both lane tests: the key waited 300 ms for the client registry and, new, for Mux.state. Two causes:

- The key still used the kept terminal of #31940. That cache is validated by process-wide route
  generations, and cargo test runs the tests of one binary as threads of one process: another test's
  mux moved the same generation slot (ids start at 1 in every mux), the kept terminal looked stale, and
  the key fell back to the locked lookup and then to remember_attached_input (registry lock). The
  stage switch was a process-wide static too.
- So the lane must not depend on a cache with process-wide invalidation at all.

Now, with CMUX_TUI_RDS_INPUT=1:
- The session's three routing tables (surfaces, terminal_catalog, terminal_catalog_by_runtime) are
  copy-on-write behind an Arc. A state guard that changed one publishes a RouteSnapshot (three Arc
  clones) with arc-swap when it drops, under the lock, so publications follow the changes; the
  snapshot it replaced is freed after the lock is released. Mux::surface reads the snapshot with one
  atomic load. A respawn, close, move, rollback or projection is seen once its guard is released, as a
  reader that waited for the lock would see it. Nothing is kept per client, so nothing is invalidated.
- input_surface on the lane: the published routes give the terminal, the registry's published facts
  say whether the client types into it (types_into: the person's input QoS mark). attached_input_surface
  and remember_attached_input stay only on the old path.
- The switch is per mux (rds_switch::InputLaneSwitch, read once from the environment when the mux is
  built; Mux::set_input_lane_for_test in tests), and it also decides whether anything is published: off,
  the registry builds no projection and the state holds no route snapshot, so a route write never
  copies a shared table and the off arm costs what the base costs.

Cost on the lane: a guard's first route write copies that table once (O(routes)), bounded by one copy
per changing guard; State::clone no longer deep-copies the three tables. Stage S2 makes both O(log n).

* test(cmux-tui-core): a key waits for no surface admission lock, with an old-path control (R-DAEMON-STATE S1 step 3 red)

Every request of every connection reserves its retained bytes from one
daemon-wide budget (ServerSurfaceOperationAdmission) under one mutex before it
is queued, and releases them under it after it ran. The S1 census measured a
key waiting 300.3 ms behind a 300 ms hold of that lock (hosted run 41648095321).

The lane test now has a control: the same two keys with stage S1 off must
wait for the held lock, so a lane result can never come from a hold that does
not reach the key path (a vacuous green). The control is added to the client
registry test too. A test-only hook (cfg(test)) holds the admission lock.

Red here: with the lane on, the key still reserves through the mutex.

* fix(cmux-tui-core): the surface admission budget reserves with atomics on the input lane (R-DAEMON-STATE S1 step 3)

Every request of every connection reserves its retained bytes (and every
clear-history its worker) from one daemon-wide budget before it is queued and
releases after it ran. Under one mutex, a key waited for any other
connection's reservation or release (red at 5d0e4a0: 300 ms behind a
300 ms hold).

With CMUX_TUI_RDS_INPUT=1 the two counters are atomics with compare-and-swap
reservations (fetch_update: add only while the sum stays within the capacity;
release saturates at zero), so nothing waits. The limits and the refusals are
the same as the mutex arm's: a worker is refused at 16 reserved, bytes when
the sum would pass 16 MiB. Each permit releases to the arm it reserved from.
With the switch off the mutex arm runs as before; it goes with the switch when
the stage passes (S1.8). The arm is fixed when the mux is built
(ServerSurfaceOperationAdmission::new(publish_routes)); tests set it with
Mux::set_input_lane_for_test before any request.

The two scheduler tests that counted reserved workers through the mutex read
reserved_workers() (both arms).

* test(cmux-tui-core): a key waits for neither the link peers nor the proxy authors lock (R-DAEMON-STATE S1 step 2 red)

Two more shared locks on the person's key line (DESIGN.md Appendix A.2 steps 3, 7, 12 and 13): the link peers map
(RemoteRelayState.peers: the remote-client check and the request's actor) and the host proxy authors
(AgentSessions.proxy_authors: the proxy-link check on the reader thread). The S1 census measured a full 300 ms wait
on each (hosted run 41648095321: 300.5 ms and 300.4 ms).

The test holds each lock 300 ms on another thread while two keys go through the real line path. It first proves
the hold reaches the old path (stage switch off: the key waits), then expects the stage's path not to wait. Red on
this commit: the stage does not publish either value yet.

* test(cmux-tui-core): a change cut short by a panic is never published (R-DAEMON-STATE S1 red)

A published value's guard publishes its projection in Drop::drop, which also runs while a panic unwinds, before
its MutexGuard field drops and marks the mutex poisoned. A writer that panics in the middle of a change so
publishes the half-made value, and in the moment between the publication and the poisoning a reader gets it as a
good value: a fail-open window for readers that fail closed on a poisoned value (transport_of, peer_checked).

Red on this commit: the test sees the half-made value 9 in the poisoned projection.

* test(cmux-tui-core): a reservation unshares a published route map; a panicked change publishes no routes (R-DAEMON-STATE S1 red)

Two defects the #34014 review found on the input lane (both only with CMUX_TUI_RDS_INPUT on):

- RouteTable::try_reserve returns early when the map has room, also when a published route snapshot shares the
  map. The insert after the durable commit then copies the whole table (Arc::make_mut), an allocation that
  ResourceMutationPlan::apply forbids ("Plan builders reserve all needed capacities before returning").
- The session state guard publishes the routes of a change that a panic cut short: Drop::drop runs during the
  unwind, before the MutexGuard field poisons the mutex, so the lane routes by half-made tables.

Red on this commit: the insert changes the map's pointer, and the routes snapshot is replaced.

* fix(cmux-tui-core): link peers and host proxy authors are published; the S1.1 review follow-ups (R-DAEMON-STATE S1 step 2)

A person's key line asked two more shared locks (DESIGN.md Appendix A.2 steps 3, 7, 12, 13): the link peers map
(the remote-client check and the request's actor) and the host proxy authors (the proxy-link check on the reader
thread). Every remote bind, disconnect and revocation sweep holds the first; every proxy bind and every client's
disconnect holds the second.

- published.rs moves to cmux-tui-util (daemon primitives, no daemon state) so cmux-tui-remote-access can use it.
  RemoteRelayState.peers becomes a Published copy of the records; peer_checked reads it with no lock when the
  mux's stage switch is on (Published::publishing), and a poisoned map still fails closed. AgentSessions.proxy_authors
  publishes the same way; proxy::author reads it. A disconnect of a client without a record publishes nothing.
- Review follow-ups of #34014 (review comment 6612546773):
  - RouteTable::try_reserve copies a map a published snapshot shares, before the durable commit, so the insert
    after the commit allocates nothing (ResourceMutationPlan::apply).
  - A change a panic cuts short is never published, by the state guard (routes) or by a PublishGuard: Drop skips
    the publication when a panic began while the guard held the lock (std's own poisoning test), so no reader
    sees half-made tables, not even before the mutex is poisoned.
  - A PublishGuard frees the projection it replaced after the unlock (as the state guard does).
  - The two poison tests of the relay run on both arms.
  - ClientFacts and types_into move to server/client_registry/facts.rs (god-file warning).

Same switch, CMUX_TUI_RDS_INPUT, default off: with it off nothing is published and every read takes its lock as
on the base.

* test(cmux-tui-core): a key waits for no creating-input lock, and for no session lock behind one (R-DAEMON-STATE S1 step 4 red)

Every key to a live terminal first asks whether creating input of its terminal must go first
(wait_for_creating_input_flush, DESIGN.md Appendix A.2 step 16). It takes the daemon-wide creating-input lock even
when no terminal is being created, and that lock's holders (begin_creating_input, knows_terminal_input_target,
finish_creating_input) look up the live terminal under it, which takes the session lock. The S1 census measured a
full 300 ms wait on the creating-input lock (hosted run 41648095321: 300.3 ms).

Two holders, each with an old-path control: the creating-input lock itself, and the session lock held while a
reader waits for it under the creating-input lock (with another terminal being created, so the key cannot skip the
lock). Red on this commit: the stage does not change these paths yet.

* fix(cmux-tui-core): a key skips the creating-input lock while nothing is creating; the lock becomes a leaf (R-DAEMON-STATE S1 step 4)

On the input lane (CMUX_TUI_RDS_INPUT, per mux, default off):

- CreatingInputs keeps the number of creating terminals in an atomic, stored under its lock after every insert and
  removal (Release). A key reads it (Acquire) and returns at once when it is zero: no queue can be ahead of it,
  because a terminal's queue is added before the terminal goes live and removed only after every queued byte was
  written to it.
- The creating-input lock's holders look up the live terminal in the published routes instead of under the session
  lock, so no holder waits for the session lock while it holds the creating-input lock, and a key that takes it
  never waits behind one. The routes carry the host-id catalog index for this: terminal_catalog_by_host becomes a
  copy-on-write route table in the RouteSnapshot, and one lookup (route_table::live_terminal) serves the session
  state and the snapshot. A commit publishes its routes before it releases the session lock, and finish runs after
  the commit, so begin, knows and finish give the answers they gave under the session lock.

Off, every path takes the locks it took on the base; the by-host index is copy-on-write on both arms, as the other
three route tables are since step 1.

* test(cmux-tui-core): the surface budget's limits on both arms; a lock control fails on a lost key (#34146 review follow-ups)

The #34146 review (comment 6613345630) noted:
- Every budget test used the mutex arm, so an atomic arm that admitted everything would have passed. The two
  mux-wide limit tests now run on both arms, and a new test checks the same limits on both: the 17th worker, the
  whole byte budget, one byte over, zero bytes, a sum that overflows, and released reservations coming back.
- The old-path control passed when a key never arrived (Duration::MAX >= BUDGET); it now fails on a lost key.
- ServerSurfaceOperationState and the admission's mutex are private (nothing outside the file reads them), and
  the doc says which requests reserve (pings, identify, v2 resource lines and reader-thread frame handlers do not).

* fix(cmux-tui-core): drop an unnecessary path qualification in a test (clippy unused_qualifications)

The lint (linux) job's clippy -D warnings failed on mux/tests/signaled_mutex.rs:120 (TerminalPublicId is in scope);
the #34174 review found it behind the base's acpmux red in the same job.

* test(cmux-tui-core): a poisoned session state never routes by the published tables (R-DAEMON-STATE S1 red)

The #34171 review noted: after a panic poisons Mux::state, the next guard that recovers the poison may publish the
half-made route tables, and the lane's Mux::surface reads the published routes with no poison check, where the old
path fails (it unwraps the lock). Red on this commit: with the lane on, a surface read after the poisoning returns
from the published routes instead of failing.

The relay poison tests now poison published values through a helper in the test file.

* fix(cmux-tui-core): the lane's surface read takes the old path on a poisoned state; the poison helper is test code (#34171 review)

Review comment 6614044759 (FIX at 8f7d405):
- published.rs: poison_for_test moved out of the test module with its panic! respelled as panic_any, which hides a
  hit from the crash ratchet's count. It is test code again: a private helper in the published tests and one in
  the relay tests, with plain panic!.
- Mux::surface on the lane reads the published routes only while the session state is not poisoned; a poisoned
  state takes the old read and fails as it does (StateMutex::is_poisoned). A guard that recovers the poison may
  publish the half-made tables of the change that panicked.
- Published::load debug-asserts that the value publishes: only a publishing value has a current projection.
- The module doc says what load gives on a poisoned value; the caller decides whether to fail closed.

The unused_qualifications error at mux/tests/signaled_mutex.rs:120 is fixed in 10f1669.

* fix(cmux-tui-core): the creating-input live check reads the routes only on an unpoisoned state; its comment states the lane's linearization (#34174 review)

Review comment 6613998735 (SHIP, notes): the begin_creating_input comment still said the check takes the state
under the creating lock; on the lane it reads the published routes, and its answer is one the old path can give
(a commit in progress has not made the id live, as when the old path takes the state before it). The lane's live
lookup now takes the old read on a poisoned state, as Mux::surface does since bb0265f.

* test(cmux-tui-core): a key waits for neither the sizing nor the activity lock; a key takes the grid back after another view's click (R-DAEMON-STATE S1 step 5 red)

After its PTY write a key notes activity in the terminal's sizing (the newest view sets the grid under the default
policy): it takes the daemon-wide client sizing lock twice (the note and the size-state publication), then the
VM activity stream's lock. Every attach, detach, resize and size report of every terminal holds the sizing lock,
and a PTY resize runs under it. The S1 census measured a full 300 ms wait on each (hosted run 41648095321: 300.2
and 300.3 ms).

The first test holds each lock 300 ms (with an old-path control each). The second keeps the fix honest: two views
of one terminal, the default policy; the other view's focus click takes the grid, and the next key from the
typing view must take it back, on both arms. Red on this commit: the first test (the stage does not change these
paths yet).

* fix(cmux-tui-util): a test dereferences the published u64 once (E0614 in bb0265f)

* fix(cmux-tui-core): a key skips its sizing note when its view is the settled newest; the person's input time is an atomic (R-DAEMON-STATE S1 step 5)

On the input lane (CMUX_TUI_RDS_INPUT, per mux, default off):

- Each terminal's sizing entry publishes, on the terminal's runtime (shared by every placement), the view a key may
  skip its sizing note for: the sole newest participant, a client's own view, only while the entry is settled (no
  grid hold, no handheld with a viewport, and the engine holds the grid or the applied grid and participants are its
  decision). Noting activity for that view changes no decision under any policy, and the size state carries no
  activity, so the key compares (client, placement) with one atomic load and takes no sizing lock.
- The publication is automatic: Mux::client_sizing is a SizingMutex whose guard republishes, on drop and under the
  lock, each entry its holder read or wrote (SizingEntries marks them; an apply changes Cells through a shared
  borrow, so reads count). A removed entry clears its slot; a change a panic cuts short publishes nothing.
- Another view's activity makes that view the newest and republishes, so the next key from the first view takes the
  slow path once and takes the grid back (the second test pins this on both arms).
- ActivityStream::note_user_input stores the time in an atomic; it takes the stream lock only while a subscriber
  exists, once per event the worker sends (a wake_pending flag the worker clears before it reads the time).
- TerminalSizingEngine::sole_latest (cmux-terminal-sizing) with a unit test; AppliedSeat::of shares the seat rule
  between the apply and the settled check; set_workspace_size_policy reads its state before the note (no index).

Off, every key takes the sizing and activity locks as on the base; the marks and the guard check one bool.

* fix(cmux-tui-core): the sizing test hook went in above another test hook's cfg(test) (dead code and 2 expect hits in production)

dfe81bf put hold_client_sizing_for_test between pin_latest_size_policy_for_test's doc and its #[cfg(test)], so
pin_latest_size_policy_for_test compiled into production: clippy -D warnings dead_code, and the panic ratchet's
cmux-tui-core expect_used 175 > 173 (hosted lint job 126202464363).

* fix(cmux-tui-core): the lane's surface read loads the routes before its poison check; two test doc comments on their items (#34171 review 2)

Review comment 6615927915 (SHIP at 0fb7c43, notes): Mux::surface checked is_poisoned() and then loaded the
routes; between the two another thread could poison the state and a third, recovering the poison, publish half-made
tables. Poison never clears on the session state, so routes loaded before an unpoisoned check were published before
any poisoning: load first, then check. Two doc comments in remote_relay/tests.rs sat above the wrong item.

* test(cmux-tui-core): a key does not wait for another writer's blocked host socket (R-DAEMON-STATE S1 step 6 red)

A key to a hosted terminal holds the terminal's runtime lock and its HostAttachment.writer across a blocking write
to the host socket (surface/input.rs, attachment.rs send_host_frame). Another client's large paste to the same
terminal holds both for its whole write, bounded only by the socket's send timeout, so the person's key waits for
automation's work on the same terminal (DESIGN.md section 2.6.1, Appendix A.2 steps 20-22).

The test pastes 8 MiB while the fake host reads nothing for 600 ms, then sends one key. A control first proves the
old path waits (the key takes at least half the stall); the lane must return the key in < 100 ms, and the host must
then read the paste before the key, and the key once. Red on this commit: the lane has no input port yet.

* fix(cmux-tui-core): a person's key to a hosted terminal goes to the terminal's input port and never waits for a socket write (R-DAEMON-STATE S1 step 6)

On the input lane (CMUX_TUI_RDS_INPUT, per mux, default off), interactive input to a hosted terminal
(Surface::write_bytes, Surface::write_paste) goes to the terminal's own input port (surface/input_port.rs): one
queue per terminal, drained in order by the terminal's writer thread (terminal-input-port, started on the first
input, ended after 30 s idle). The request returns once the port took the bytes; it takes neither the terminal's
runtime lock nor the host writer, so another writer of the same terminal (a large paste, receipted input, a
cell-pixel change waiting for its acknowledgement) and a host that does not read its socket no longer hold the
person's key or the connection's dispatcher.

- The writer writes on the route of the current host connection (HostInputRoute: the connection's writer), which
  the terminal publishes whenever its runtime changes: the hosted and exited constructors, and
  PtyTerminalRuntime::replace_runtime, which the reconnect install and the host exit now use. So the writer never
  takes the runtime lock either.
- A lost connection is retried as write_bytes did (cx-6so.49), on the port's thread: wait for the reader's next
  stream change, write to the new host, until HOST_INPUT_RECONNECT_WAIT.
- A full port (32 MiB queued) refuses at once (terminal.input_busy, WouldBlock); a write larger than one host frame
  is refused as before; the exited final screen drops input as before.
- Input the port took and then could not write is lost as a failed write was, with a log line: the request was
  already answered (the reply now means "accepted by the terminal's port", DESIGN.md 2.6.1).
- Not in this step (S1.6b, before the default flip): the connection's other frames still write directly, so while
  the port is behind they can go before a key it still holds.

Off, every write takes the path it took on the base; the route is published on both arms (one Arc per change).

* test(cmux-tui-core): after a respawn, keys skip the sizing lock again; the lock tests report a viewport (#34261 review red)

Review comment 6619473751 (FIX at 846d412):
1. A respawn under the same runtime id keeps the sizing entry, which keeps the dead runtime's fast-path slot: the
   new runtime's slot is never published, so every later key takes Mux::client_sizing twice. The new test respawns
   the typing terminal, sends one key, then holds the sizing lock: the next keys must not wait. Red on this
   commit.
2. The lock tests' view never reported a size, so the engine held the grid and the skip went only through that
   branch. The typing view now reports 100x30 as the app's view does, so its keys skip through the settled-grid
   check.

* fix(cmux-tui-core): a sizing entry follows its terminal across a respawn; the input time never goes back (#34261 review)

Review comment 6619473751 (FIX at 846d412):
1. A respawn under the same runtime id keeps the sizing entry but builds a new terminal runtime with a new
   fast-path slot. TerminalSizingEntry::follow_runtime_of re-points the entry at the slot of the runtime its keys
   reach, under the sizing lock, and clears the old slot: on the key's slow path (note_terminal_activity_of has the
   key's surface) and on every terminal_sizing_entry call (attach, claim, size report). The red test
   after_a_respawn_keys_wait_for_no_sizing_lock_again (4bf5af5) pins it.
3. The entry takes or follows a slot only from a surface whose runtime id is the entry's, so two entries never
   share one slot.
4. The lock-free input time is fetch_max, so a key preempted after its clock read never moves it back.
7. The poison comment says what happens: a later holder that recovers the poison republishes what it touches.

Lock order is unchanged: terminal_sizing_entry now looks the surface up on each call (it did at creation, and
apply_terminal_grid does), under the sizing lock, the established sizing-then-session order.

* fix(cmux-tui-core): name the input port module without super:: where surface::* is in scope (clippy unused_qualifications)

* test(cmux-tui-core): a settled size-state read takes the session lock once with the switch off (#34261 review 2 red)

The sizing entry looks its surface up on every access since 5fe770d, so with the switch off a size-state read
takes the session lock twice (once under the sizing lock): expected red at this commit (lane off: 2, not 1).

* fix(cmux-tui-core): a sizing entry looks its surface up only when it is made; the respawn follow runs only on the lane (#34261 review 2)

5fe770d looked the surface up on every sizing-entry access, so with the switch off (the default) every attach,
size report, claim and size-state read took the session lock under the sizing lock. The surface is looked up only to
make the entry again; a respawn's new runtime is followed by its first key's sizing note, which holds the surface,
and only while the switch is on. An unchanged fast path is not stored again. The respawn test pins the old slot's
clear.

* fix(cmux-tui-core): the input port's order test can fail; the port copies outside its lock, counts each frame, recovers from a writer panic and refuses a failed link (#34544 review)

- The test compared two Options (None < Some), so a key that passed the paste still passed; both positions are
  required now. A new test sends 26 keys on the lane and checks the host reads each once, in order.
- enqueue copies the bytes before the port lock, and each queued frame counts 64 bytes of bookkeeping against the
  32 MiB capacity, so many tiny frames cannot grow the queue without bound.
- A writer that unwinds drops the frames after the one it lost (logged) and marks itself stopped, so the next input
  starts a new writer instead of queueing behind a writer that is gone.
- A host connection the reader gave up on (Failed) refuses input at once, as the old path's failed write did.
- The deadlines use checked_add (no new arithmetic_side_effects hits); the thread name fits Linux's 15 bytes.

* fix(cmux-tui-core): the input port refuses only when the host reader ended, not while a Failed link still retries (#34544 review 2)

A Failed link keeps retrying its host, and a successful retry installs the live replacement before the state
leaves Failed; the old path wrote to it, the lane refused. The lane now refuses only when the reader ended (no
connection will come back), where the old path's write fails too. The capacity holds two of the largest frames
with their per-frame cost; the 26-key test also checks that no key is written twice.

* fix(cmux-tui-core): a key to a failed host link is refused at once on the input port, as on the old path (#34544 review 3)

When the reader moves a link to Failed it shuts the connection down (and keeps retrying); the old path's write then
failed and `cmux send` exited non-zero, while the port took the key and only logged its failure. The reader now
publishes InputRoute::Failed under the runtime lock when it shuts the connection down; the port refuses it with
NotConnected, and the replacement's route (replace_runtime) is accepted as soon as it is installed. Also the 26-key
test's tail loop is a while-let (clippy::while_let_loop).

Test: a_key_to_a_failed_link_is_refused_until_a_live_route_is_published.

* fix(cmux-tui-core): a failed reconnect completion on a failed link also makes the input port refuse (#34544 review 4)

A retry that installed a replacement and then failed to complete it (terminal_host_reconnected false) shuts the
replacement down while the link stays Failed; the route was still the shut replacement, so the port took keys and
only logged them. That path now publishes InputRoute::Failed under the runtime lock too, as the Failed transition
does. Not driven by a test: the hosted fixture's live reader thread reacts to the disconnect itself; the route
behavior is covered by a_key_to_a_failed_link_is_refused_until_a_live_route_is_published.

* test(cmux-tui-core): a frame sent after a key the input port holds must reach the host after it (R-DAEMON-STATE S1.6b red)

The port counts the frames it took and finished and offers PortOrder::barrier; nothing calls it yet, so a direct
frame (here an Activate under the runtime lock, as viewer sizes and the other direct writers send) passes a key
the paused port still holds: expected red in a_frame_sent_after_a_key_the_port_holds_reaches_the_host_after_it.

* fix(cmux-tui-core): every other frame to a hosted terminal's host goes after the keys its input port took (R-DAEMON-STATE S1.6b order)

HostAttachment gets the terminal's PortOrder (set by the hosted constructor and replace_runtime, so it follows
every connection change); each frame it writes besides the port's own (send, receipted input, viewer size, cell
pixels, Kitty limits, control requests such as clear history and terminate, and the clipboard replier's replies)
first waits until the port finished every frame it had taken (PortOrder::barrier, at most 2 s, as one frame write
may block). The wait returns at once while the port's writer waits for a reconnect, since the install it waits
for may need the runtime lock the caller holds. With the switch off the port takes nothing and the wait is one
uncontended lock. The 9 test literals of HostAttachment take input_order: None.

Test: a_frame_sent_after_a_key_the_port_holds_reaches_the_host_after_it (red at 8b9f34a).

* test(cmux-tui-core): an automation write answers after the input port wrote it, with its frame's error (R-DAEMON-STATE S1.6b red)

On the lane every hosted write answered when the port took the bytes, so the send handler, the creating-input
flush (its give-back) and the resource input effect (its indeterminate mark) never saw a write error. Expected red:
an_automation_write_answers_after_the_port_wrote_it (it answers while the port is paused) and
an_automation_write_gets_the_error_of_its_frame (ok for bytes never written).

* fix(cmux-tui-core): on the input lane only the person's keys answer before the write; every other write answers with its frame's result (R-DAEMON-STATE S1.6b)

S1.6a answered every hosted write when the port took it, so the send handler for scripts, the creating-input
flush (its give-back), the resource input effect (its indeterminate mark) and the image paste (its reservation)
never saw a write error. Now Surface::write_bytes and write_paste answer when the port wrote the frame
(InputReply::Written: the port reports each frame's result on a channel, bounded at 15 s); only the person's key
(the send and send-key handlers, when the client is attached to the terminal it types into) passes
InputReply::Accepted and returns at once. The port also keeps one reconnect wait for its queue: frames queued
behind a host that does not come back fail together, not 5 s each. The give-back notice of the plan is dropped
(DESIGN 2.6.1: the app reads it only on its control connection, and it never retried a failed key).

Tests: an_automation_write_answers_after_the_port_wrote_it and an_automation_write_gets_the_error_of_its_frame
(red at 189ba58); the_persons_keys_answer_when_taken_and_other_writes_when_written; the person-key tests pass
InputReply::Accepted.

* test(cmux-tui-core): the input port's reconnect and reply rules from review 1 of #35082 (R-DAEMON-STATE S1.6b red)

Red:
- a_frame_sent_after_a_replacement_install_waits_for_the_keys_the_port_held (must-fix 1): after replace_runtime and
  before the reader's stream notify, a direct frame on the new connection passes the key the port held.
- a_later_host_loss_waits_for_its_own_reconnect (must-fix 2): the first loss's deadline outlives it, so a later
  loss's write fails at once.
- a_write_whose_wait_ends_before_the_port_takes_it_is_never_written, a_write_whose_wait_ends_during_its_write_may_be_written
  and the_creating_input_flush_keeps_input_that_may_have_been_written (item 6): a timed-out write stays queued and
  is written later, and the flush gives back bytes that may still be written.
Guards (pass before and after): a_frame_sent_while_the_port_waits_for_a_reconnect_does_not_wait_for_it,
writes_queued_behind_a_lost_host_fail_together, receipted_input_sent_after_a_key_the_port_holds_reaches_the_host_after_it.
an_automation_write_gets_the_error_of_its_frame waits for the port's count instead of sleeping 100 ms.

Test seams: the port's reply and reconnect waits (test overrides), its reconnecting/accepted/queued counts, and the
InputMayBeWritten marker with its classifier (nothing produces the marker yet).

* fix(cmux-tui-core): a route publication ends the input port's reconnect wait; each frame waits for a reconnect from when the port took it; a timed-out write is cancelled or marked possibly written (#35082 review 1)

- Must-fix 1: replace_runtime and input_route_failed call InputPort::route_changed. A port-owned route epoch ends
  the writer's reconnect wait and clears reconnecting under the port lock, so after the reader installs a live
  replacement the port writes the keys it holds to it before any other frame (before, the writer slept on the
  stream revision until the reader's notify after the VT replay, and the barrier skipped meanwhile).
- Must-fix 2: each frame waits for a reconnect until its own deadline, HOST_INPUT_RECONNECT_WAIT after the port took
  it, as the old path's write waited from its call. The queue-wide reconnect_until outlived its loss, so a later
  loss's writes failed at once. Writes queued behind a host that does not come back still fail together.
- Item 6: InputPort::write cancels a frame its waiter gave up on before the writer took it (the error says not
  written, and it never is); a wait that ends during the write answers InputMayBeWritten, and the creating-input
  flush does not give those bytes back (that typed them twice).
- Item 5: the order barrier ends by the caller's own deadline (control requests, cell pixels).
- Item 3: the order-wait comment states the bound; the clipboard replier uses the same deadline.
- Item 8: the port and input docs describe the current rules.

Tests (red at 2133c10): a_frame_sent_after_a_replacement_install_waits_for_the_keys_the_port_held,
a_later_host_loss_waits_for_its_own_reconnect, a_write_whose_wait_ends_before_the_port_takes_it_is_never_written,
a_write_whose_wait_ends_during_its_write_may_be_written, the_creating_input_flush_keeps_input_that_may_have_been_written.

* test(cmux-tui-core): the review-1 port tests name Frame and input_may_be_written through the module scope (clippy unused_qualifications)

lint (linux) at bbc44c2 (runs 41806402684, 41806407494) failed clippy -D warnings on four qualified paths in
surface/tests/hosted.rs; both names are in scope through use super::*.

* test(cmux-tui-core): a write waiting for a reconnect fails when the reader gives up (R-DAEMON-STATE S1.6c red, #35082 review 2)

The reader's end, give-up and failed-reconnect paths now call PtyTerminalRuntime::reader_gave_up, which still
only notifies stream_progress (today's behavior). The port's writer waits on its route epoch since #35082 review 1,
so the wake does not reach it: a_write_waiting_for_a_reconnect_fails_when_the_reader_gives_up waits its full
reconnect deadline (5 s) instead of failing at once.

* fix(cmux-tui-core): the reader's end, give-up and the owner's detach end the input port's reconnect wait (R-DAEMON-STATE S1.6c, #35082 review 2)

Since review 1 the port's writer waits on its own route epoch, so the reader's stream_progress wake no longer
reached it: a Written write behind a host the reader gave up on failed only at its reconnect deadline (5 s), not at
once as the old path's write did. InputPort::reconnect_ended moves the epoch with no new route; the writer checks the
route and the terminal's state again and fails its frame (Failed state, ended reader, or owner detaching).
- PtyTerminalRuntime::reader_gave_up (WakeInputOnReaderEnd, wait_or_fail's give-up, a failed reconnect completion)
  notifies the stream and ends the port's wait.
- The four owner_detaching stores in shutdown.rs end the port's wait (only the port lock, under the runtime lock).

Test: a_write_waiting_for_a_reconnect_fails_when_the_reader_gives_up (red at b6b0b37).

---------

Co-authored-by: cmux-bot[bot] <348531757+cmux-bot[bot]@users.noreply.github.com>
lawrencecchen added a commit that referenced this pull request Nov 4, 2026
… without waiting; the settle ends by its deadline; a lost taken frame may be written (R-DAEMON-STATE S1.6c part 2) (#35375)

* test(cmux-tui-core): the person's key waits for no shared lock (R-DAEMON-STATE S1 red)

A key line from an attached view goes through the real connection path (the
line handler, the connection's dispatcher, the send handler) while another
thread holds one shared lock for 300 ms. #31940 took Mux.state off the key;
the key still waits for the client registry lock (five takes per line), the
link peers lock, the host proxy authors lock, the surface admission lock, the
creating-input lock, the client sizing lock and the activity lock. Their other
holders are every connect, attach, resize and request of every client.
Mux.state and the workspace registry rows are controls (pass on the base).

Two keys go in a row, so a lock taken after the PTY write (sizing, activity)
shows as a delay of the second key on the connection's dispatcher.

Test-only hold hooks (cfg(test)) in creating_input.rs, terminal_sizing.rs,
activity.rs and agent_session_attach.rs. No product change.

Design: handoff/rewrites/R-DAEMON-STATE/DESIGN.md section 2.6 (stage S1).

* test(cmux-tui-core): a key waits for no client registry lock (R-DAEMON-STATE S1 step 1 red)

The previous commit's census test held nine shared locks in turn; this commit
splits it by fix step. Step 1 is the client registry lock (ClientRegistry.state),
which a key line takes about nine times (handoff checks, remote-client and
remote-device checks, the request's actor, the kept terminal, the reply's
incognito filter) while every connect, disconnect, attach and detach of every
client holds it.

The test turns on the stage switch CMUX_TUI_RDS_INPUT (rds_switch.rs, inert in
this commit) and drives two keys through the real connection path while another
thread holds the lock for 300 ms. A second test keeps Mux.state and the
workspace registry off the key on the new path (#31940).

The hold hooks of the other locks leave with this commit; they come back with
their fix steps.

* fix(cmux-tui-core): the person's key reads the client registry's published facts, never its lock (R-DAEMON-STATE S1 step 1)

A key line took the client registry lock (ClientRegistry.state) about nine
times: the daemon handoff checks, the remote-client and remote-device checks,
the request's actor, the kept terminal of the key and the reply's incognito
filter. Every connect, disconnect, attach, detach and capability change of every
client holds that lock, so an agent storm held back the person's keys (red at
2472251: 300.4 ms behind a 300 ms hold; STATUS 9131).

The registry is now a Published value (published.rs): writers still serialize
on its mutex, and a guard that changed the value publishes an immutable
ClientFacts projection (handoff state; per connection its transport, remote
device flag, base actor and kept input terminals) with arc-swap when it drops,
under the mutex, so projections follow the order of the changes. With the stage
switch CMUX_TUI_RDS_INPUT=1, the line and key paths read the projection with
one atomic load: daemon_handoff_in_progress/committed, transport_of (so
is_remote_client), is_remote_device (refuse_line, line_to_recheck,
prune_reply), connection_actor and attached_input_surface. A poisoned registry
still fails closed (the projection comes in the error, as Mutex::lock gives
its guard). With the switch off nothing changes.

A projection costs O(connections) under the registry lock per change, bounded
by the 64-connection cap; stage S2 makes it O(changed).

arc-swap 1.9 is already in the lockfile and the shipped notices (through
iroh-dns); cmux-tui-core now names it.

Tests: rds_input_lane (a key waits for no client registry lock; the session and
workspace registry locks stay off; a respawn under the same id sends the next
key to the new shell), published.rs unit tests (a change publishes on drop, a
read publishes nothing, a reader never waits for a writer, a poisoned value
fails closed). The two remote-relay poison tests poison through the new type.

* fix(cmux-tui-core): the key reads the session's published routes; one stage switch per mux (R-DAEMON-STATE S1 step 1)

The hosted run of f1ea1b6 (https://github.com/manaflow-ai/cmux/actions/runs/41655768031) still failed
both lane tests: the key waited 300 ms for the client registry and, new, for Mux.state. Two causes:

- The key still used the kept terminal of #31940. That cache is validated by process-wide route
  generations, and cargo test runs the tests of one binary as threads of one process: another test's
  mux moved the same generation slot (ids start at 1 in every mux), the kept terminal looked stale, and
  the key fell back to the locked lookup and then to remember_attached_input (registry lock). The
  stage switch was a process-wide static too.
- So the lane must not depend on a cache with process-wide invalidation at all.

Now, with CMUX_TUI_RDS_INPUT=1:
- The session's three routing tables (surfaces, terminal_catalog, terminal_catalog_by_runtime) are
  copy-on-write behind an Arc. A state guard that changed one publishes a RouteSnapshot (three Arc
  clones) with arc-swap when it drops, under the lock, so publications follow the changes; the
  snapshot it replaced is freed after the lock is released. Mux::surface reads the snapshot with one
  atomic load. A respawn, close, move, rollback or projection is seen once its guard is released, as a
  reader that waited for the lock would see it. Nothing is kept per client, so nothing is invalidated.
- input_surface on the lane: the published routes give the terminal, the registry's published facts
  say whether the client types into it (types_into: the person's input QoS mark). attached_input_surface
  and remember_attached_input stay only on the old path.
- The switch is per mux (rds_switch::InputLaneSwitch, read once from the environment when the mux is
  built; Mux::set_input_lane_for_test in tests), and it also decides whether anything is published: off,
  the registry builds no projection and the state holds no route snapshot, so a route write never
  copies a shared table and the off arm costs what the base costs.

Cost on the lane: a guard's first route write copies that table once (O(routes)), bounded by one copy
per changing guard; State::clone no longer deep-copies the three tables. Stage S2 makes both O(log n).

* test(cmux-tui-core): a key waits for no surface admission lock, with an old-path control (R-DAEMON-STATE S1 step 3 red)

Every request of every connection reserves its retained bytes from one
daemon-wide budget (ServerSurfaceOperationAdmission) under one mutex before it
is queued, and releases them under it after it ran. The S1 census measured a
key waiting 300.3 ms behind a 300 ms hold of that lock (hosted run 41648095321).

The lane test now has a control: the same two keys with stage S1 off must
wait for the held lock, so a lane result can never come from a hold that does
not reach the key path (a vacuous green). The control is added to the client
registry test too. A test-only hook (cfg(test)) holds the admission lock.

Red here: with the lane on, the key still reserves through the mutex.

* fix(cmux-tui-core): the surface admission budget reserves with atomics on the input lane (R-DAEMON-STATE S1 step 3)

Every request of every connection reserves its retained bytes (and every
clear-history its worker) from one daemon-wide budget before it is queued and
releases after it ran. Under one mutex, a key waited for any other
connection's reservation or release (red at 5d0e4a0: 300 ms behind a
300 ms hold).

With CMUX_TUI_RDS_INPUT=1 the two counters are atomics with compare-and-swap
reservations (fetch_update: add only while the sum stays within the capacity;
release saturates at zero), so nothing waits. The limits and the refusals are
the same as the mutex arm's: a worker is refused at 16 reserved, bytes when
the sum would pass 16 MiB. Each permit releases to the arm it reserved from.
With the switch off the mutex arm runs as before; it goes with the switch when
the stage passes (S1.8). The arm is fixed when the mux is built
(ServerSurfaceOperationAdmission::new(publish_routes)); tests set it with
Mux::set_input_lane_for_test before any request.

The two scheduler tests that counted reserved workers through the mutex read
reserved_workers() (both arms).

* test(cmux-tui-core): a key waits for neither the link peers nor the proxy authors lock (R-DAEMON-STATE S1 step 2 red)

Two more shared locks on the person's key line (DESIGN.md Appendix A.2 steps 3, 7, 12 and 13): the link peers map
(RemoteRelayState.peers: the remote-client check and the request's actor) and the host proxy authors
(AgentSessions.proxy_authors: the proxy-link check on the reader thread). The S1 census measured a full 300 ms wait
on each (hosted run 41648095321: 300.5 ms and 300.4 ms).

The test holds each lock 300 ms on another thread while two keys go through the real line path. It first proves
the hold reaches the old path (stage switch off: the key waits), then expects the stage's path not to wait. Red on
this commit: the stage does not publish either value yet.

* test(cmux-tui-core): a change cut short by a panic is never published (R-DAEMON-STATE S1 red)

A published value's guard publishes its projection in Drop::drop, which also runs while a panic unwinds, before
its MutexGuard field drops and marks the mutex poisoned. A writer that panics in the middle of a change so
publishes the half-made value, and in the moment between the publication and the poisoning a reader gets it as a
good value: a fail-open window for readers that fail closed on a poisoned value (transport_of, peer_checked).

Red on this commit: the test sees the half-made value 9 in the poisoned projection.

* test(cmux-tui-core): a reservation unshares a published route map; a panicked change publishes no routes (R-DAEMON-STATE S1 red)

Two defects the #34014 review found on the input lane (both only with CMUX_TUI_RDS_INPUT on):

- RouteTable::try_reserve returns early when the map has room, also when a published route snapshot shares the
  map. The insert after the durable commit then copies the whole table (Arc::make_mut), an allocation that
  ResourceMutationPlan::apply forbids ("Plan builders reserve all needed capacities before returning").
- The session state guard publishes the routes of a change that a panic cut short: Drop::drop runs during the
  unwind, before the MutexGuard field poisons the mutex, so the lane routes by half-made tables.

Red on this commit: the insert changes the map's pointer, and the routes snapshot is replaced.

* fix(cmux-tui-core): link peers and host proxy authors are published; the S1.1 review follow-ups (R-DAEMON-STATE S1 step 2)

A person's key line asked two more shared locks (DESIGN.md Appendix A.2 steps 3, 7, 12, 13): the link peers map
(the remote-client check and the request's actor) and the host proxy authors (the proxy-link check on the reader
thread). Every remote bind, disconnect and revocation sweep holds the first; every proxy bind and every client's
disconnect holds the second.

- published.rs moves to cmux-tui-util (daemon primitives, no daemon state) so cmux-tui-remote-access can use it.
  RemoteRelayState.peers becomes a Published copy of the records; peer_checked reads it with no lock when the
  mux's stage switch is on (Published::publishing), and a poisoned map still fails closed. AgentSessions.proxy_authors
  publishes the same way; proxy::author reads it. A disconnect of a client without a record publishes nothing.
- Review follow-ups of #34014 (review comment 6612546773):
  - RouteTable::try_reserve copies a map a published snapshot shares, before the durable commit, so the insert
    after the commit allocates nothing (ResourceMutationPlan::apply).
  - A change a panic cuts short is never published, by the state guard (routes) or by a PublishGuard: Drop skips
    the publication when a panic began while the guard held the lock (std's own poisoning test), so no reader
    sees half-made tables, not even before the mutex is poisoned.
  - A PublishGuard frees the projection it replaced after the unlock (as the state guard does).
  - The two poison tests of the relay run on both arms.
  - ClientFacts and types_into move to server/client_registry/facts.rs (god-file warning).

Same switch, CMUX_TUI_RDS_INPUT, default off: with it off nothing is published and every read takes its lock as
on the base.

* test(cmux-tui-core): a key waits for no creating-input lock, and for no session lock behind one (R-DAEMON-STATE S1 step 4 red)

Every key to a live terminal first asks whether creating input of its terminal must go first
(wait_for_creating_input_flush, DESIGN.md Appendix A.2 step 16). It takes the daemon-wide creating-input lock even
when no terminal is being created, and that lock's holders (begin_creating_input, knows_terminal_input_target,
finish_creating_input) look up the live terminal under it, which takes the session lock. The S1 census measured a
full 300 ms wait on the creating-input lock (hosted run 41648095321: 300.3 ms).

Two holders, each with an old-path control: the creating-input lock itself, and the session lock held while a
reader waits for it under the creating-input lock (with another terminal being created, so the key cannot skip the
lock). Red on this commit: the stage does not change these paths yet.

* fix(cmux-tui-core): a key skips the creating-input lock while nothing is creating; the lock becomes a leaf (R-DAEMON-STATE S1 step 4)

On the input lane (CMUX_TUI_RDS_INPUT, per mux, default off):

- CreatingInputs keeps the number of creating terminals in an atomic, stored under its lock after every insert and
  removal (Release). A key reads it (Acquire) and returns at once when it is zero: no queue can be ahead of it,
  because a terminal's queue is added before the terminal goes live and removed only after every queued byte was
  written to it.
- The creating-input lock's holders look up the live terminal in the published routes instead of under the session
  lock, so no holder waits for the session lock while it holds the creating-input lock, and a key that takes it
  never waits behind one. The routes carry the host-id catalog index for this: terminal_catalog_by_host becomes a
  copy-on-write route table in the RouteSnapshot, and one lookup (route_table::live_terminal) serves the session
  state and the snapshot. A commit publishes its routes before it releases the session lock, and finish runs after
  the commit, so begin, knows and finish give the answers they gave under the session lock.

Off, every path takes the locks it took on the base; the by-host index is copy-on-write on both arms, as the other
three route tables are since step 1.

* test(cmux-tui-core): the surface budget's limits on both arms; a lock control fails on a lost key (#34146 review follow-ups)

The #34146 review (comment 6613345630) noted:
- Every budget test used the mutex arm, so an atomic arm that admitted everything would have passed. The two
  mux-wide limit tests now run on both arms, and a new test checks the same limits on both: the 17th worker, the
  whole byte budget, one byte over, zero bytes, a sum that overflows, and released reservations coming back.
- The old-path control passed when a key never arrived (Duration::MAX >= BUDGET); it now fails on a lost key.
- ServerSurfaceOperationState and the admission's mutex are private (nothing outside the file reads them), and
  the doc says which requests reserve (pings, identify, v2 resource lines and reader-thread frame handlers do not).

* fix(cmux-tui-core): drop an unnecessary path qualification in a test (clippy unused_qualifications)

The lint (linux) job's clippy -D warnings failed on mux/tests/signaled_mutex.rs:120 (TerminalPublicId is in scope);
the #34174 review found it behind the base's acpmux red in the same job.

* test(cmux-tui-core): a poisoned session state never routes by the published tables (R-DAEMON-STATE S1 red)

The #34171 review noted: after a panic poisons Mux::state, the next guard that recovers the poison may publish the
half-made route tables, and the lane's Mux::surface reads the published routes with no poison check, where the old
path fails (it unwraps the lock). Red on this commit: with the lane on, a surface read after the poisoning returns
from the published routes instead of failing.

The relay poison tests now poison published values through a helper in the test file.

* fix(cmux-tui-core): the lane's surface read takes the old path on a poisoned state; the poison helper is test code (#34171 review)

Review comment 6614044759 (FIX at 8f7d405):
- published.rs: poison_for_test moved out of the test module with its panic! respelled as panic_any, which hides a
  hit from the crash ratchet's count. It is test code again: a private helper in the published tests and one in
  the relay tests, with plain panic!.
- Mux::surface on the lane reads the published routes only while the session state is not poisoned; a poisoned
  state takes the old read and fails as it does (StateMutex::is_poisoned). A guard that recovers the poison may
  publish the half-made tables of the change that panicked.
- Published::load debug-asserts that the value publishes: only a publishing value has a current projection.
- The module doc says what load gives on a poisoned value; the caller decides whether to fail closed.

The unused_qualifications error at mux/tests/signaled_mutex.rs:120 is fixed in 10f1669.

* fix(cmux-tui-core): the creating-input live check reads the routes only on an unpoisoned state; its comment states the lane's linearization (#34174 review)

Review comment 6613998735 (SHIP, notes): the begin_creating_input comment still said the check takes the state
under the creating lock; on the lane it reads the published routes, and its answer is one the old path can give
(a commit in progress has not made the id live, as when the old path takes the state before it). The lane's live
lookup now takes the old read on a poisoned state, as Mux::surface does since bb0265f.

* test(cmux-tui-core): a key waits for neither the sizing nor the activity lock; a key takes the grid back after another view's click (R-DAEMON-STATE S1 step 5 red)

After its PTY write a key notes activity in the terminal's sizing (the newest view sets the grid under the default
policy): it takes the daemon-wide client sizing lock twice (the note and the size-state publication), then the
VM activity stream's lock. Every attach, detach, resize and size report of every terminal holds the sizing lock,
and a PTY resize runs under it. The S1 census measured a full 300 ms wait on each (hosted run 41648095321: 300.2
and 300.3 ms).

The first test holds each lock 300 ms (with an old-path control each). The second keeps the fix honest: two views
of one terminal, the default policy; the other view's focus click takes the grid, and the next key from the
typing view must take it back, on both arms. Red on this commit: the first test (the stage does not change these
paths yet).

* fix(cmux-tui-util): a test dereferences the published u64 once (E0614 in bb0265f)

* fix(cmux-tui-core): a key skips its sizing note when its view is the settled newest; the person's input time is an atomic (R-DAEMON-STATE S1 step 5)

On the input lane (CMUX_TUI_RDS_INPUT, per mux, default off):

- Each terminal's sizing entry publishes, on the terminal's runtime (shared by every placement), the view a key may
  skip its sizing note for: the sole newest participant, a client's own view, only while the entry is settled (no
  grid hold, no handheld with a viewport, and the engine holds the grid or the applied grid and participants are its
  decision). Noting activity for that view changes no decision under any policy, and the size state carries no
  activity, so the key compares (client, placement) with one atomic load and takes no sizing lock.
- The publication is automatic: Mux::client_sizing is a SizingMutex whose guard republishes, on drop and under the
  lock, each entry its holder read or wrote (SizingEntries marks them; an apply changes Cells through a shared
  borrow, so reads count). A removed entry clears its slot; a change a panic cuts short publishes nothing.
- Another view's activity makes that view the newest and republishes, so the next key from the first view takes the
  slow path once and takes the grid back (the second test pins this on both arms).
- ActivityStream::note_user_input stores the time in an atomic; it takes the stream lock only while a subscriber
  exists, once per event the worker sends (a wake_pending flag the worker clears before it reads the time).
- TerminalSizingEngine::sole_latest (cmux-terminal-sizing) with a unit test; AppliedSeat::of shares the seat rule
  between the apply and the settled check; set_workspace_size_policy reads its state before the note (no index).

Off, every key takes the sizing and activity locks as on the base; the marks and the guard check one bool.

* fix(cmux-tui-core): the sizing test hook went in above another test hook's cfg(test) (dead code and 2 expect hits in production)

dfe81bf put hold_client_sizing_for_test between pin_latest_size_policy_for_test's doc and its #[cfg(test)], so
pin_latest_size_policy_for_test compiled into production: clippy -D warnings dead_code, and the panic ratchet's
cmux-tui-core expect_used 175 > 173 (hosted lint job 126202464363).

* fix(cmux-tui-core): the lane's surface read loads the routes before its poison check; two test doc comments on their items (#34171 review 2)

Review comment 6615927915 (SHIP at 0fb7c43, notes): Mux::surface checked is_poisoned() and then loaded the
routes; between the two another thread could poison the state and a third, recovering the poison, publish half-made
tables. Poison never clears on the session state, so routes loaded before an unpoisoned check were published before
any poisoning: load first, then check. Two doc comments in remote_relay/tests.rs sat above the wrong item.

* test(cmux-tui-core): a key does not wait for another writer's blocked host socket (R-DAEMON-STATE S1 step 6 red)

A key to a hosted terminal holds the terminal's runtime lock and its HostAttachment.writer across a blocking write
to the host socket (surface/input.rs, attachment.rs send_host_frame). Another client's large paste to the same
terminal holds both for its whole write, bounded only by the socket's send timeout, so the person's key waits for
automation's work on the same terminal (DESIGN.md section 2.6.1, Appendix A.2 steps 20-22).

The test pastes 8 MiB while the fake host reads nothing for 600 ms, then sends one key. A control first proves the
old path waits (the key takes at least half the stall); the lane must return the key in < 100 ms, and the host must
then read the paste before the key, and the key once. Red on this commit: the lane has no input port yet.

* fix(cmux-tui-core): a person's key to a hosted terminal goes to the terminal's input port and never waits for a socket write (R-DAEMON-STATE S1 step 6)

On the input lane (CMUX_TUI_RDS_INPUT, per mux, default off), interactive input to a hosted terminal
(Surface::write_bytes, Surface::write_paste) goes to the terminal's own input port (surface/input_port.rs): one
queue per terminal, drained in order by the terminal's writer thread (terminal-input-port, started on the first
input, ended after 30 s idle). The request returns once the port took the bytes; it takes neither the terminal's
runtime lock nor the host writer, so another writer of the same terminal (a large paste, receipted input, a
cell-pixel change waiting for its acknowledgement) and a host that does not read its socket no longer hold the
person's key or the connection's dispatcher.

- The writer writes on the route of the current host connection (HostInputRoute: the connection's writer), which
  the terminal publishes whenever its runtime changes: the hosted and exited constructors, and
  PtyTerminalRuntime::replace_runtime, which the reconnect install and the host exit now use. So the writer never
  takes the runtime lock either.
- A lost connection is retried as write_bytes did (cx-6so.49), on the port's thread: wait for the reader's next
  stream change, write to the new host, until HOST_INPUT_RECONNECT_WAIT.
- A full port (32 MiB queued) refuses at once (terminal.input_busy, WouldBlock); a write larger than one host frame
  is refused as before; the exited final screen drops input as before.
- Input the port took and then could not write is lost as a failed write was, with a log line: the request was
  already answered (the reply now means "accepted by the terminal's port", DESIGN.md 2.6.1).
- Not in this step (S1.6b, before the default flip): the connection's other frames still write directly, so while
  the port is behind they can go before a key it still holds.

Off, every write takes the path it took on the base; the route is published on both arms (one Arc per change).

* test(cmux-tui-core): after a respawn, keys skip the sizing lock again; the lock tests report a viewport (#34261 review red)

Review comment 6619473751 (FIX at 846d412):
1. A respawn under the same runtime id keeps the sizing entry, which keeps the dead runtime's fast-path slot: the
   new runtime's slot is never published, so every later key takes Mux::client_sizing twice. The new test respawns
   the typing terminal, sends one key, then holds the sizing lock: the next keys must not wait. Red on this
   commit.
2. The lock tests' view never reported a size, so the engine held the grid and the skip went only through that
   branch. The typing view now reports 100x30 as the app's view does, so its keys skip through the settled-grid
   check.

* fix(cmux-tui-core): a sizing entry follows its terminal across a respawn; the input time never goes back (#34261 review)

Review comment 6619473751 (FIX at 846d412):
1. A respawn under the same runtime id keeps the sizing entry but builds a new terminal runtime with a new
   fast-path slot. TerminalSizingEntry::follow_runtime_of re-points the entry at the slot of the runtime its keys
   reach, under the sizing lock, and clears the old slot: on the key's slow path (note_terminal_activity_of has the
   key's surface) and on every terminal_sizing_entry call (attach, claim, size report). The red test
   after_a_respawn_keys_wait_for_no_sizing_lock_again (4bf5af5) pins it.
3. The entry takes or follows a slot only from a surface whose runtime id is the entry's, so two entries never
   share one slot.
4. The lock-free input time is fetch_max, so a key preempted after its clock read never moves it back.
7. The poison comment says what happens: a later holder that recovers the poison republishes what it touches.

Lock order is unchanged: terminal_sizing_entry now looks the surface up on each call (it did at creation, and
apply_terminal_grid does), under the sizing lock, the established sizing-then-session order.

* fix(cmux-tui-core): name the input port module without super:: where surface::* is in scope (clippy unused_qualifications)

* test(cmux-tui-core): a settled size-state read takes the session lock once with the switch off (#34261 review 2 red)

The sizing entry looks its surface up on every access since 5fe770d, so with the switch off a size-state read
takes the session lock twice (once under the sizing lock): expected red at this commit (lane off: 2, not 1).

* fix(cmux-tui-core): a sizing entry looks its surface up only when it is made; the respawn follow runs only on the lane (#34261 review 2)

5fe770d looked the surface up on every sizing-entry access, so with the switch off (the default) every attach,
size report, claim and size-state read took the session lock under the sizing lock. The surface is looked up only to
make the entry again; a respawn's new runtime is followed by its first key's sizing note, which holds the surface,
and only while the switch is on. An unchanged fast path is not stored again. The respawn test pins the old slot's
clear.

* fix(cmux-tui-core): the input port's order test can fail; the port copies outside its lock, counts each frame, recovers from a writer panic and refuses a failed link (#34544 review)

- The test compared two Options (None < Some), so a key that passed the paste still passed; both positions are
  required now. A new test sends 26 keys on the lane and checks the host reads each once, in order.
- enqueue copies the bytes before the port lock, and each queued frame counts 64 bytes of bookkeeping against the
  32 MiB capacity, so many tiny frames cannot grow the queue without bound.
- A writer that unwinds drops the frames after the one it lost (logged) and marks itself stopped, so the next input
  starts a new writer instead of queueing behind a writer that is gone.
- A host connection the reader gave up on (Failed) refuses input at once, as the old path's failed write did.
- The deadlines use checked_add (no new arithmetic_side_effects hits); the thread name fits Linux's 15 bytes.

* fix(cmux-tui-core): the input port refuses only when the host reader ended, not while a Failed link still retries (#34544 review 2)

A Failed link keeps retrying its host, and a successful retry installs the live replacement before the state
leaves Failed; the old path wrote to it, the lane refused. The lane now refuses only when the reader ended (no
connection will come back), where the old path's write fails too. The capacity holds two of the largest frames
with their per-frame cost; the 26-key test also checks that no key is written twice.

* fix(cmux-tui-core): a key to a failed host link is refused at once on the input port, as on the old path (#34544 review 3)

When the reader moves a link to Failed it shuts the connection down (and keeps retrying); the old path's write then
failed and `cmux send` exited non-zero, while the port took the key and only logged its failure. The reader now
publishes InputRoute::Failed under the runtime lock when it shuts the connection down; the port refuses it with
NotConnected, and the replacement's route (replace_runtime) is accepted as soon as it is installed. Also the 26-key
test's tail loop is a while-let (clippy::while_let_loop).

Test: a_key_to_a_failed_link_is_refused_until_a_live_route_is_published.

* fix(cmux-tui-core): a failed reconnect completion on a failed link also makes the input port refuse (#34544 review 4)

A retry that installed a replacement and then failed to complete it (terminal_host_reconnected false) shuts the
replacement down while the link stays Failed; the route was still the shut replacement, so the port took keys and
only logged them. That path now publishes InputRoute::Failed under the runtime lock too, as the Failed transition
does. Not driven by a test: the hosted fixture's live reader thread reacts to the disconnect itself; the route
behavior is covered by a_key_to_a_failed_link_is_refused_until_a_live_route_is_published.

* test(cmux-tui-core): a frame sent after a key the input port holds must reach the host after it (R-DAEMON-STATE S1.6b red)

The port counts the frames it took and finished and offers PortOrder::barrier; nothing calls it yet, so a direct
frame (here an Activate under the runtime lock, as viewer sizes and the other direct writers send) passes a key
the paused port still holds: expected red in a_frame_sent_after_a_key_the_port_holds_reaches_the_host_after_it.

* fix(cmux-tui-core): every other frame to a hosted terminal's host goes after the keys its input port took (R-DAEMON-STATE S1.6b order)

HostAttachment gets the terminal's PortOrder (set by the hosted constructor and replace_runtime, so it follows
every connection change); each frame it writes besides the port's own (send, receipted input, viewer size, cell
pixels, Kitty limits, control requests such as clear history and terminate, and the clipboard replier's replies)
first waits until the port finished every frame it had taken (PortOrder::barrier, at most 2 s, as one frame write
may block). The wait returns at once while the port's writer waits for a reconnect, since the install it waits
for may need the runtime lock the caller holds. With the switch off the port takes nothing and the wait is one
uncontended lock. The 9 test literals of HostAttachment take input_order: None.

Test: a_frame_sent_after_a_key_the_port_holds_reaches_the_host_after_it (red at 8b9f34a).

* test(cmux-tui-core): an automation write answers after the input port wrote it, with its frame's error (R-DAEMON-STATE S1.6b red)

On the lane every hosted write answered when the port took the bytes, so the send handler, the creating-input
flush (its give-back) and the resource input effect (its indeterminate mark) never saw a write error. Expected red:
an_automation_write_answers_after_the_port_wrote_it (it answers while the port is paused) and
an_automation_write_gets_the_error_of_its_frame (ok for bytes never written).

* fix(cmux-tui-core): on the input lane only the person's keys answer before the write; every other write answers with its frame's result (R-DAEMON-STATE S1.6b)

S1.6a answered every hosted write when the port took it, so the send handler for scripts, the creating-input
flush (its give-back), the resource input effect (its indeterminate mark) and the image paste (its reservation)
never saw a write error. Now Surface::write_bytes and write_paste answer when the port wrote the frame
(InputReply::Written: the port reports each frame's result on a channel, bounded at 15 s); only the person's key
(the send and send-key handlers, when the client is attached to the terminal it types into) passes
InputReply::Accepted and returns at once. The port also keeps one reconnect wait for its queue: frames queued
behind a host that does not come back fail together, not 5 s each. The give-back notice of the plan is dropped
(DESIGN 2.6.1: the app reads it only on its control connection, and it never retried a failed key).

Tests: an_automation_write_answers_after_the_port_wrote_it and an_automation_write_gets_the_error_of_its_frame
(red at 189ba58); the_persons_keys_answer_when_taken_and_other_writes_when_written; the person-key tests pass
InputReply::Accepted.

* test(cmux-tui-core): the input port's reconnect and reply rules from review 1 of #35082 (R-DAEMON-STATE S1.6b red)

Red:
- a_frame_sent_after_a_replacement_install_waits_for_the_keys_the_port_held (must-fix 1): after replace_runtime and
  before the reader's stream notify, a direct frame on the new connection passes the key the port held.
- a_later_host_loss_waits_for_its_own_reconnect (must-fix 2): the first loss's deadline outlives it, so a later
  loss's write fails at once.
- a_write_whose_wait_ends_before_the_port_takes_it_is_never_written, a_write_whose_wait_ends_during_its_write_may_be_written
  and the_creating_input_flush_keeps_input_that_may_have_been_written (item 6): a timed-out write stays queued and
  is written later, and the flush gives back bytes that may still be written.
Guards (pass before and after): a_frame_sent_while_the_port_waits_for_a_reconnect_does_not_wait_for_it,
writes_queued_behind_a_lost_host_fail_together, receipted_input_sent_after_a_key_the_port_holds_reaches_the_host_after_it.
an_automation_write_gets_the_error_of_its_frame waits for the port's count instead of sleeping 100 ms.

Test seams: the port's reply and reconnect waits (test overrides), its reconnecting/accepted/queued counts, and the
InputMayBeWritten marker with its classifier (nothing produces the marker yet).

* fix(cmux-tui-core): a route publication ends the input port's reconnect wait; each frame waits for a reconnect from when the port took it; a timed-out write is cancelled or marked possibly written (#35082 review 1)

- Must-fix 1: replace_runtime and input_route_failed call InputPort::route_changed. A port-owned route epoch ends
  the writer's reconnect wait and clears reconnecting under the port lock, so after the reader installs a live
  replacement the port writes the keys it holds to it before any other frame (before, the writer slept on the
  stream revision until the reader's notify after the VT replay, and the barrier skipped meanwhile).
- Must-fix 2: each frame waits for a reconnect until its own deadline, HOST_INPUT_RECONNECT_WAIT after the port took
  it, as the old path's write waited from its call. The queue-wide reconnect_until outlived its loss, so a later
  loss's writes failed at once. Writes queued behind a host that does not come back still fail together.
- Item 6: InputPort::write cancels a frame its waiter gave up on before the writer took it (the error says not
  written, and it never is); a wait that ends during the write answers InputMayBeWritten, and the creating-input
  flush does not give those bytes back (that typed them twice).
- Item 5: the order barrier ends by the caller's own deadline (control requests, cell pixels).
- Item 3: the order-wait comment states the bound; the clipboard replier uses the same deadline.
- Item 8: the port and input docs describe the current rules.

Tests (red at 2133c10): a_frame_sent_after_a_replacement_install_waits_for_the_keys_the_port_held,
a_later_host_loss_waits_for_its_own_reconnect, a_write_whose_wait_ends_before_the_port_takes_it_is_never_written,
a_write_whose_wait_ends_during_its_write_may_be_written, the_creating_input_flush_keeps_input_that_may_have_been_written.

* test(cmux-tui-core): the review-1 port tests name Frame and input_may_be_written through the module scope (clippy unused_qualifications)

lint (linux) at bbc44c2 (runs 41806402684, 41806407494) failed clippy -D warnings on four qualified paths in
surface/tests/hosted.rs; both names are in scope through use super::*.

* test(cmux-tui-core): a write waiting for a reconnect fails when the reader gives up (R-DAEMON-STATE S1.6c red, #35082 review 2)

The reader's end, give-up and failed-reconnect paths now call PtyTerminalRuntime::reader_gave_up, which still
only notifies stream_progress (today's behavior). The port's writer waits on its route epoch since #35082 review 1,
so the wake does not reach it: a_write_waiting_for_a_reconnect_fails_when_the_reader_gives_up waits its full
reconnect deadline (5 s) instead of failing at once.

* fix(cmux-tui-core): the reader's end, give-up and the owner's detach end the input port's reconnect wait (R-DAEMON-STATE S1.6c, #35082 review 2)

Since review 1 the port's writer waits on its own route epoch, so the reader's stream_progress wake no longer
reached it: a Written write behind a host the reader gave up on failed only at its reconnect deadline (5 s), not at
once as the old path's write did. InputPort::reconnect_ended moves the epoch with no new route; the writer checks the
route and the terminal's state again and fails its frame (Failed state, ended reader, or owner detaching).
- PtyTerminalRuntime::reader_gave_up (WakeInputOnReaderEnd, wait_or_fail's give-up, a failed reconnect completion)
  notifies the stream and ends the port's wait.
- The four owner_detaching stores in shutdown.rs end the port's wait (only the port lock, under the runtime lock).

Test: a_write_waiting_for_a_reconnect_fails_when_the_reader_gives_up (red at b6b0b37).

* test(cmux-tui-core): a viewer size behind a held key does not wait and goes after it; the settle barrier ends by its deadline; a frame the writer lost may be written (R-DAEMON-STATE S1.6c part 2 red)

Red:
- a_viewer_size_behind_a_key_the_port_holds_goes_after_it_without_waiting (#35082 review 1 item 4): the viewer
  size waits up to 2 s in the order barrier (under the sizing lock on a key's turn), then passes the held key.
- a_viewer_size_settle_ends_by_its_own_deadline_behind_a_held_frame (review 2 should-do 2): the attach's settle
  waits the barrier's 2 s, past its own 200 ms deadline.
- a_write_the_writer_lost_after_taking_it_may_be_written (review 2 should-do 3): a frame the writer took and lost
  answers 'not written'.
Test seams: hosted_surface_with (configure the fake host's attachment); InputPort::panic_after_take_for_test.

* fix(cmux-tui-core): viewer-size frames go on the input port behind its frames, with no wait; the settle barrier ends by its deadline; a lost taken frame may be written (R-DAEMON-STATE S1.6c part 2)

- #35082 review 1 item 4: a key that moves the grid sends a ViewerSize on its own turn under the global sizing lock
  (apply_terminal_grid -> resize -> send_viewer_size). It waited in the order barrier for up to 2 s behind the
  port's frames (a slow paste), holding the sizing lock, then went first. ViewerSize and ReleaseViewer now go on the
  port when it holds frames (PortOrder::enqueue_after_pending): ordered after them, with no waiter; a newer size
  replaces a queued one that is still the last frame, so sizes cannot pile up. With nothing pending (or the lane
  off: the port holds nothing) they are written directly, as before.
- Review 2 should-do 2: begin_viewer_size_barrier takes the attach's deadline (direct_writer_until), so an attach
  with a 1 s settle no longer waits the barrier's 2 s.
- Review 2 should-do 3: a Written frame whose writer took it and was lost (a panic during the write) answers
  InputMayBeWritten, so the creating-input flush keeps those bytes; one still queued answers not written.

Tests (red at ac7ffa2): a_viewer_size_behind_a_key_the_port_holds_goes_after_it_without_waiting,
a_viewer_size_settle_ends_by_its_own_deadline_behind_a_held_frame, a_write_the_writer_lost_after_taking_it_may_be_written.

* test(cmux-tui-core): the reply-timeout tests take their frame on a running writer, not a new thread (#35082 review 2, should-do 4)

a_write_whose_wait_ends_during_its_write_may_be_written and the_creating_input_flush_keeps_input_that_may_have_been_written
needed the port's writer thread to start and take the frame within the 300 ms reply wait. running_paused_writer
starts the writer on a key first and holds it paused; the next frame is taken at once by the running writer, and the
reply wait is 2 s.

* test(cmux-tui-core): the reader's real give-up and the daemon's shutdown detach end a write's reconnect wait (#35280 review, should-do 1)

The S1.6c part-1 test called reader_gave_up by hand, so a call site that lost its wake turned no test red. These
drive the real call sites: an exhausted TerminalHostReconnectBackoff's wait_or_fail, and
Surface::disconnect_for_daemon_shutdown, each while a Written write waits for a reconnect.

* docs(cmux-tui-core): the input port docs name reconnect_ended and state the lock rule the right way round (#35280 review, should-do 2)

The port lock is a leaf: no holder of it waits for another lock, so the runtime lock's holders may take it. The
shutdown.rs comments said the reverse.

* fix(cmux-tui-core): a viewer size queued behind a held key no longer takes the port lock twice (S1.6c part 2)

PortOrder::enqueue_after_pending computed its frame's reconnect deadline
with reconnect_wait while it held the port lock, and in test builds
reconnect_wait took that same std Mutex again to read the test override.
The second lock on one thread deadlocks: the hosted focused runs at
10b577c, 67023f5 and 476e0bf hung for 40 minutes in
a_viewer_size_behind_a_key_the_port_holds_goes_after_it_without_waiting
("has been running for over 60 seconds", run 41819499722, cancelled).

reconnect_wait now reads the guard its caller holds and never locks;
both callers (the port's enqueue and enqueue_after_pending) call it under
the lock. Release builds never took the second lock (the override is
cfg(test)), so this changes no shipped behaviour, only the deadlock the
tests hit.

* test(cmux-tui-core): a viewer size or release sent while the input port waits for a reconnect is the new host's last size frame (#35375 review, red)

enqueue_after_pending writes a size directly while the port waits for a
reconnect, but a size queued before the loss stays on the port. The
direct write goes to the lost connection; after the install the port
writes the stale queued size to the new host, so the host keeps 97x31
while the daemon's lease says 100x30 (or keeps a lease the daemon
released), and a later send of the same lease writes nothing.

Red: a_viewer_size_sent_during_a_reconnect_wait_is_the_new_hosts_last_size
and a_viewer_size_release_during_a_reconnect_wait_is_the_new_hosts_last_size_frame
read the replacement host's size frames and require the newest last.

* fix(cmux-tui-core): a viewer size sent while the input port waits for a reconnect goes on the port, so the new host's last size is the newest (#35375 review)

enqueue_after_pending returned false while the port's writer waited for
a reconnect, so the caller wrote the size directly, to the lost
connection, while a size queued before the loss stayed on the port. After
the install the port wrote that stale size to the new host after the
install's replay of the newest lease: the host kept the old size (or a
lease the daemon had released), and a later send of the same lease wrote
nothing.

It now queues whenever the port holds frames, during a reconnect wait
too: the newest size replaces a trailing queued one, so the last size
frame the new host reads is the newest lease or release. The barrier's
reconnect skip exists because its callers hold the runtime lock the
install needs; this enqueue never waits, so it needs no skip.

Tests (red at 5ae437a):
a_viewer_size_sent_during_a_reconnect_wait_is_the_new_hosts_last_size,
a_viewer_size_release_during_a_reconnect_wait_is_the_new_hosts_last_size_frame.

---------

Co-authored-by: cmux-bot[bot] <348531757+cmux-bot[bot]@users.noreply.github.com>
lawrencecchen added a commit that referenced this pull request Nov 5, 2026
…_RDS_INPUT=0 keeps the old path for one nightly (R-DAEMON-STATE S1.7) (#35413)

* test(cmux-tui-core): the person's key waits for no shared lock (R-DAEMON-STATE S1 red)

A key line from an attached view goes through the real connection path (the
line handler, the connection's dispatcher, the send handler) while another
thread holds one shared lock for 300 ms. #31940 took Mux.state off the key;
the key still waits for the client registry lock (five takes per line), the
link peers lock, the host proxy authors lock, the surface admission lock, the
creating-input lock, the client sizing lock and the activity lock. Their other
holders are every connect, attach, resize and request of every client.
Mux.state and the workspace registry rows are controls (pass on the base).

Two keys go in a row, so a lock taken after the PTY write (sizing, activity)
shows as a delay of the second key on the connection's dispatcher.

Test-only hold hooks (cfg(test)) in creating_input.rs, terminal_sizing.rs,
activity.rs and agent_session_attach.rs. No product change.

Design: handoff/rewrites/R-DAEMON-STATE/DESIGN.md section 2.6 (stage S1).

* test(cmux-tui-core): a key waits for no client registry lock (R-DAEMON-STATE S1 step 1 red)

The previous commit's census test held nine shared locks in turn; this commit
splits it by fix step. Step 1 is the client registry lock (ClientRegistry.state),
which a key line takes about nine times (handoff checks, remote-client and
remote-device checks, the request's actor, the kept terminal, the reply's
incognito filter) while every connect, disconnect, attach and detach of every
client holds it.

The test turns on the stage switch CMUX_TUI_RDS_INPUT (rds_switch.rs, inert in
this commit) and drives two keys through the real connection path while another
thread holds the lock for 300 ms. A second test keeps Mux.state and the
workspace registry off the key on the new path (#31940).

The hold hooks of the other locks leave with this commit; they come back with
their fix steps.

* fix(cmux-tui-core): the person's key reads the client registry's published facts, never its lock (R-DAEMON-STATE S1 step 1)

A key line took the client registry lock (ClientRegistry.state) about nine
times: the daemon handoff checks, the remote-client and remote-device checks,
the request's actor, the kept terminal of the key and the reply's incognito
filter. Every connect, disconnect, attach, detach and capability change of every
client holds that lock, so an agent storm held back the person's keys (red at
2472251831862: 300.4 ms behind a 300 ms hold; STATUS 9131).

The registry is now a Published value (published.rs): writers still serialize
on its mutex, and a guard that changed the value publishes an immutable
ClientFacts projection (handoff state; per connection its transport, remote
device flag, base actor and kept input terminals) with arc-swap when it drops,
under the mutex, so projections follow the order of the changes. With the stage
switch CMUX_TUI_RDS_INPUT=1, the line and key paths read the projection with
one atomic load: daemon_handoff_in_progress/committed, transport_of (so
is_remote_client), is_remote_device (refuse_line, line_to_recheck,
prune_reply), connection_actor and attached_input_surface. A poisoned registry
still fails closed (the projection comes in the error, as Mutex::lock gives
its guard). With the switch off nothing changes.

A projection costs O(connections) under the registry lock per change, bounded
by the 64-connection cap; stage S2 makes it O(changed).

arc-swap 1.9 is already in the lockfile and the shipped notices (through
iroh-dns); cmux-tui-core now names it.

Tests: rds_input_lane (a key waits for no client registry lock; the session and
workspace registry locks stay off; a respawn under the same id sends the next
key to the new shell), published.rs unit tests (a change publishes on drop, a
read publishes nothing, a reader never waits for a writer, a poisoned value
fails closed). The two remote-relay poison tests poison through the new type.

* fix(cmux-tui-core): the key reads the session's published routes; one stage switch per mux (R-DAEMON-STATE S1 step 1)

The hosted run of f1ea1b69709 (https://github.com/manaflow-ai/cmux/actions/runs/41655768031) still failed
both lane tests: the key waited 300 ms for the client registry and, new, for Mux.state. Two causes:

- The key still used the kept terminal of #31940. That cache is validated by process-wide route
  generations, and cargo test runs the tests of one binary as threads of one process: another test's
  mux moved the same generation slot (ids start at 1 in every mux), the kept terminal looked stale, and
  the key fell back to the locked lookup and then to remember_attached_input (registry lock). The
  stage switch was a process-wide static too.
- So the lane must not depend on a cache with process-wide invalidation at all.

Now, with CMUX_TUI_RDS_INPUT=1:
- The session's three routing tables (surfaces, terminal_catalog, terminal_catalog_by_runtime) are
  copy-on-write behind an Arc. A state guard that changed one publishes a RouteSnapshot (three Arc
  clones) with arc-swap when it drops, under the lock, so publications follow the changes; the
  snapshot it replaced is freed after the lock is released. Mux::surface reads the snapshot with one
  atomic load. A respawn, close, move, rollback or projection is seen once its guard is released, as a
  reader that waited for the lock would see it. Nothing is kept per client, so nothing is invalidated.
- input_surface on the lane: the published routes give the terminal, the registry's published facts
  say whether the client types into it (types_into: the person's input QoS mark). attached_input_surface
  and remember_attached_input stay only on the old path.
- The switch is per mux (rds_switch::InputLaneSwitch, read once from the environment when the mux is
  built; Mux::set_input_lane_for_test in tests), and it also decides whether anything is published: off,
  the registry builds no projection and the state holds no route snapshot, so a route write never
  copies a shared table and the off arm costs what the base costs.

Cost on the lane: a guard's first route write copies that table once (O(routes)), bounded by one copy
per changing guard; State::clone no longer deep-copies the three tables. Stage S2 makes both O(log n).

* test(cmux-tui-core): a key waits for no surface admission lock, with an old-path control (R-DAEMON-STATE S1 step 3 red)

Every request of every connection reserves its retained bytes from one
daemon-wide budget (ServerSurfaceOperationAdmission) under one mutex before it
is queued, and releases them under it after it ran. The S1 census measured a
key waiting 300.3 ms behind a 300 ms hold of that lock (hosted run 41648095321).

The lane test now has a control: the same two keys with stage S1 off must
wait for the held lock, so a lane result can never come from a hold that does
not reach the key path (a vacuous green). The control is added to the client
registry test too. A test-only hook (cfg(test)) holds the admission lock.

Red here: with the lane on, the key still reserves through the mutex.

* fix(cmux-tui-core): the surface admission budget reserves with atomics on the input lane (R-DAEMON-STATE S1 step 3)

Every request of every connection reserves its retained bytes (and every
clear-history its worker) from one daemon-wide budget before it is queued and
releases after it ran. Under one mutex, a key waited for any other
connection's reservation or release (red at 5d0e4a0e2c1: 300 ms behind a
300 ms hold).

With CMUX_TUI_RDS_INPUT=1 the two counters are atomics with compare-and-swap
reservations (fetch_update: add only while the sum stays within the capacity;
release saturates at zero), so nothing waits. The limits and the refusals are
the same as the mutex arm's: a worker is refused at 16 reserved, bytes when
the sum would pass 16 MiB. Each permit releases to the arm it reserved from.
With the switch off the mutex arm runs as before; it goes with the switch when
the stage passes (S1.8). The arm is fixed when the mux is built
(ServerSurfaceOperationAdmission::new(publish_routes)); tests set it with
Mux::set_input_lane_for_test before any request.

The two scheduler tests that counted reserved workers through the mutex read
reserved_workers() (both arms).

* test(cmux-tui-core): a key waits for neither the link peers nor the proxy authors lock (R-DAEMON-STATE S1 step 2 red)

Two more shared locks on the person's key line (DESIGN.md Appendix A.2 steps 3, 7, 12 and 13): the link peers map
(RemoteRelayState.peers: the remote-client check and the request's actor) and the host proxy authors
(AgentSessions.proxy_authors: the proxy-link check on the reader thread). The S1 census measured a full 300 ms wait
on each (hosted run 41648095321: 300.5 ms and 300.4 ms).

The test holds each lock 300 ms on another thread while two keys go through the real line path. It first proves
the hold reaches the old path (stage switch off: the key waits), then expects the stage's path not to wait. Red on
this commit: the stage does not publish either value yet.

* test(cmux-tui-core): a change cut short by a panic is never published (R-DAEMON-STATE S1 red)

A published value's guard publishes its projection in Drop::drop, which also runs while a panic unwinds, before
its MutexGuard field drops and marks the mutex poisoned. A writer that panics in the middle of a change so
publishes the half-made value, and in the moment between the publication and the poisoning a reader gets it as a
good value: a fail-open window for readers that fail closed on a poisoned value (transport_of, peer_checked).

Red on this commit: the test sees the half-made value 9 in the poisoned projection.

* test(cmux-tui-core): a reservation unshares a published route map; a panicked change publishes no routes (R-DAEMON-STATE S1 red)

Two defects the #34014 review found on the input lane (both only with CMUX_TUI_RDS_INPUT on):

- RouteTable::try_reserve returns early when the map has room, also when a published route snapshot shares the
  map. The insert after the durable commit then copies the whole table (Arc::make_mut), an allocation that
  ResourceMutationPlan::apply forbids ("Plan builders reserve all needed capacities before returning").
- The session state guard publishes the routes of a change that a panic cut short: Drop::drop runs during the
  unwind, before the MutexGuard field poisons the mutex, so the lane routes by half-made tables.

Red on this commit: the insert changes the map's pointer, and the routes snapshot is replaced.

* fix(cmux-tui-core): link peers and host proxy authors are published; the S1.1 review follow-ups (R-DAEMON-STATE S1 step 2)

A person's key line asked two more shared locks (DESIGN.md Appendix A.2 steps 3, 7, 12, 13): the link peers map
(the remote-client check and the request's actor) and the host proxy authors (the proxy-link check on the reader
thread). Every remote bind, disconnect and revocation sweep holds the first; every proxy bind and every client's
disconnect holds the second.

- published.rs moves to cmux-tui-util (daemon primitives, no daemon state) so cmux-tui-remote-access can use it.
  RemoteRelayState.peers becomes a Published copy of the records; peer_checked reads it with no lock when the
  mux's stage switch is on (Published::publishing), and a poisoned map still fails closed. AgentSessions.proxy_authors
  publishes the same way; proxy::author reads it. A disconnect of a client without a record publishes nothing.
- Review follow-ups of #34014 (review comment 6612546773):
  - RouteTable::try_reserve copies a map a published snapshot shares, before the durable commit, so the insert
    after the commit allocates nothing (ResourceMutationPlan::apply).
  - A change a panic cuts short is never published, by the state guard (routes) or by a PublishGuard: Drop skips
    the publication when a panic began while the guard held the lock (std's own poisoning test), so no reader
    sees half-made tables, not even before the mutex is poisoned.
  - A PublishGuard frees the projection it replaced after the unlock (as the state guard does).
  - The two poison tests of the relay run on both arms.
  - ClientFacts and types_into move to server/client_registry/facts.rs (god-file warning).

Same switch, CMUX_TUI_RDS_INPUT, default off: with it off nothing is published and every read takes its lock as
on the base.

* test(cmux-tui-core): a key waits for no creating-input lock, and for no session lock behind one (R-DAEMON-STATE S1 step 4 red)

Every key to a live terminal first asks whether creating input of its terminal must go first
(wait_for_creating_input_flush, DESIGN.md Appendix A.2 step 16). It takes the daemon-wide creating-input lock even
when no terminal is being created, and that lock's holders (begin_creating_input, knows_terminal_input_target,
finish_creating_input) look up the live terminal under it, which takes the session lock. The S1 census measured a
full 300 ms wait on the creating-input lock (hosted run 41648095321: 300.3 ms).

Two holders, each with an old-path control: the creating-input lock itself, and the session lock held while a
reader waits for it under the creating-input lock (with another terminal being created, so the key cannot skip the
lock). Red on this commit: the stage does not change these paths yet.

* fix(cmux-tui-core): a key skips the creating-input lock while nothing is creating; the lock becomes a leaf (R-DAEMON-STATE S1 step 4)

On the input lane (CMUX_TUI_RDS_INPUT, per mux, default off):

- CreatingInputs keeps the number of creating terminals in an atomic, stored under its lock after every insert and
  removal (Release). A key reads it (Acquire) and returns at once when it is zero: no queue can be ahead of it,
  because a terminal's queue is added before the terminal goes live and removed only after every queued byte was
  written to it.
- The creating-input lock's holders look up the live terminal in the published routes instead of under the session
  lock, so no holder waits for the session lock while it holds the creating-input lock, and a key that takes it
  never waits behind one. The routes carry the host-id catalog index for this: terminal_catalog_by_host becomes a
  copy-on-write route table in the RouteSnapshot, and one lookup (route_table::live_terminal) serves the session
  state and the snapshot. A commit publishes its routes before it releases the session lock, and finish runs after
  the commit, so begin, knows and finish give the answers they gave under the session lock.

Off, every path takes the locks it took on the base; the by-host index is copy-on-write on both arms, as the other
three route tables are since step 1.

* test(cmux-tui-core): the surface budget's limits on both arms; a lock control fails on a lost key (#34146 review follow-ups)

The #34146 review (comment 6613345630) noted:
- Every budget test used the mutex arm, so an atomic arm that admitted everything would have passed. The two
  mux-wide limit tests now run on both arms, and a new test checks the same limits on both: the 17th worker, the
  whole byte budget, one byte over, zero bytes, a sum that overflows, and released reservations coming back.
- The old-path control passed when a key never arrived (Duration::MAX >= BUDGET); it now fails on a lost key.
- ServerSurfaceOperationState and the admission's mutex are private (nothing outside the file reads them), and
  the doc says which requests reserve (pings, identify, v2 resource lines and reader-thread frame handlers do not).

* fix(cmux-tui-core): drop an unnecessary path qualification in a test (clippy unused_qualifications)

The lint (linux) job's clippy -D warnings failed on mux/tests/signaled_mutex.rs:120 (TerminalPublicId is in scope);
the #34174 review found it behind the base's acpmux red in the same job.

* test(cmux-tui-core): a poisoned session state never routes by the published tables (R-DAEMON-STATE S1 red)

The #34171 review noted: after a panic poisons Mux::state, the next guard that recovers the poison may publish the
half-made route tables, and the lane's Mux::surface reads the published routes with no poison check, where the old
path fails (it unwraps the lock). Red on this commit: with the lane on, a surface read after the poisoning returns
from the published routes instead of failing.

The relay poison tests now poison published values through a helper in the test file.

* fix(cmux-tui-core): the lane's surface read takes the old path on a poisoned state; the poison helper is test code (#34171 review)

Review comment 6614044759 (FIX at 8f7d40500e8):
- published.rs: poison_for_test moved out of the test module with its panic! respelled as panic_any, which hides a
  hit from the crash ratchet's count. It is test code again: a private helper in the published tests and one in
  the relay tests, with plain panic!.
- Mux::surface on the lane reads the published routes only while the session state is not poisoned; a poisoned
  state takes the old read and fails as it does (StateMutex::is_poisoned). A guard that recovers the poison may
  publish the half-made tables of the change that panicked.
- Published::load debug-asserts that the value publishes: only a publishing value has a current projection.
- The module doc says what load gives on a poisoned value; the caller decides whether to fail closed.

The unused_qualifications error at mux/tests/signaled_mutex.rs:120 is fixed in 10f16693a03.

* fix(cmux-tui-core): the creating-input live check reads the routes only on an unpoisoned state; its comment states the lane's linearization (#34174 review)

Review comment 6613998735 (SHIP, notes): the begin_creating_input comment still said the check takes the state
under the creating lock; on the lane it reads the published routes, and its answer is one the old path can give
(a commit in progress has not made the id live, as when the old path takes the state before it). The lane's live
lookup now takes the old read on a poisoned state, as Mux::surface does since bb0265f43d5.

* test(cmux-tui-core): a key waits for neither the sizing nor the activity lock; a key takes the grid back after another view's click (R-DAEMON-STATE S1 step 5 red)

After its PTY write a key notes activity in the terminal's sizing (the newest view sets the grid under the default
policy): it takes the daemon-wide client sizing lock twice (the note and the size-state publication), then the
VM activity stream's lock. Every attach, detach, resize and size report of every terminal holds the sizing lock,
and a PTY resize runs under it. The S1 census measured a full 300 ms wait on each (hosted run 41648095321: 300.2
and 300.3 ms).

The first test holds each lock 300 ms (with an old-path control each). The second keeps the fix honest: two views
of one terminal, the default policy; the other view's focus click takes the grid, and the next key from the
typing view must take it back, on both arms. Red on this commit: the first test (the stage does not change these
paths yet).

* fix(cmux-tui-util): a test dereferences the published u64 once (E0614 in bb0265f43d5)

* fix(cmux-tui-core): a key skips its sizing note when its view is the settled newest; the person's input time is an atomic (R-DAEMON-STATE S1 step 5)

On the input lane (CMUX_TUI_RDS_INPUT, per mux, default off):

- Each terminal's sizing entry publishes, on the terminal's runtime (shared by every placement), the view a key may
  skip its sizing note for: the sole newest participant, a client's own view, only while the entry is settled (no
  grid hold, no handheld with a viewport, and the engine holds the grid or the applied grid and participants are its
  decision). Noting activity for that view changes no decision under any policy, and the size state carries no
  activity, so the key compares (client, placement) with one atomic load and takes no sizing lock.
- The publication is automatic: Mux::client_sizing is a SizingMutex whose guard republishes, on drop and under the
  lock, each entry its holder read or wrote (SizingEntries marks them; an apply changes Cells through a shared
  borrow, so reads count). A removed entry clears its slot; a change a panic cuts short publishes nothing.
- Another view's activity makes that view the newest and republishes, so the next key from the first view takes the
  slow path once and takes the grid back (the second test pins this on both arms).
- ActivityStream::note_user_input stores the time in an atomic; it takes the stream lock only while a subscriber
  exists, once per event the worker sends (a wake_pending flag the worker clears before it reads the time).
- TerminalSizingEngine::sole_latest (cmux-terminal-sizing) with a unit test; AppliedSeat::of shares the seat rule
  between the apply and the settled check; set_workspace_size_policy reads its state before the note (no index).

Off, every key takes the sizing and activity locks as on the base; the marks and the guard check one bool.

* fix(cmux-tui-core): the sizing test hook went in above another test hook's cfg(test) (dead code and 2 expect hits in production)

dfe81bff4d8 put hold_client_sizing_for_test between pin_latest_size_policy_for_test's doc and its #[cfg(test)], so
pin_latest_size_policy_for_test compiled into production: clippy -D warnings dead_code, and the panic ratchet's
cmux-tui-core expect_used 175 > 173 (hosted lint job 126202464363).

* fix(cmux-tui-core): the lane's surface read loads the routes before its poison check; two test doc comments on their items (#34171 review 2)

Review comment 6615927915 (SHIP at 0fb7c43a810, notes): Mux::surface checked is_poisoned() and then loaded the
routes; between the two another thread could poison the state and a third, recovering the poison, publish half-made
tables. Poison never clears on the session state, so routes loaded before an unpoisoned check were published before
any poisoning: load first, then check. Two doc comments in remote_relay/tests.rs sat above the wrong item.

* test(cmux-tui-core): a key does not wait for another writer's blocked host socket (R-DAEMON-STATE S1 step 6 red)

A key to a hosted terminal holds the terminal's runtime lock and its HostAttachment.writer across a blocking write
to the host socket (surface/input.rs, attachment.rs send_host_frame). Another client's large paste to the same
terminal holds both for its whole write, bounded only by the socket's send timeout, so the person's key waits for
automation's work on the same terminal (DESIGN.md section 2.6.1, Appendix A.2 steps 20-22).

The test pastes 8 MiB while the fake host reads nothing for 600 ms, then sends one key. A control first proves the
old path waits (the key takes at least half the stall); the lane must return the key in < 100 ms, and the host must
then read the paste before the key, and the key once. Red on this commit: the lane has no input port yet.

* fix(cmux-tui-core): a person's key to a hosted terminal goes to the terminal's input port and never waits for a socket write (R-DAEMON-STATE S1 step 6)

On the input lane (CMUX_TUI_RDS_INPUT, per mux, default off), interactive input to a hosted terminal
(Surface::write_bytes, Surface::write_paste) goes to the terminal's own input port (surface/input_port.rs): one
queue per terminal, drained in order by the terminal's writer thread (terminal-input-port, started on the first
input, ended after 30 s idle). The request returns once the port took the bytes; it takes neither the terminal's
runtime lock nor the host writer, so another writer of the same terminal (a large paste, receipted input, a
cell-pixel change waiting for its acknowledgement) and a host that does not read its socket no longer hold the
person's key or the connection's dispatcher.

- The writer writes on the route of the current host connection (HostInputRoute: the connection's writer), which
  the terminal publishes whenever its runtime changes: the hosted and exited constructors, and
  PtyTerminalRuntime::replace_runtime, which the reconnect install and the host exit now use. So the writer never
  takes the runtime lock either.
- A lost connection is retried as write_bytes did (cx-6so.49), on the port's thread: wait for the reader's next
  stream change, write to the new host, until HOST_INPUT_RECONNECT_WAIT.
- A full port (32 MiB queued) refuses at once (terminal.input_busy, WouldBlock); a write larger than one host frame
  is refused as before; the exited final screen drops input as before.
- Input the port took and then could not write is lost as a failed write was, with a log line: the request was
  already answered (the reply now means "accepted by the terminal's port", DESIGN.md 2.6.1).
- Not in this step (S1.6b, before the default flip): the connection's other frames still write directly, so while
  the port is behind they can go before a key it still holds.

Off, every write takes the path it took on the base; the route is published on both arms (one Arc per change).

* test(cmux-tui-core): after a respawn, keys skip the sizing lock again; the lock tests report a viewport (#34261 review red)

Review comment 6619473751 (FIX at 846d412ccf7):
1. A respawn under the same runtime id keeps the sizing entry, which keeps the dead runtime's fast-path slot: the
   new runtime's slot is never published, so every later key takes Mux::client_sizing twice. The new test respawns
   the typing terminal, sends one key, then holds the sizing lock: the next keys must not wait. Red on this
   commit.
2. The lock tests' view never reported a size, so the engine held the grid and the skip went only through that
   branch. The typing view now reports 100x30 as the app's view does, so its keys skip through the settled-grid
   check.

* fix(cmux-tui-core): a sizing entry follows its terminal across a respawn; the input time never goes back (#34261 review)

Review comment 6619473751 (FIX at 846d412ccf7):
1. A respawn under the same runtime id keeps the sizing entry but builds a new terminal runtime with a new
   fast-path slot. TerminalSizingEntry::follow_runtime_of re-points the entry at the slot of the runtime its keys
   reach, under the sizing lock, and clears the old slot: on the key's slow path (note_terminal_activity_of has the
   key's surface) and on every terminal_sizing_entry call (attach, claim, size report). The red test
   after_a_respawn_keys_wait_for_no_sizing_lock_again (4bf5af5c6ae) pins it.
3. The entry takes or follows a slot only from a surface whose runtime id is the entry's, so two entries never
   share one slot.
4. The lock-free input time is fetch_max, so a key preempted after its clock read never moves it back.
7. The poison comment says what happens: a later holder that recovers the poison republishes what it touches.

Lock order is unchanged: terminal_sizing_entry now looks the surface up on each call (it did at creation, and
apply_terminal_grid does), under the sizing lock, the established sizing-then-session order.

* fix(cmux-tui-core): name the input port module without super:: where surface::* is in scope (clippy unused_qualifications)

* test(cmux-tui-core): a settled size-state read takes the session lock once with the switch off (#34261 review 2 red)

The sizing entry looks its surface up on every access since 5fe770df4ff, so with the switch off a size-state read
takes the session lock twice (once under the sizing lock): expected red at this commit (lane off: 2, not 1).

* fix(cmux-tui-core): a sizing entry looks its surface up only when it is made; the respawn follow runs only on the lane (#34261 review 2)

5fe770df4ff looked the surface up on every sizing-entry access, so with the switch off (the default) every attach,
size report, claim and size-state read took the session lock under the sizing lock. The surface is looked up only to
make the entry again; a respawn's new runtime is followed by its first key's sizing note, which holds the surface,
and only while the switch is on. An unchanged fast path is not stored again. The respawn test pins the old slot's
clear.

* fix(cmux-tui-core): the input port's order test can fail; the port copies outside its lock, counts each frame, recovers from a writer panic and refuses a failed link (#34544 review)

- The test compared two Options (None < Some), so a key that passed the paste still passed; both positions are
  required now. A new test sends 26 keys on the lane and checks the host reads each once, in order.
- enqueue copies the bytes before the port lock, and each queued frame counts 64 bytes of bookkeeping against the
  32 MiB capacity, so many tiny frames cannot grow the queue without bound.
- A writer that unwinds drops the frames after the one it lost (logged) and marks itself stopped, so the next input
  starts a new writer instead of queueing behind a writer that is gone.
- A host connection the reader gave up on (Failed) refuses input at once, as the old path's failed write did.
- The deadlines use checked_add (no new arithmetic_side_effects hits); the thread name fits Linux's 15 bytes.

* fix(cmux-tui-core): the input port refuses only when the host reader ended, not while a Failed link still retries (#34544 review 2)

A Failed link keeps retrying its host, and a successful retry installs the live replacement before the state
leaves Failed; the old path wrote to it, the lane refused. The lane now refuses only when the reader ended (no
connection will come back), where the old path's write fails too. The capacity holds two of the largest frames
with their per-frame cost; the 26-key test also checks that no key is written twice.

* fix(cmux-tui-core): a key to a failed host link is refused at once on the input port, as on the old path (#34544 review 3)

When the reader moves a link to Failed it shuts the connection down (and keeps retrying); the old path's write then
failed and `cmux send` exited non-zero, while the port took the key and only logged its failure. The reader now
publishes InputRoute::Failed under the runtime lock when it shuts the connection down; the port refuses it with
NotConnected, and the replacement's route (replace_runtime) is accepted as soon as it is installed. Also the 26-key
test's tail loop is a while-let (clippy::while_let_loop).

Test: a_key_to_a_failed_link_is_refused_until_a_live_route_is_published.

* fix(cmux-tui-core): a failed reconnect completion on a failed link also makes the input port refuse (#34544 review 4)

A retry that installed a replacement and then failed to complete it (terminal_host_reconnected false) shuts the
replacement down while the link stays Failed; the route was still the shut replacement, so the port took keys and
only logged them. That path now publishes InputRoute::Failed under the runtime lock too, as the Failed transition
does. Not driven by a test: the hosted fixture's live reader thread reacts to the disconnect itself; the route
behavior is covered by a_key_to_a_failed_link_is_refused_until_a_live_route_is_published.

* test(cmux-tui-core): a frame sent after a key the input port holds must reach the host after it (R-DAEMON-STATE S1.6b red)

The port counts the frames it took and finished and offers PortOrder::barrier; nothing calls it yet, so a direct
frame (here an Activate under the runtime lock, as viewer sizes and the other direct writers send) passes a key
the paused port still holds: expected red in a_frame_sent_after_a_key_the_port_holds_reaches_the_host_after_it.

* fix(cmux-tui-core): every other frame to a hosted terminal's host goes after the keys its input port took (R-DAEMON-STATE S1.6b order)

HostAttachment gets the terminal's PortOrder (set by the hosted constructor and replace_runtime, so it follows
every connection change); each frame it writes besides the port's own (send, receipted input, viewer size, cell
pixels, Kitty limits, control requests such as clear history and terminate, and the clipboard replier's replies)
first waits until the port finished every frame it had taken (PortOrder::barrier, at most 2 s, as one frame write
may block). The wait returns at once while the port's writer waits for a reconnect, since the install it waits
for may need the runtime lock the caller holds. With the switch off the port takes nothing and the wait is one
uncontended lock. The 9 test literals of HostAttachment take input_order: None.

Test: a_frame_sent_after_a_key_the_port_holds_reaches_the_host_after_it (red at 8b9f34a9be8).

* test(cmux-tui-core): an automation write answers after the input port wrote it, with its frame's error (R-DAEMON-STATE S1.6b red)

On the lane every hosted write answered when the port took the bytes, so the send handler, the creating-input
flush (its give-back) and the resource input effect (its indeterminate mark) never saw a write error. Expected red:
an_automation_write_answers_after_the_port_wrote_it (it answers while the port is paused) and
an_automation_write_gets_the_error_of_its_frame (ok for bytes never written).

* fix(cmux-tui-core): on the input lane only the person's keys answer before the write; every other write answers with its frame's result (R-DAEMON-STATE S1.6b)

S1.6a answered every hosted write when the port took it, so the send handler for scripts, the creating-input
flush (its give-back), the resource input effect (its indeterminate mark) and the image paste (its reservation)
never saw a write error. Now Surface::write_bytes and write_paste answer when the port wrote the frame
(InputReply::Written: the port reports each frame's result on a channel, bounded at 15 s); only the person's key
(the send and send-key handlers, when the client is attached to the terminal it types into) passes
InputReply::Accepted and returns at once. The port also keeps one reconnect wait for its queue: frames queued
behind a host that does not come back fail together, not 5 s each. The give-back notice of the plan is dropped
(DESIGN 2.6.1: the app reads it only on its control connection, and it never retried a failed key).

Tests: an_automation_write_answers_after_the_port_wrote_it and an_automation_write_gets_the_error_of_its_frame
(red at 189ba5886e5); the_persons_keys_answer_when_taken_and_other_writes_when_written; the person-key tests pass
InputReply::Accepted.

* test(cmux-tui-core): an activity subscriber hears the person's keys on both arms; the sizing-note skip sets the grids the old path sets (R-DAEMON-STATE S1.7 prerequisites)

The two test gaps the #34261 review named before the default flip:
- an_activity_subscriber_hears_the_persons_keys_on_both_arms: the snapshot has a key typed before it, the next key
  is an event at once, a key inside the one-second spacing comes in the trailing event with a newer time.
- the_sizing_note_skip_sets_the_grids_the_old_path_sets: for the latest, smallest, largest and fixed policies, with
  a desktop or a phone as the other view (keys from both, the other view leaving: a phone starts the departed-grid
  hold), and with a relay's phone sub-view, the grid after every step is the same on the lane and the old path.

* test(cmux-tui-core): the input lane is on unless CMUX_TUI_RDS_INPUT=0 turns it off (R-DAEMON-STATE S1.7 red)

The switch is read through InputLaneSwitch::from_value so a test can check the default without the
process environment. Red: with no value the lane is still off.

* fix(cmux-tui-core): the input lane is the default; CMUX_TUI_RDS_INPUT=0 keeps the old path for one nightly (R-DAEMON-STATE S1.7)

The S1 typing A/B (STATUS 11230) met A4 with the lane on: the daemon part of a key p99 1.56-2.29 ms, max
3.50-3.75 ms; with it off max 11.2-14.1 ms (the lock waits S1 removes). Now the lane is on unless the
daemon's environment (or the app knob CMUX_NEXT_TUI_RDS_INPUT) sets 0. S1.8 deletes the old path and the
switch after one nightly.

The kept-terminal tests of the old path (server/tests/input_lane.rs) pin the switch off, so the fallback
keeps its coverage while it ships; every other test now runs on the lane.

Test: rds_switch::tests::the_input_lane_is_on_unless_the_environment_turns_it_off (red at 9674a79fd8f).

* test(cmux-tui-core): the input port's reconnect and reply rules from review 1 of #35082 (R-DAEMON-STATE S1.6b red)

Red:
- a_frame_sent_after_a_replacement_install_waits_for_the_keys_the_port_held (must-fix 1): after replace_runtime and
  before the reader's stream notify, a direct frame on the new connection passes the key the port held.
- a_later_host_loss_waits_for_its_own_reconnect (must-fix 2): the first loss's deadline outlives it, so a later
  loss's write fails at once.
- a_write_whose_wait_ends_before_the_port_takes_it_is_never_written, a_write_whose_wait_ends_during_its_write_may_be_written
  and the_creating_input_flush_keeps_input_that_may_have_been_written (item 6): a timed-out write stays queued and
  is written later, and the flush gives back bytes that may still be written.
Guards (pass before and after): a_frame_sent_while_the_port_waits_for_a_reconnect_does_not_wait_for_it,
writes_queued_behind_a_lost_host_fail_together, receipted_input_sent_after_a_key_the_port_holds_reaches_the_host_after_it.
an_automation_write_gets_the_error_of_its_frame waits for the port's count instead of sleeping 100 ms.

Test seams: the port's reply and reconnect waits (test overrides), its reconnecting/accepted/queued counts, and the
InputMayBeWritten marker with its classifier (nothing produces the marker yet).

* fix(cmux-tui-core): a route publication ends the input port's reconnect wait; each frame waits for a reconnect from when the port took it; a timed-out write is cancelled or marked possibly written (#35082 review 1)

- Must-fix 1: replace_runtime and input_route_failed call InputPort::route_changed. A port-owned route epoch ends
  the writer's reconnect wait and clears reconnecting under the port lock, so after the reader installs a live
  replacement the port writes the keys it holds to it before any other frame (before, the writer slept on the
  stream revision until the reader's notify after the VT replay, and the barrier skipped meanwhile).
- Must-fix 2: each frame waits for a reconnect until its own deadline, HOST_INPUT_RECONNECT_WAIT after the port took
  it, as the old path's write waited from its call. The queue-wide reconnect_until outlived its loss, so a later
  loss's writes failed at once. Writes queued behind a host that does not come back still fail together.
- Item 6: InputPort::write cancels a frame its waiter gave up on before the writer took it (the error says not
  written, and it never is); a wait that ends during the write answers InputMayBeWritten, and the creating-input
  flush does not give those bytes back (that typed them twice).
- Item 5: the order barrier ends by the caller's own deadline (control requests, cell pixels).
- Item 3: the order-wait comment states the bound; the clipboard replier uses the same deadline.
- Item 8: the port and input docs describe the current rules.

Tests (red at 2133c10ae2f): a_frame_sent_after_a_replacement_install_waits_for_the_keys_the_port_held,
a_later_host_loss_waits_for_its_own_reconnect, a_write_whose_wait_ends_before_the_port_takes_it_is_never_written,
a_write_whose_wait_ends_during_its_write_may_be_written, the_creating_input_flush_keeps_input_that_may_have_been_written.

* test(cmux-tui-core): the review-1 port tests name Frame and input_may_be_written through the module scope (clippy unused_qualifications)

lint (linux) at bbc44c24ae4 (runs 41806402684, 41806407494) failed clippy -D warnings on four qualified paths in
surface/tests/hosted.rs; both names are in scope through use super::*.

* test(cmux-tui-core): a write waiting for a reconnect fails when the reader gives up (R-DAEMON-STATE S1.6c red, #35082 review 2)

The reader's end, give-up and failed-reconnect paths now call PtyTerminalRuntime::reader_gave_up, which still
only notifies stream_progress (today's behavior). The port's writer waits on its route epoch since #35082 review 1,
so the wake does not reach it: a_write_waiting_for_a_reconnect_fails_when_the_reader_gives_up waits its full
reconnect deadline (5 s) instead of failing at once.

* fix(cmux-tui-core): the reader's end, give-up and the owner's detach end the input port's reconnect wait (R-DAEMON-STATE S1.6c, #35082 review 2)

Since review 1 the port's writer waits on its own route epoch, so the reader's stream_progress wake no longer
reached it: a Written write behind a host the reader gave up on failed only at its reconnect deadline (5 s), not at
once as the old path's write did. InputPort::reconnect_ended moves the epoch with no new route; the writer checks the
route and the terminal's state again and fails its frame (Failed state, ended reader, or owner detaching).
- PtyTerminalRuntime::reader_gave_up (WakeInputOnReaderEnd, wait_or_fail's give-up, a failed reconnect completion)
  notifies the stream and ends the port's wait.
- The four owner_detaching stores in shutdown.rs end the port's wait (only the port lock, under the runtime lock).

Test: a_write_waiting_for_a_reconnect_fails_when_the_reader_gives_up (red at b6b0b37f3b0).

* test(cmux-tui-core): a viewer size behind a held key does not wait and goes after it; the settle barrier ends by its deadline; a frame the writer lost may be written (R-DAEMON-STATE S1.6c part 2 red)

Red:
- a_viewer_size_behind_a_key_the_port_holds_goes_after_it_without_waiting (#35082 review 1 item 4): the viewer
  size waits up to 2 s in the order barrier (under the sizing lock on a key's turn), then passes the held key.
- a_viewer_size_settle_ends_by_its_own_deadline_behind_a_held_frame (review 2 should-do 2): the attach's settle
  waits the barrier's 2 s, past its own 200 ms deadline.
- a_write_the_writer_lost_after_taking_it_may_be_written (review 2 should-do 3): a frame the writer took and lost
  answers 'not written'.
Test seams: hosted_surface_with (configure the fake host's attachment); InputPort::panic_after_take_for_test.

* fix(cmux-tui-core): viewer-size frames go on the input port behind its frames, with no wait; the settle barrier ends by its deadline; a lost taken frame may be written (R-DAEMON-STATE S1.6c part 2)

- #35082 review 1 item 4: a key that moves the grid sends a ViewerSize on its own turn under the global sizing lock
  (apply_terminal_grid -> resize -> send_viewer_size). It waited in the order barrier for up to 2 s behind the
  port's frames (a slow paste), holding the sizing lock, then went first. ViewerSize and ReleaseViewer now go on the
  port when it holds frames (PortOrder::enqueue_after_pending): ordered after them, with no waiter; a newer size
  replaces a queued one that is still the last frame, so sizes cannot pile up. With nothing pending (or the lane
  off: the port holds nothing) they are written directly, as before.
- Review 2 should-do 2: begin_viewer_size_barrier takes the attach's deadline (direct_writer_until), so an attach
  with a 1 s settle no longer waits the barrier's 2 s.
- Review 2 should-do 3: a Written frame whose writer took it and was lost (a panic during the write) answers
  InputMayBeWritten, so the creating-input flush keeps those bytes; one still queued answers not written.

Tests (red at ac7ffa2b4e1): a_viewer_size_behind_a_key_the_port_holds_goes_after_it_without_waiting,
a_viewer_size_settle_ends_by_its_own_deadline_behind_a_held_frame, a_write_the_writer_lost_after_taking_it_may_be_written.

* test(cmux-tui-core): the reply-timeout tests take their frame on a running writer, not a new thread (#35082 review 2, should-do 4)

a_write_whose_wait_ends_during_its_write_may_be_written and the_creating_input_flush_keeps_input_that_may_have_been_written
needed the port's writer thread to start and take the frame within the 300 ms reply wait. running_paused_writer
starts the writer on a key first and holds it paused; the next frame is taken at once by the running writer, and the
reply wait is 2 s.

* test(cmux-tui-core): the reader's real give-up and the daemon's shutdown detach end a write's reconnect wait (#35280 review, should-do 1)

The S1.6c part-1 test called reader_gave_up by hand, so a call site that lost its wake turned no test red. These
drive the real call sites: an exhausted TerminalHostReconnectBackoff's wait_or_fail, and
Surface::disconnect_for_daemon_shutdown, each while a Written write waits for a reconnect.

* docs(cmux-tui-core): the input port docs name reconnect_ended and state the lock rule the right way round (#35280 review, should-do 2)

The port lock is a leaf: no holder of it waits for another lock, so the runtime lock's holders may take it. The
shutdown.rs comments said the reverse.

* fix(cmux-tui-core): a viewer size queued behind a held key no longer takes the port lock twice (S1.6c part 2)

PortOrder::enqueue_after_pending computed its frame's reconnect deadline
with reconnect_wait while it held the port lock, and in test builds
reconnect_wait took that same std Mutex again to read the test override.
The second lock on one thread deadlocks: the hosted focused runs at
10b577c0a0b, 67023f50a46 and 476e0bfc4a0 hung for 40 minutes in
a_viewer_size_behind_a_key_the_port_holds_goes_after_it_without_waiting
("has been running for over 60 seconds", run 41819499722, cancelled).

reconnect_wait now reads the guard its caller holds and never locks;
both callers (the port's enqueue and enqueue_after_pending) call it under
the lock. Release builds never took the second lock (the override is
cfg(test)), so this changes no shipped behaviour, only the deadlock the
tests hit.

* test(cmux-tui-core): a viewer size or release sent while the input port waits for a reconnect is the new host's last size frame (#35375 review, red)

enqueue_after_pending writes a size directly while the port waits for a
reconnect, but a size queued before the loss stays on the port. The
direct write goes to the lost connection; after the install the port
writes the stale queued size to the new host, so the host keeps 97x31
while the daemon's lease says 100x30 (or keeps a lease the daemon
released), and a later send of the same lease writes nothing.

Red: a_viewer_size_sent_during_a_reconnect_wait_is_the_new_hosts_last_size
and a_viewer_size_release_during_a_reconnect_wait_is_the_new_hosts_last_size_frame
read the replacement host's size frames and require the newest last.

* fix(cmux-tui-core): a viewer size sent while the input port waits for a reconnect goes on the port, so the new host's last size is the newest (#35375 review)

enqueue_after_pending returned false while the port's writer waited for
a reconnect, so the caller wrote the size directly, to the lost
connection, while a size queued before the loss stayed on the port. After
the install the port wrote that stale size to the new host after the
install's replay of the newest lease: the host kept the old size (or a
lease the daemon had released), and a later send of the same lease wrote
nothing.

It now queues whenever the port holds frames, during a reconnect wait
too: the newest size replaces a trailing queued one, so the last size
frame the new host reads is the newest lease or release. The barrier's
reconnect skip exists because its callers hold the runtime lock the
install needs; this enqueue never waits, so it needs no skip.

Tests (red at 5ae437af1df):
a_viewer_size_sent_during_a_reconnect_wait_is_the_new_hosts_last_size,
a_viewer_size_release_during_a_reconnect_wait_is_the_new_hosts_last_size_frame.

* test(cmux-tui-core): the input port's writer runs at user-interactive QoS once it carries a person's key (R-DAEMON-STATE S1.6d red, #35413 review)

DESIGN 2.6.1 asks for the port's writer at user-interactive QoS while it
carries the person's input, as the connection threads that carry the
key run (server/input_qos.rs, TYPING FIX 2 F7). The writer thread is
spawned at the default QoS and never raises itself, so under full CPU
the hop from daemon.send.written to the host's PTY write waits for a
CPU: the build 3b ON keys over 4 ms waited there (STATUS 11804).

Red (macOS only; QoS is a no-op elsewhere):
the_input_ports_writer_runs_at_user_interactive_qos_once_it_carries_a_persons_key.
Test seam: the writer records its QoS each time it takes a frame
(InputPort::writer_qos_for_test).

* fix(cmux-tui-core): the input port's writer runs at user-interactive QoS once it carries a person's key (R-DAEMON-STATE S1.6d, #35413 review)

The person's keys cross the connection's three threads at
user-interactive QoS (server/input_qos.rs, TYPING FIX 2 F7), then the
terminal's port writer, which ran at the default QoS: under full CPU it
waited for a CPU between the key's acceptance (daemon.send.written) and
its write to the host. DESIGN 2.6.1 asks for it at user-interactive
while it carries the person's input; it was listed and not built.

The writer now raises itself (input_qos::raise_this_thread, once per
thread) when it takes a frame with no waiter that is not a viewer size:
InputPort::enqueue, which only the person's keys use
(InputReply::Accepted). Automation's writes (InputPort::write, a waiter)
and viewer sizes do not raise it, so a port only automation writes to
keeps the default QoS. Off macOS this sets nothing.

Test (red at b0786647b9e, macOS):
the_input_ports_writer_runs_at_user_interactive_qos_once_it_carries_a_persons_key.

* test(cmux-tui-core): false, off and no turn the input lane off too (R-DAEMON-STATE S1.7 red, #35413 review)

* fix(cmux-tui-core): false, off and no turn the input lane off too, and the daemon logs which path its keys take (R-DAEMON-STATE S1.7, #35413 review)

Only the exact value 0 turned the lane off, so an operator who set
CMUX_TUI_RDS_INPUT=false or off for the one-nightly fallback kept the
lane on with no sign of it. 0, false, off and no (any case, spaces
ignored) now turn it off, and the first switch a process reads is
logged: "cmux-tui: input lane on, CMUX_TUI_RDS_INPUT unset" or
"... off (the old path), CMUX_TUI_RDS_INPUT=\"0\"".

Test (red at 5fab0a55b51): false_off_and_no_turn_the_input_lane_off_too.

* test(cmux-tui-core): the port writer's QoS test seam exists only in macOS test builds, where its test runs (clippy dead_code on Linux)

* fix(cmux-tui-core): the lane log's match arm ends with a semicolon (clippy semicolon_if_nothing_returned)

* test(cmux-tui): a terminal's next output frame must reach it while a publication waits for the registry (#35413 echo stall; red)

The output readers (hosted and local) make the durable commits their output
asks for (directory, progress, activity, last output) inline, under the
workspace registry lock. Under L6 that lock waits up to 1.5 s and the next
output frame, the echo of a person's key, waits behind it (s17d: all 16 echo
gaps over 20 ms began inside one of those commits, lane on and off).

Both tests hold the workspace registry on another thread, let a first output
frame make a publication due, then send the next frame: it must reach the
terminal within 2 s. Today it reaches it only after the registry is free.

* fix(cmux-tui): durable publications of terminal output commit on a per-mux publisher, not the output reader (#35413 echo stall)

Root cause of the L6 typing echo stalls (s17d diagnostic rounds, lane on and
off): after each output frame the terminal's output reader published the
durable facts the frame changed (cwd, OSC 9;4 progress, OSC 7501 program
status, the command and prompt facts, and "last output", pushed every 5 s
while the terminal prints) itself, as resource commits under the workspace
registry lock. Under load that lock waits up to 1.5 s; the next frame, the
echo of a person's key, waited in the reader's queue behind it. All 16 echo
gaps over 20 ms in s17d (to 501 ms) began inside one of those commits, 26 to
27 of them per round, about every 5.1 s. The reader also posted OSC 9/777/99
notifications and bell attention marks, durable commits of the same kind.

Now the reader only checks, in memory under the terminal's own locks, whether
a publication is due (new has_change / has_publication_change peeks that
mirror each take exactly) and queues it on the mux's terminal publisher
(mux/terminal_publisher.rs): one worker thread that runs the queue in order.
A terminal has at most one publication queued, so changes that arrive while
it waits are published with it, as the latest state, which is what every
publication commits. Notifications and bell marks queue one by one, in
output order. All four reader sites move: hosted output, hosted resize,
hosted reconnect and the local PTY reader. Adoption, the first view, a
terminal read and the topology commits still publish directly on their own
threads. At shutdown, after the readers stop, the publisher finishes its
queue within the same reader grace before the journal stops, as when the
readers published inline.

Tests: output_publications (red at 2c75845dec6: the next frame did not reach
the terminal while the registry was held) for the hosted and the local
reader; terminal_publisher: a queued publication commits once the registry is
free and not before; notifications and the bell mark post in output order.

* test(cmux-tui): output publication tests use the channel names already in scope (unused-qualifications)

* test(cmux-tui): an exit commits the last reported directory; an alert posts after earlier notifications (#35765 review; red)

Two orderings the inline publication on the output reader kept, and the
terminal publisher (a91fb3a343e) lost:
- a_terminal_exit_commits_the_directory_its_shell_reported_last: the shell's
  last cwd report is queued, then the host's Exit frame arrives; the queued
  publication runs after the exit and commits nothing (an ended terminal
  settles without publishing), so the exited tab and its restart keep the old
  directory.
- a_programs_alert_posts_after_the_notifications_of_earlier_output: an OSC 9
  notification from one frame, then an OSC 7501 alert from a later frame,
  while the registry is held; the publication job ran ahead of the earlier
  frame's notification job and took and posted the alert itself, first.

Test-only seams: Mux::hold_terminal_publisher_for_test (the worker takes no
job until the hold drops) and wait_for_terminal_publisher_for_test. Also the
`closed` doc: queued jobs still run after the close.

* fix(cmux-tui): an exit commits the last reported directory; alerts keep output order (#35765 review)

Review findings on the terminal publisher (a91fb3a343e), red at 19bcc9ec556:
- Exit (should-fix 4): the hosted reader used to commit the shell's last cwd
  before it handled the host's Exit frame. With the publisher, that queued
  publication could run after the exit, and an ended terminal settles
  without publishing, so `cd x; exit` kept the old directory on the exited
  tab, its exit record and its restart. mark_hosted_surface_exited now
  publishes the pending directory itself before the exit commit (on the
  thread that commits the exit: the reader at its Exit frame, or the exit
  detach retry).
- Alert order (note 9): the publication job took and posted the OSC 7501
  alerts itself, so an alert from later output could post before an earlier
  frame's OSC 9 notification (and pass the rate limit at a different time).
  The reader now takes and rate-limits the alerts at the output that raised
  them, as the inline publication did, and queues them after the
  publication of their records; the publisher's job
  (Surface::publish_pending_state) leaves alerts alone. The program-status
  peek no longer counts alerts. Direct callers keep publish_pending_progress
  unchanged.
- Docs (should-fix 7, 8): the reader still takes Mux::state for its events,
  so the comment now says it never waits for the registry; the module doc
  names the real direct callers (adoption, first view, terminal.get,
  topology commits, a hosted exit) and why the queue stays bounded; the
  shutdown comment covers Mux::drop.
- The worker reports with writeln to stderr, not eprintln, which panics when
  stderr fails and would have stopped the worker for good (note 2).

The close race the review mentions (a tab closed while its cwd publication
waits) is not new: the inline reader waited for the same registry lock as
the close and lost the same race.

* test(cmux-tui): a host loss commits the directory the shell reported last (#35765 review 2; red)

Second review, should-fix 1: when a hosted terminal's host connection is
lost, the reader commits the terminal Adopting before the publication it
queued for the terminal publisher runs. An adopting terminal keeps a
directory publication pending without committing it, and a host declared
dead then ends it, so the dead tab and its L2 respawn kept the old
directory. The inline reader always committed the directory before it read
the end of the stream.

a_host_loss_commits_the_directory_the_shell_reported_last: a seeded Running
terminal whose catalog runtime is a live hosted reader on a fake host
socket; with the publisher held, an OSC 7 report reaches the terminal, the
host socket is dropped and the reader commits the loss; after the publisher
drains, the terminal must present the reported directory.

Test-only seams: Mux::install_live_terminal_runtime_for_test and
next_surface_id_for_test.

* fix(cmux-tui): a terminal's stream end settles its publications before it leaves Running (#35765 review 2)

Second review, should-fix 1 (red at 3108cf0bb75): a host connection lost
just after a cwd report kept the old directory. The reader commits the
terminal Adopting when its stream is lost, and the publication it queued
for the terminal publisher then commits nothing (an adopting or ended
terminal never does), so the dead tab and its L2 respawn kept the old
directory; progress and activity taken by such a job were dropped too
(review note 2).

Mux::settle_terminal_publications: take the terminal's queued publication
off the publisher's queue, wait while the worker runs one for that terminal,
then publish its directory, progress, activity and program status on the
calling thread. Called
- by the hosted reader when its stream ends by an exit or a loss, before
  the Exited or Adopting commit (a resync keeps the terminal running: its
  queued publication still commits);
- by Mux::surface_exited for every exit (local PTY exits, dead hosts), before
  the creation fences, so a registry wait there no longer holds creations
  (review note 5). The flush moves out of mark_hosted_surface_exited.
This is the order the inline reader had: it published before it read the
end of its stream. No output frame waits behind it: the stream has ended.

Publications are matched by terminal runtime, not by view, so a settle also
finds a publication queued through another view of the same terminal.

Docs (review note 6): post_terminal_notifications and the program status
module now say that the reader takes the alerts and the publisher posts them.

* test(cmux-tui): the host-loss test adopts its seeded terminal before the live reader starts

At 3108cf0bb75 the test failed at its setup, not at its assertion, on both
the old and the new code (fleet step 7612c04c51d785247a6fd3c2: "terminal is
not awaiting host topology publication"): a hosted reader starts only for a
launching or adopting terminal, and the seeded terminal was running. The
test now commits the seeded terminal Adopting with its incarnation, starts
the live reader, makes it the catalog runtime, releases its pending host
binding and commits the terminal Running, as an adoption does. Also drops a
redundant clone (clippy).

Test-only seam: Mux::commit_terminal_lifecycle_for_test.

* test(cmux-tui): the host-loss test moves its workspace key (clippy redundant_clone) and imports the lifecycle at the top

* test(cmux-tui): the tree entity check must hold when a reader changes the tab's title between its builds (red)

Full Linux suite at b2ebe6809df (hq run 41899477440):
live_root_transfer_revocation_survives_restart_and_both_abort_paths_clear_it
failed because the test-only check in tree_entity_json (server/tree_json.rs)
panicked on the terminal publisher thread: "the TabChanged entity of 7
differs from the tree's", title "" vs the shell's title. The check builds
the entity, then the whole tree, and compares them; both read live fields
(title, size, connection state, end) without the state lock, so a reader
that sets its title between the two builds fails it, and the panic, under
the state lock, poisons the session (STATUS 12320 reported the same race in
a title test). Before #35765 a terminal's directory TabChanged was built on
its own reader, which could not set the title meanwhile; the publisher
builds it on its own thread, so the race now hits every shell that reports
its directory and title at a prompt.

the_entity_check_holds_when_a_reader_changes_the_title_between_its_builds:
a test seam (set_entity_check_hook_for_test) runs between the entity build
and the whole tree build and sets the tab's title, as the reader would.

* fix(cmux-tui): the tree entity check compares the two builders only on inputs that held still

Red at 2f52b7eb82e (the_entity_check_holds_when_a_reader_changes_the_title_between_its_builds).
The test-only check in tree_entity_json now builds the entity again after
the whole tree and compares them only when the entity built before and
after the tree agree: then the live fields both builders read without the
state lock (title, size, connection state, end) held still across the
tree's build, and a difference is a real builder difference. Live fields
that moved are read again (at most 16 times; a field that never stops
moving leaves nothing to compare). The check's builds still do not count
as built panes. Production code is unchanged: the check is cfg(test).

This was a base race (STATUS 12320, reported to the #32408 owner); #35765
makes it frequent, because a terminal's directory TabChanged is now built
on the terminal publisher while the terminal's reader can set its title.

* test(cmux-tui): a person's key to an idle input port is on the host socket when its request returns (#35413 A/B; red)

The s17fa A/B on build 5cd1899b (STATUS 12857, 12866): with the input lane
on, the daemon part of a key was +0.025 / +0.17 / +0.53 ms at p50 / p95 /
p99 over the lane off, all of it in daemon.send.written -> host.pty.write.
The request thread puts the key on the terminal's input port and returns;
the port's writer thread then wakes to write the host socket, and under the
L6 CPU load the wake is the extra time. With the lane off the request
thread writes the socket itself.

The test holds the port's writer before it writes anything, sends a key,
and reads the host socket: the key must already be there.

* fix(cmux-tui): a person's key to an idle input port is written on its own turn, with no thread hand-off (#35413 A/B)

Red at dd67a789756. The s17fa A/B on build 5cd1899b (STATUS 12857, 12866):
with the S1 input lane on, the daemon part of a key was +0.025 / +0.17 /
+0.53 ms at p50 / p95 / p99 over the lane off, all of it between
daemon.send.written and the host's PTY write (OFF 0.016-0.019 / 0.14-0.17 /
0.38-0.48 ms, ON 0.038-0.046 / 0.27-0.32 / 0.67-0.99 ms, in every round,
also in s17d). The request thread put each key on the terminal's input
port; the port's writer thread then woke to write the host socket, and
under the L6 CPU load that wake was the extra time. With the lane off the
request thread wrote the socket itself.

Now a key (no waiter, at most 1 KB) to an idle port (nothing queued or in
flight, no reconnect wait) is written on the request's own turn, under the
port lock, when HostInputRoute::try_send_now can write it at once: try_lock
on the connection's writer and a non-blocking send (MSG_DONTWAIT, and
MSG_NOSIGNAL off macOS) that a Unix stream socket takes whole or not at
all for a frame this small (a partial write would shut the connection down,
as a failed write does). A busy writer, a full socket, an error or a frame
ahead leaves the key to the port as before, so a key still never waits for
another writer or a host that stopped reading (S1.6). The key counts as one
of the port's frames, taken and finished at once, so the order barrier and
the viewer-size queue see the same state as before.

Tests: the port's writer tests that hold a key at a paused writer opt out
of the inline write (queue_every_write_for_test), so they still exercise
the writer's order, reconnect and QoS.

* test(cmux-tui): a notification queued before a terminal's exit names its terminal (#35765 review 3; red)

Third review, should-fix 2: the settle at a terminal's stream end and exit
took only the terminal's queued publication. A notification its last output
asked for (OSC 9/777/99, an OSC 7501 alert, a bell mark) stayed queued and
posted after the exit removed the terminal's tab, so it named no terminal.
The inline reader posted it before it handled the exit.

a_notification_queued_before_an_exit_names_its_terminal: a seeded running
terminal (closes on exit); with the publisher held, a notification is queued
for it, then it exits; the posted notification must name the terminal.

* fix(cmux-tui): a terminal's settle takes its notifications too; no surface is dropped under the publisher lock (#35765 review 3)

Red at 37fd891b8a3 (a_notification_queued_before_an_exit_names_its_terminal).

- should-fix 2: TerminalPublisher::settle now takes every queued job of the
  terminal (its publication, notifications and bell…

This branch was successfully deployed

1 inactive deployment
artifacts — ae5c3cf1 Deployed Nov 4, 2026 by cmux-bot[bot] via publish tree-addressed cmux-tui #108821
sentry-dsyms — ae5c3cf1 Deployed Nov 4, 2026 by cmux-bot[bot] via upload cmux-tui dSYMs to Sentry #108821
ci-routing-agent — ae5c3cf1 Deployed Nov 4, 2026 by cmux-bot[bot] via cmux-tui tree preflight #108821
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-to-land Reviewed at this head; next-autoland merges it into feat-cmux-next when its only reds are base reds risk:normal Risk tier (docs/ci/risk-tiers.md)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant