Skip to content

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) - #34174

Merged
lawrencecchen merged 33 commits into
feat-cmux-nextfrom
fix/p0-typing-s1-creating-input
Nov 4, 2026
Merged

lawrencecchen merged 33 commits into
feat-cmux-nextfrom
fix/p0-typing-s1-creating-input

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

Summary

R-DAEMON-STATE stage S1 step 4 (handoff/rewrites/R-DAEMON-STATE/DESIGN.md, section 3 and Appendix A.2 step 16). Base feat-cmux-next (S1.2, #34171, and S1.3, #34146, have landed). Behind the same per-mux switch CMUX_TUI_RDS_INPUT (default off).

Every key to a live terminal first asks whether creating input of its terminal must go first (wait_for_creating_input_flush, terminal-input-while-creating-v1). It took the daemon-wide creating-input lock even when no terminal was being created, and that lock's holders (begin_creating_input, knows_terminal_input_target, finish_creating_input) looked up the live terminal under it, which takes the session lock. So a key waited for any creation in progress and, behind it, for any holder of the session lock. The S1 census measured a full wait on the creating-input lock (300.3 ms, hosted run 41648095321).

With the switch on:

  • 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 then: a terminal's queue is added before the terminal goes live, and removed only after every queued byte was written to it.
  • The holders of the creating-input lock look up the live terminal in the published routes, not under the session lock. No holder waits for the session lock while it holds the creating-input lock, so a key that takes that lock (while another terminal is being created) 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.
  • Why the creation checks keep their answers: a commit publishes its routes before it releases the session lock, and finish_creating_input runs after the commit (the old path could not call it under the session lock: it would deadlock), so begin, knows and finish see what they saw under the session lock.

With the switch 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 have been since step 1.

Testing

a_key_waits_for_no_creating_input_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 (1) the creating-input lock, (2) the session lock while a reader waits for it under the creating-input lock, with another terminal being created so the key cannot skip that lock. Each holder has an old-path control (switch off: the key must wait).

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

Compile: fleet job 862158b530775c4020d371bd at 8a50288 passed (CmuxNext 282 s, app 94 s, cmux-tui 61 s).

Observed / Not run

  • Observed: the hosted runs and the fleet compile above.
  • Not run: the full cmux-tui suite on this head (with the stack, after the merge with feat); macOS execution of the focused test; the real-app A/B (after S1 lands).

Review follow-ups (head 8ee8220)

The review (comment 6613998735, SHIP at 8a50288) left notes; taken in 3b2e29f:

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).
…roxy 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.
… (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.
…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.
…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.
…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.
… 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.
@cursor

cursor Bot commented Nov 3, 2026

Copy link
Copy Markdown

Bugbot needs on-demand usage enabled

Bugbot uses usage-based billing for this team and requires on-demand usage to be enabled.

A team admin can enable on-demand usage in the Cursor dashboard.

@lawrencecchen lawrencecchen added the risk:normal Risk tier (docs/ci/risk-tiers.md) label Nov 3, 2026
@lawrencecchen

Copy link
Copy Markdown
Contributor Author

Review (claude-reviewer-rds-s14): SHIP at 8a50288
reviewer-session: 0c608041-99d1-40a5-b456-d71ce58b2bcf
author-session: ae25c6d154f94c01a
R9: pass | layer-ownership.md row "Terminals: PTY, process, output, cwd, title, exit, grid size" (Rust, session host). terminal_catalog_by_host keeps its only writers, State::insert_catalog_terminal and remove_catalog_terminal, under the session state guard. The RouteSnapshot is a read-only copy that the same guard publishes when it drops. The creating-input map keeps its only writers, CreatingInputs::insert and remove, under its own lock, and the count is a value those writers store under that lock. The PR adds no writer.
perf claim: none

No defect blocks the merge. I traced each path below in the head checkout. I did not build or run anything locally.

What I checked

  1. Ordering with the lane on. All inserts and removals of a CreatingTerminal go through CreatingInputs::insert and remove (creating_input.rs:61-85). The map is private, and nothing else writes it. Each helper stores len() with Release under the lock, so the stores follow the lock order.
    • A key gets its surface from the published routes (input_surface → get_surface → Mux::surface). For terminal.input.*, the surface comes from Mux::surface after a session-lock read.
    • begin_creating_input runs at submit, on the dispatcher. That is before the job, the commit and the routes publication (an ArcSwap swap under the state lock). So a key that found the new terminal reads the count at or after that insert's store.
    • The key reads 0 only from a store made while the map was empty, so after the terminal's entry was removed. The flush removes the entry under the lock, and only after it wrote the last batch. Every store follows the previous holder's unlock, so the flush's writes happen before the key's write, also when a store for another terminal is the one the key reads.
    • Sometimes begin reads routes from before a commit's publication and registers a terminal that is already live. A key misses that entry only if it read the count before the insert, which means the key arrived first. The old path has the same race, because its state guard is dropped before the insert.
    • send-key, clear-history, image paste and terminal.input.* call the same function with a surface from Mux::surface. None of them holds a mux lock there.
  2. Creation checks on the snapshot. A guard publishes before it unlocks.
    • CreateCleanup (terminal_create.rs:457-463) calls finish_creating_input after run_pending_request returns, on the committing thread. activate_created_terminal_launch runs on that thread before it. A group member calls it after wait_answer, which comes after the leader's commit.
    • The other callers hold no session lock: the expiry thread, with a failure and no lookup, and wait_for_creating_input_flush, whose surface came from the routes.
    • begin and knows can take effect before a guard that is still held. The old path can do the same if it takes the lock first. I found no interleaving that loses or misroutes input that the old path cannot also produce. That includes a close or a respawn under the same host id, and a creation that commits while begin runs.
  3. By-host table. The only writers are terminal_catalog_index.rs:49 and :67. Each writes the catalog in the same guard. The constructors are restore.rs:333 and public_projections/tests.rs:27.
    • RouteKey for String → None is correct: no SurfaceId routes through the host index, so the old path's kept terminal never depends on it.
    • of and is_current with four tables are correct. Arc::ptr_eq cannot give a false match, because the snapshot keeps its Arcs alive.
    • indexed_owner and live_terminal are the old bodies of indexed_catalog_terminal and live_surface_for_terminal, so both arms give the same answers as before.
  4. Off arm. It takes the same locks as the base. It adds one Release store per insert and removal. The by-host index is now copy-on-write: a State clone is cheaper, and the first write after a clone copies the map once. No post-commit apply closure writes the catalog or the by-host index.
  5. Tests. Red at eecb0d8 (run 41699822496, job 126139931200): controls 300.17 ms and 300.28 ms, lane 300.24 ms and 300.36 ms, and a_key_waits_for_no_creating_input_lock FAILED. Green at the head (run 41700308786, job 126141589752): rds_input_lane passed 6 of 6.
    • The second holder is sound. begin_creating_input(CREATING_TERMINAL) sets the count to 1, so the key must take the lock. On the old path, the reader blocks under the creating lock while it waits for the held session lock.
    • The test is deterministic enough. The margins are 300 ms against 150 ms, and the 1 s spin only limits how long the control's setup can take.
  6. Ratchets. The PR adds no unwrap, expect, indexing, arithmetic or panic to product code. The only lock calls are unwrap_or_else(PoisonError::into_inner).
  7. Compile. Fleet job 862158b530775c4020d371bd at the head: CmuxNext ok (282.1 s), app ok (93.9 s), cmux-tui ok (60.8 s). It shows no warning in the changed files.

Non-blocking notes

  1. No test runs the ordering contract with the lane on. creation_burst_tests.rs and cmux-tui/tests/terminal_host_recovery/creating_input.rs run with the lane off. This PR's key goes to a local test PTY with no host identity (surface/test_spawn.rs:202), so with a count above 0 it returns before the queue code. The lane-default-on rehearsal, run 41706039745 at b8f599a (it contains this head), was still queued when I reviewed. Read its creating-input results before S1.7 turns the lane on by default.
  2. A comment is out of date at creating_input.rs:130-131. It says "lock order: creating, then state), so a commit cannot make the id live in between". On the lane, begin takes no state lock. On both arms, the state guard is dropped before the insert, so a commit can come in between. Please say that begin reads the routes (lane on) or the session state (lane off). Also say that an entry made for a terminal that just went live is written to it in order, by the creation's finish or by the next input on another path.
  3. Panic path. On the lane, assume a create's commit panics after it puts the terminal in the state. Its queue then goes back as "failed", because the guard that was cut short publishes nothing. The old path wrote the queue into the half-made terminal. No bytes are lost, and the result agrees with the CreateCleanup doc ("else the input goes back as failed").
  4. The base stack has a red that is not from this PR. The green run's lint (linux) and macOS lint and tests jobs also fail on unused_qualifications at crates/cmux-tui-core/src/mux/tests/signaled_mutex.rs:120 (commit 4462389, fix(cmux-tui-core): link peers and host proxy authors are published; S1.1 review follow-ups (R-DAEMON-STATE S1 step 2) #34171). The description names only the acpmux red. base red: the notify Watcher import is macOS-only (unused on Linux after #33406) #33664 and base red: acpmux's login-env watcher test module is Linux-only, so macOS has no unused import (clippy, after #33406) #33690 on feat do not fix this one, because the line exists only in the stack. fix(cmux-tui-core): link peers and host proxy authors are published; S1.1 review follow-ups (R-DAEMON-STATE S1 step 2) #34171 must fix it, or both jobs stay red after the retarget. The macOS failure also includes an acpmux test-binary abort at tests/support/daemon_env.rs:142, which is not related to this PR.
  5. Perf, no action. While any terminal is being created, the key still takes the creating lock. Its longest holder is now begin's scan of every surface for a new id (live_terminal, route_table.rs:237), which clones each host identity. That work is limited and takes no other lock, and the old path did the same scan. On the lane, the first catalog write after each publication copies the by-host map (two Strings per entry) under the session lock. That copy is not on the key path.

…(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.
…lished 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.
…oisoned 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.
…ly 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.
@cursor

cursor Bot commented Nov 3, 2026

Copy link
Copy Markdown

Bugbot needs on-demand usage enabled

Bugbot uses usage-based billing for this team and requires on-demand usage to be enabled.

A team admin can enable on-demand usage in the Cursor dashboard.

lawrencecchen added a commit that referenced this pull request Nov 4, 2026
…S1.1 review follow-ups (R-DAEMON-STATE S1 step 2) (#34171)

* 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.

* 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-util): a test dereferences the published u64 once (E0614 in bb0265f)

* 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.

---------

Co-authored-by: cmux-bot[bot] <348531757+cmux-bot[bot]@users.noreply.github.com>
@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 4, 2026
… its content is already here (7e8aa9e); tree unchanged
Merge-main commit by scripts/merge-main.sh.
Merged by scripts/merge-main.sh: origin/feat-cmux-next at 2e0b6ac.

Merge-main-previous-head: 6234806
Merge-main-base: 2e0b6ac
@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 4, 2026
@github-actions

github-actions Bot commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

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

Review 5aa3ec4: 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=pr34174 -f ref=fix/p0-typing-s1-creating-input -f platform=macos -f backend_mode=local

Shipped in:

@blacksmith-sh

blacksmith-sh Bot commented Nov 4, 2026

Copy link
Copy Markdown

Found 6 test failures on Blacksmith runners:

Failures

Test View Logs
/tmp/tmpc2olstlb/summary-fail.json (live):/summary-fail.json (live): View Logs
/tmp/tmpejkr5dgq/summary-missing.json (live):/summary-missing.json (live): View Logs
[new_workspaces] debug.new_workspace timed 0 of 50 new workspace rows/
[new_workspaces] debug.new_workspace timed 0 of 50 new workspace rows
View Logs
[new_workspaces] the window draws no frames/[new_workspaces] the window draws no frames View Logs
[tabs] debug.tab_switch timed 0 of 40 switches/
[tabs] debug.tab_switch timed 0 of 40 switches
View Logs
[tabs] the window draws no frames/[tabs] the window draws no frames 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-s14): SHIP at 5aa3ec4
reviewer-session: 64b87adf-5554-426d-ab2b-b3ae166399af
author-session: ae25c6d154f94c01a
R9: pass | layer-ownership.md row "Terminals: PTY, process, output, cwd, title, exit, grid size" (Rust, session host). The PR adds no writer. terminal_catalog_by_host is written only by State::insert_catalog_terminal and remove_catalog_terminal under the session state guard, and the RouteSnapshot is a read-only copy that the same guard publishes before it unlocks. The creating-input map is written only by CreatingInputs::insert and remove under its own lock, and they also store the count. The feat range merged since my first review adds no writer of either.
perf claim: none

No defect blocks the merge. I traced the paths below in the head checkout. I did not build or run anything locally.

What changed since my SHIP at 8a50288

One product commit, 3b2e29f (the poison check and the begin_creating_input comment), and merges.

The merges keep the PR's change.

  • The stable patch-id is 5f12b0acf23e for each of these:
    • git diff 7e8aa9e0e87 7b90f439ed7 (before);
    • git diff e039c73b64a 62348068a30 (after the squash);
    • the diff against the merge base 2e0b6ac;
    • gh pr diff.
  • 6234806 has its first parent's tree (ccd30576e3f1).
  • The squash e039c73 has the content of 7e8aa9e, the S1.2 head that this branch merged: patch-id 39adb8693d5d against each one's feat parent.
  • Against feat, the head changes only the PR's 7 files.

1. The live check on a poisoned state (3b2e29f)

  • Old read. live_surface_for_terminal (mux/creating_input.rs:375-388) takes the old read on a poisoned state. That read recovers the poison (unwrap_or_else(PoisonError::into_inner)) as on the base, so the creation checks give the old answers.
  • A create whose commit panics. The cut-short guard publishes nothing. Its MutexGuard poisons the state before CreateCleanup (terminal_create.rs:457) runs in the outer frame. So finish_creating_input sees the poison, takes the old read and writes the queue into the half-made terminal, as the old path does. This closes my earlier note 3.
  • Lock order. The old read takes the state under the creating-input lock (creating, then state), as the base does. No caller takes the creating-input lock while it holds the session lock, so the old read cannot deadlock. I checked every caller:
    • terminal_create.rs:203 and :463. CreateCleanup is made in commit_creation, which holds no guard.
    • server/creating_input.rs:52 and :66.
    • cmd_terminal_io.rs:199, :290 and :492.
    • image_paste.rs:24.
    • resource_router/content.rs:418.
  • Check, then load. This function checks the poison and then loads the routes. Mux::surface loads first (19dd118). Between the check and the load, another thread can poison the state, and a guard that recovers the poison can then publish half-made tables. Here that is harmless: this function's old read also recovers the poison and reads those same tables, at an instant inside the call. Mux::surface must load first only because its old read fails (unwrap()). See note 1.
  • Fast return. The lane's fast return (mux/creating_input.rs:213) reads no session state, so a poisoned state cannot change its answer. The count equals the map's length at every unlock. insert and remove are the only writers of the length, and nothing that can panic comes between the map change and the store.

2. The ordering rule (terminal-input-while-creating-v1)

The rule holds on both arms.

  • Every path that writes to a live surface gets the surface from Mux::surface or under the session lock. That includes input_surface, get_surface, image paste and the terminal.input.* effect.
  • On the lane, Mux::surface reads the routes. On a poisoned state it takes the old read, which fails.
  • So a key that found the new terminal read the commit's publication. That publication comes after begin_creating_input stored the count (at submit, before the job). So the key's Acquire load reads that store or a later one.
  • A 0 comes from a store made while the map was empty, so after the entry was removed. The flush removes the entry only after its last write, and every store follows the previous holder's unlock. So the flush's writes come before the key's write.
  • send {terminal_id} still queues under the lock while the entry exists.

3. Feat changes merged since the first review (7cdc873 to 2e0b6ac)

  • No new caller of begin_creating_input, finish_creating_input, knows_terminal_input_target, write_terminal_input or wait_for_creating_input_flush. server/creating_input.rs is unchanged.
  • No new reader of StateMutex::routes(). No added line writes the four route tables or by_host, and no added line replaces the whole state. The callers of insert_catalog_terminal and remove_catalog_terminal are the same.
  • Mux::surface changed only in S1.2 (bb0265f, 19dd118). That change is the poison fallback that this PR follows.
  • There is one new reader of the catalog: terminals_held_by_this_owner_locked (mux/pending_terminals.rs:77, cmux-tui: a kept tab, a crash restart and an abandoned respawn keep an app session's owner (R-LIFECYCLE S1b) #34188). It reads terminal_catalog keys under lock_state_pinned (registry, connection, state) and takes no creating-input lock.
  • tab_drag.rs:443 adds a read-only pane-id check under the registry and state locks.
  • Neither of these interacts with this PR.
  • 164d65f reverted 339 files by mistake, S1.2's among them, and 93703fe restored them. In cmux-tui-core, 7e8aa9e and 2e0b6ac differ only in server/agent_session_shares{,_tests}.rs (cmux daemon: chat-share invites answer their invite link #34634).

4. Evidence at the head

Hosted run 41783051119

  • test (linux) (job 126421284534): rds_input_lane 6 of 6 ok, including a_key_waits_for_no_creating_input_lock.
  • lint (linux) (job 126421283921) is red, on base findings only:
  • Rust MSRV (1.91), Go pane SDK and the aarch64-apple-darwin dogfood build passed.
  • macOS lint and tests was still queued when I posted.

Fleet compile job 49f9b443e09b623932a07322 at 5aa3ec4

  • CmuxNext ok (299 s), app ok (92 s), and cmux-tui cargo check ok (58 s). The cargo check includes cmux-tui-core. The job ended with exit=0.
  • It shows no warning for cmux-tui-core.

Lane-on run of the contract (my earlier note 1)

  • This is now answered for the code at 8a50288.
  • The lane-default-on rehearsal is run 41706039745 at b8f599a. That commit contains 8a50288, but not 3b2e29f.
  • Its Linux job 126161574978 ran the five terminal_host_recovery creating_input tests with a real daemon. All five passed, including queued_input_precedes_input_sent_to_the_new_surface and input_for_a_creating_terminal_is_queued_then_written_in_order.
  • That job is red for other tests.

Non-blocking notes

  1. A comment is out of date at mux/creating_input.rs:376. It says "A poisoned state takes the old read, as Mux::surface does". Since 19dd118, Mux::surface loads the routes before its poison check, and this function checks first. The behavior is correct (see section 1), but the comment says the order is the same. Either load first, as Mux::surface does, or say why the order does not matter here.
  2. The creating-input lock is a leaf only while the state is not poisoned. On a poisoned state, its holders take the session lock under it. Two places should say so, so that no later change takes the creating-input lock under the session lock:
    • the doc at creating_input.rs:369-373 ("no holder waits for the session lock under it");
    • DESIGN.md S1.4 ("never Mux::state").
  3. No test covers the poisoned arm of live_surface_for_terminal. 3b2e29f has no regression test. A test like a_poisoned_session_state_never_routes_by_the_published_tables would cover it: lane on, a change that puts a terminal in the state and then panics, then knows_terminal_input_target must answer from the state.
  4. The off-arm sentence at creating_input.rs:130-132 is not exact. It says "so a commit cannot make the id live in between", but on that arm too the state guard is dropped before the insert. This is harmless: an entry made for a terminal that just went live is written to it in order, by the creation's finish or by the next input on another path.
  5. The PR description is out of date.

@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
@lawrencecchen
lawrencecchen merged commit eaa00d9 into feat-cmux-next Nov 4, 2026
260 of 264 checks passed
@lawrencecchen
lawrencecchen deleted the fix/p0-typing-s1-creating-input branch November 4, 2026 07:06
lawrencecchen added a commit that referenced this pull request Nov 4, 2026
… its content is here through the S1.4 head
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
…ating-input check; it loads the routes before its poison check (#34174 review follow-ups) (#35127)

* test(cmux-tui-core): a poisoned session state answers the creating-input checks from the state, not the published routes (#34174 review follow-up)

The poison check of 3b2e29f had no test. Lane on: a create's commit puts its terminal in the state and panics;
the cut-short guard publishes no routes, so knows_terminal_input_target must answer from the state.

* fix(cmux-tui-core): the creating-input live check loads the routes before its poison check, as Mux::surface does; the comments state the leaf rule and the insert race (#34174 review follow-ups)

- live_surface_for_terminal loads the published routes before the poison check (poison never clears), so the routes
  it answers with come from a change that finished before any panic; its old read still recovers the poison.
- The creating-input lock is a leaf only while the state is not poisoned: on a poisoned state its holders take the
  session lock under it, so no code may take it under the session lock (doc and DESIGN S1.4).
- begin_creating_input: on both arms the live read ends before the insert; an entry made for a terminal that just
  went live is written to it in order, by the creation's finish or by the next input on another path.

Test: mux::tests::signaled_mutex::a_poisoned_session_state_answers_the_creating_input_checks_from_the_state.

* fix(cmux-tui-core): the poisoned-arm test names its types through the mux scope (clippy unused_qualifications); comment wording (#35127 review 1)

- mux/tests/signaled_mutex.rs: TerminalHostIdentity, TerminalId, Surface and SurfaceOptions are in scope through
  use super::*; the crate:: prefixes failed clippy -D warnings (clippy (changed cmux-tui packages), lint (linux)).
- creating_input.rs: a queue can also be given back; the leaf rule is stated for the lane; the routes lack a
  panicked create's terminal only until a later change that recovers the poison publishes them.

---------

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 previously deployed

3 inactive deployments
ci-routing-agent — 5aa3ec47 Deployed Nov 4, 2026 by github-actions[bot] via cmux-next macOS placement #68940
artifacts — 5aa3ec47 Deployed Nov 4, 2026 by lawrencecchen via publish tree-addressed cmux-tui #109986
sentry-dsyms — 5aa3ec47 Deployed Nov 4, 2026 by lawrencecchen via upload cmux-tui dSYMs to Sentry #109986
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