Repository navigation
P0 typing under load (F4): the person's keys never wait for the daemon's session lock - #31940
Conversation
… session lock (TYPING UNDER LOAD F4) Under the L6 load the person's key waited in the daemon before its PTY write: socket submit to PTY arrival p95 54 ms, max 475 ms (STATUS 5299). The input lane test holds Mux.state on another thread for 300 ms (as a list, a snapshot or a durable tab create does) and sends a key from an attached frontend client: the key must take under 50 ms. On the base the key's send looks its terminal up through Mux.state and waits.
…ING UNDER LOAD F4) A key's send looked its terminal up through Mux::surface (the global session lock) before the PTY write, and its sizing note looked it up again after the write. Automation holds that lock for every list, snapshot and durable mutation (resource projections build the whole public snapshot under it; topology mutations hold it across their registry commits), so under the CLI/MCP storms a key waited up to 475 ms before its PTY write while the app's own part stayed under 35 ms. Now an attached client (the app's terminal view) keeps the terminal its keys go to in its attach record from the first key on (ClientRegistry attached_input_surface / remember_attached_input; it goes with the record at the last detach and at disconnect, and a dead terminal is looked up again), and note_terminal_input takes the terminal the bytes were written to. The typing path reads only the client registry, the terminal's own runtime lock and the sizing lock, never Mux.state. Unattached senders (the CLI's terminal write) keep the lookup. Chrome's rule: the person's input goes before automation, on its own path.
…terminal_activity)
|
cmux status (one comment, edited in place; each line is updated by the job that owns it) Review App of this head (app.zip, kept 1 day)
web and size checks: green Shipped in:
|
|
Review (claude-subagent, correctness first): SHIP at e777e82 I read the code at the head with git. I did not build or run anything. 1. Can the cached
2. Lifetime. Correct. The record is created by 3. Lock order. No new lock edge. 4. 5. Test. It is representative. The app attaches and sends on the same transport (TerminalAttachment.swift:212 and :283). The test uses the same
6. Is the fix complete for the measured hop? It is complete for the default configuration. I checked every step on the send path of a local attached client: the frame prelude, the scheduler FIFO,
None of the findings needs a change before this merges. |
…put-lane budget 150 ms Review follow-ups (#31940): a terminal found through the catalog has another surface id than the one the keys name, so the fast path keyed by the found id never served them. The test budget is 150 ms (the lock is held 300 ms; the red waited 310 ms), so a busy host does not flake it.
|
Review (claude-subagent, correctness first): SHIP at ddadeae I reviewed the diff from e777e82 to ddadeae (1 commit, 3 files) with git. I did not build or run anything.
These follow-ups are still open (none of them blocks this PR): clear |
|
Lead hold (perf lead): ready-to-land removed. The attach record keeps the terminal that a client's keys go to. If that kept terminal is not cleared or updated when the terminal respawns, closes or moves, the person's keys go to a dead terminal and are lost (the same class as REOPEN-F4/F5). Required in this PR: the kept terminal is dropped or updated on respawn, close and move, with a red test (respawn, then type: the key reaches the new shell). send_key and the one lock take per key with a size default can be follow-ups. |
… (TYPING UNDER LOAD F4, lead order) The terminal kept for a client's keys must never outlive the terminal its id routes to: a respawn replaces the runtime under the same placement id while the old one still lives, and the next key must reach the new shell. Red at the current #31940 head: the kept terminal is still alive and not dead, so the key goes to the replaced terminal.
…ent (TYPING UNDER LOAD F4) The person's keys skip the session lock by using the terminal kept in their attach record. That terminal could outlive the terminal its id routes to: a respawn puts a new runtime under the same placement id while the old one still lives, and a close or move changes the routing, so a key could go to a replaced terminal and be lost (the REOPEN-F4/F5 class; lead 2026-11-02). The routing maps (State::surfaces, State::terminal_catalog) are now RouteTables: reads deref to the map, and the only writes (insert, remove) move a process-wide route generation, so the compiler finds every write and none can skip it. A kept terminal carries the generation read before the lookup that found it; any later routing change (respawn, close, move, projection, rollback) makes the next key look it up again. Typing between routing changes still takes no session lock.
…o fix/p0-typing-f4-input-lane
Bugbot needs on-demand usage enabledBugbot 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. |
… before it commits); test lint resource_content's projection reserves room in State::surfaces before its commit cannot fail; RouteTable forwards try_reserve (it routes nothing). The input-lane test drops two unneeded qualifications.
This comment has been minimized.
This comment has been minimized.
|
Review (claude-subagent, correctness first): SHIP at 6306d65 This review replaces my SHIP at 15c133d, because the head moved. I read the code with git. I did not build or run anything.
These non-blocking follow-ups are still open:
Not observed by me: the fleet steps quoted in the PR body, including the table of the 20 head-only full-run failures. Each of those passes alone at 15c133d, as the author reports. |
…nges (TYPING FIX 2, #31940 follow-up A) #31940 checked the terminal kept for the person's keys against one process-wide route generation that every routing write moved. Every tab create or close anywhere (an agent's tab storm, a CLI new-workspace) therefore sent every attached client's next key back to the session lock: O(clients) lock takes per change, under the load the lane is for. Now each surface id has its own route generation (a fixed array of 1024 slots, id % 1024: bounded memory, nothing to remove, lock-free reads). A RouteTable write moves only the ids whose route it changes (RouteKey): a surfaces or terminal_catalog_by_runtime write its key, a terminal_catalog write the runtime ids of the owner it removes and the owner it puts in. terminal_catalog_by_runtime is a RouteTable now too, so every table an id routes through moves that id. Two ids in one slot only make a key look its terminal up once more, never use a stale one. The respawn, rollback and mutation tests of #31940 keep their meaning.
…FIX 2, #31940 follow-up B) Every sizing mutation re-expanded the host policy for its terminal, because participants may have come, gone or changed identity. A key's activity note changes none of them, and the expansion depends on nothing else a key changes. But the refresh first asks whether the terminal follows a workspace default (workspace_default_of), which looks the terminal's workspace up under the session lock and may walk every surface for its placements; with any workspace size default set, every key took the session lock again, behind automation. Activity now goes through mutate_terminal_sizing_keeping_participants, which is mutate_terminal_sizing without that refresh. Every mutation that adds, removes or renames a participant still refreshes it, and a workspace default, terminal override or host policy change still re-applies to the terminals it covers.
…follow-up C) send-key looked its terminal up through the session lock (get_surface) on every call, so a named key from the app (Return, Ctrl-C, arrows sent as send-key) still waited behind automation's lists, snapshots and durable mutations. It now goes through input_surface, as send does: an attached client's kept terminal, valid only while its id's route generation is the one it was kept at (respawn, close, move and the other route changes make the next key look it up again); any other sender looks it up.
…nteractive QoS (TYPING FIX 2, F7) The daemon set no thread QoS, so under full CPU (the L6 storms keep every core busy) a key waited for a CPU at each of its connection's three threads: the fleet probe measured a default-QoS key thread's wake at p99 up to 7.7 ms and max up to 24.8 ms, plus a 13.2 ms preemption in the middle of a send (STATUS 6593). Now a key from a client attached to its terminal (the input lane of #31940, send and send-key) marks its connection as one that carries the person's input (MessageWriter::input_lane). The thread that runs the key raises itself at once; the connection's mux-conn reader and mux-line-out writer raise themselves on their next line or reply. One atomic load per line, one system call per thread. Automation's connections (CLI, MCP, agents writing to terminals they do not view) keep the default QoS, so they never compete with the person's input. This follows Chrome, which runs the thread that carries input at the highest priority. threading-model.md records the new QoS.
…no longer sends every client's next key to the session lock (#32249) * test(cmux-tui): a tab created or closed elsewhere keeps the key off the session lock (TYPING FIX 2, #31940 follow-up A) The terminal kept for the person's keys (#31940) is checked against one process-wide route generation that every routing write moves. A tab create or close in any workspace (an agent's tab storm) therefore sent every attached client's next key back to the session lock: O(clients) lock takes per routing change, during the very load the lane exists for. The test counts the session lock acquisitions of the key's turn on its own thread (a test-only counter in StateMutex), so it needs no timing: after a new workspace, a tab in it and that tab's close, the next key must take the lock 0 times. Red at this commit: it takes it once. * fix(cmux-tui): a kept terminal is dropped only when its own route changes (TYPING FIX 2, #31940 follow-up A) #31940 checked the terminal kept for the person's keys against one process-wide route generation that every routing write moved. Every tab create or close anywhere (an agent's tab storm, a CLI new-workspace) therefore sent every attached client's next key back to the session lock: O(clients) lock takes per change, under the load the lane is for. Now each surface id has its own route generation (a fixed array of 1024 slots, id % 1024: bounded memory, nothing to remove, lock-free reads). A RouteTable write moves only the ids whose route it changes (RouteKey): a surfaces or terminal_catalog_by_runtime write its key, a terminal_catalog write the runtime ids of the owner it removes and the owner it puts in. terminal_catalog_by_runtime is a RouteTable now too, so every table an id routes through moves that id. Two ids in one slot only make a key look its terminal up once more, never use a stale one. The respawn, rollback and mutation tests of #31940 keep their meaning. --------- Co-authored-by: cmux-bot[bot] <348531757+cmux-bot[bot]@users.noreply.github.com>
…a workspace has a size default (#32255) * test(cmux-tui): a tab created or closed elsewhere keeps the key off the session lock (TYPING FIX 2, #31940 follow-up A) The terminal kept for the person's keys (#31940) is checked against one process-wide route generation that every routing write moves. A tab create or close in any workspace (an agent's tab storm) therefore sent every attached client's next key back to the session lock: O(clients) lock takes per routing change, during the very load the lane exists for. The test counts the session lock acquisitions of the key's turn on its own thread (a test-only counter in StateMutex), so it needs no timing: after a new workspace, a tab in it and that tab's close, the next key must take the lock 0 times. Red at this commit: it takes it once. * fix(cmux-tui): a kept terminal is dropped only when its own route changes (TYPING FIX 2, #31940 follow-up A) #31940 checked the terminal kept for the person's keys against one process-wide route generation that every routing write moved. Every tab create or close anywhere (an agent's tab storm, a CLI new-workspace) therefore sent every attached client's next key back to the session lock: O(clients) lock takes per change, under the load the lane is for. Now each surface id has its own route generation (a fixed array of 1024 slots, id % 1024: bounded memory, nothing to remove, lock-free reads). A RouteTable write moves only the ids whose route it changes (RouteKey): a surfaces or terminal_catalog_by_runtime write its key, a terminal_catalog write the runtime ids of the owner it removes and the owner it puts in. terminal_catalog_by_runtime is a RouteTable now too, so every table an id routes through moves that id. Two ids in one slot only make a key look its terminal up once more, never use a stale one. The respawn, rollback and mutation tests of #31940 keep their meaning. * test(cmux-tui): a workspace size default keeps the key off the session lock (TYPING FIX 2, #31940 follow-up B) Each key notes its view's activity in the sizing engine. That mutation re-expanded the host policy, which asks whether the terminal follows a workspace default (workspace_default_of), which looks the terminal's workspace up under the session lock, and may walk every surface for its placements, as soon as any workspace has a size default. So with a workspace default every key took the session lock again. Red at this commit: with the settings owner's host policy and a default on the typing terminal's workspace, the second key takes the lock once. * fix(cmux-tui): a key does not re-expand the host size policy (TYPING FIX 2, #31940 follow-up B) Every sizing mutation re-expanded the host policy for its terminal, because participants may have come, gone or changed identity. A key's activity note changes none of them, and the expansion depends on nothing else a key changes. But the refresh first asks whether the terminal follows a workspace default (workspace_default_of), which looks the terminal's workspace up under the session lock and may walk every surface for its placements; with any workspace size default set, every key took the session lock again, behind automation. Activity now goes through mutate_terminal_sizing_keeping_participants, which is mutate_terminal_sizing without that refresh. Every mutation that adds, removes or renames a participant still refreshes it, and a workspace default, terminal override or host policy change still re-applies to the terminals it covers.
…no session lock (#32256) * test(cmux-tui): a tab created or closed elsewhere keeps the key off the session lock (TYPING FIX 2, #31940 follow-up A) The terminal kept for the person's keys (#31940) is checked against one process-wide route generation that every routing write moves. A tab create or close in any workspace (an agent's tab storm) therefore sent every attached client's next key back to the session lock: O(clients) lock takes per routing change, during the very load the lane exists for. The test counts the session lock acquisitions of the key's turn on its own thread (a test-only counter in StateMutex), so it needs no timing: after a new workspace, a tab in it and that tab's close, the next key must take the lock 0 times. Red at this commit: it takes it once. * fix(cmux-tui): a kept terminal is dropped only when its own route changes (TYPING FIX 2, #31940 follow-up A) #31940 checked the terminal kept for the person's keys against one process-wide route generation that every routing write moved. Every tab create or close anywhere (an agent's tab storm, a CLI new-workspace) therefore sent every attached client's next key back to the session lock: O(clients) lock takes per change, under the load the lane is for. Now each surface id has its own route generation (a fixed array of 1024 slots, id % 1024: bounded memory, nothing to remove, lock-free reads). A RouteTable write moves only the ids whose route it changes (RouteKey): a surfaces or terminal_catalog_by_runtime write its key, a terminal_catalog write the runtime ids of the owner it removes and the owner it puts in. terminal_catalog_by_runtime is a RouteTable now too, so every table an id routes through moves that id. Two ids in one slot only make a key look its terminal up once more, never use a stale one. The respawn, rollback and mutation tests of #31940 keep their meaning. * test(cmux-tui): a workspace size default keeps the key off the session lock (TYPING FIX 2, #31940 follow-up B) Each key notes its view's activity in the sizing engine. That mutation re-expanded the host policy, which asks whether the terminal follows a workspace default (workspace_default_of), which looks the terminal's workspace up under the session lock, and may walk every surface for its placements, as soon as any workspace has a size default. So with a workspace default every key took the session lock again. Red at this commit: with the settings owner's host policy and a default on the typing terminal's workspace, the second key takes the lock once. * fix(cmux-tui): a key does not re-expand the host size policy (TYPING FIX 2, #31940 follow-up B) Every sizing mutation re-expanded the host policy for its terminal, because participants may have come, gone or changed identity. A key's activity note changes none of them, and the expansion depends on nothing else a key changes. But the refresh first asks whether the terminal follows a workspace default (workspace_default_of), which looks the terminal's workspace up under the session lock and may walk every surface for its placements; with any workspace size default set, every key took the session lock again, behind automation. Activity now goes through mutate_terminal_sizing_keeping_participants, which is mutate_terminal_sizing without that refresh. Every mutation that adds, removes or renames a participant still refreshes it, and a workspace default, terminal override or host policy change still re-applies to the terminals it covers. * test(cmux-tui): a named key reaches its terminal without the session lock (TYPING FIX 2, #31940 follow-up C) send-key (Return, Ctrl-C and other named keys from an attached view) still looked its terminal up through the session lock on every call, while send used the kept terminal of #31940. Red at this commit: the second named key takes the lock once. The test also checks the route rule: after a respawn the next named key reaches the new shell. * fix(cmux-tui): send-key uses the kept terminal (TYPING FIX 2, #31940 follow-up C) send-key looked its terminal up through the session lock (get_surface) on every call, so a named key from the app (Return, Ctrl-C, arrows sent as send-key) still waited behind automation's lists, snapshots and durable mutations. It now goes through input_surface, as send does: an attached client's kept terminal, valid only while its id's route generation is the one it was kept at (respawn, close, move and the other route changes make the next key look it up again); any other sender looks it up.
…ys run at user-interactive QoS (#32317) * test(cmux-tui): a tab created or closed elsewhere keeps the key off the session lock (TYPING FIX 2, #31940 follow-up A) The terminal kept for the person's keys (#31940) is checked against one process-wide route generation that every routing write moves. A tab create or close in any workspace (an agent's tab storm) therefore sent every attached client's next key back to the session lock: O(clients) lock takes per routing change, during the very load the lane exists for. The test counts the session lock acquisitions of the key's turn on its own thread (a test-only counter in StateMutex), so it needs no timing: after a new workspace, a tab in it and that tab's close, the next key must take the lock 0 times. Red at this commit: it takes it once. * fix(cmux-tui): a kept terminal is dropped only when its own route changes (TYPING FIX 2, #31940 follow-up A) #31940 checked the terminal kept for the person's keys against one process-wide route generation that every routing write moved. Every tab create or close anywhere (an agent's tab storm, a CLI new-workspace) therefore sent every attached client's next key back to the session lock: O(clients) lock takes per change, under the load the lane is for. Now each surface id has its own route generation (a fixed array of 1024 slots, id % 1024: bounded memory, nothing to remove, lock-free reads). A RouteTable write moves only the ids whose route it changes (RouteKey): a surfaces or terminal_catalog_by_runtime write its key, a terminal_catalog write the runtime ids of the owner it removes and the owner it puts in. terminal_catalog_by_runtime is a RouteTable now too, so every table an id routes through moves that id. Two ids in one slot only make a key look its terminal up once more, never use a stale one. The respawn, rollback and mutation tests of #31940 keep their meaning. * test(cmux-tui): a workspace size default keeps the key off the session lock (TYPING FIX 2, #31940 follow-up B) Each key notes its view's activity in the sizing engine. That mutation re-expanded the host policy, which asks whether the terminal follows a workspace default (workspace_default_of), which looks the terminal's workspace up under the session lock, and may walk every surface for its placements, as soon as any workspace has a size default. So with a workspace default every key took the session lock again. Red at this commit: with the settings owner's host policy and a default on the typing terminal's workspace, the second key takes the lock once. * fix(cmux-tui): a key does not re-expand the host size policy (TYPING FIX 2, #31940 follow-up B) Every sizing mutation re-expanded the host policy for its terminal, because participants may have come, gone or changed identity. A key's activity note changes none of them, and the expansion depends on nothing else a key changes. But the refresh first asks whether the terminal follows a workspace default (workspace_default_of), which looks the terminal's workspace up under the session lock and may walk every surface for its placements; with any workspace size default set, every key took the session lock again, behind automation. Activity now goes through mutate_terminal_sizing_keeping_participants, which is mutate_terminal_sizing without that refresh. Every mutation that adds, removes or renames a participant still refreshes it, and a workspace default, terminal override or host policy change still re-applies to the terminals it covers. * test(cmux-tui): a named key reaches its terminal without the session lock (TYPING FIX 2, #31940 follow-up C) send-key (Return, Ctrl-C and other named keys from an attached view) still looked its terminal up through the session lock on every call, while send used the kept terminal of #31940. Red at this commit: the second named key takes the lock once. The test also checks the route rule: after a respawn the next named key reaches the new shell. * fix(cmux-tui): send-key uses the kept terminal (TYPING FIX 2, #31940 follow-up C) send-key looked its terminal up through the session lock (get_surface) on every call, so a named key from the app (Return, Ctrl-C, arrows sent as send-key) still waited behind automation's lists, snapshots and durable mutations. It now goes through input_surface, as send does: an attached client's kept terminal, valid only while its id's route generation is the one it was kept at (respawn, close, move and the other route changes make the next key look it up again); any other sender looks it up. * test(cmux-tui): a connection that carries the person's keys runs at user-interactive QoS (TYPING FIX 2, F7) Under full CPU a default-QoS thread waits for a CPU. The fleet probe (STATUS 6593: 14-core macOS, 28 busy default-QoS threads) measured a default-QoS key thread's wake at p99 up to 7.7 ms, max up to 24.8 ms, and one 13.2 ms wait in the middle of a send; at user-interactive the max was about half and no send was preempted. A key crosses three threads of its connection (mux-conn reader, mux-control-dispatch, mux-line-out writer), all at the default QoS. A test-only hook makes each of those threads record its own QoS before each line, request or reply. The test attaches a terminal over a real socket and sends two keys: after the second reply all three threads must be at QOS_CLASS_USER_INTERACTIVE. A second test: a CLI connection that writes to a terminal it does not view keeps the default. Red at this commit: the first test fails (the threads stay at the default). * fix(cmux-tui): the threads that carry the person's keys run at user-interactive QoS (TYPING FIX 2, F7) The daemon set no thread QoS, so under full CPU (the L6 storms keep every core busy) a key waited for a CPU at each of its connection's three threads: the fleet probe measured a default-QoS key thread's wake at p99 up to 7.7 ms and max up to 24.8 ms, plus a 13.2 ms preemption in the middle of a send (STATUS 6593). Now a key from a client attached to its terminal (the input lane of #31940, send and send-key) marks its connection as one that carries the person's input (MessageWriter::input_lane). The thread that runs the key raises itself at once; the connection's mux-conn reader and mux-line-out writer raise themselves on their next line or reply. One atomic load per line, one system call per thread. Automation's connections (CLI, MCP, agents writing to terminals they do not view) keep the default QoS, so they never compete with the person's input. This follows Chrome, which runs the thread that carries input at the highest priority. threading-model.md records the new QoS. * fix(cmux-tui): input QoS clippy (no super:: in line_connection; the QoS read hook is macOS-only) * test(cmux-tui): input QoS test clippy on macOS (no needless qualification) --------- Co-authored-by: cmux-bot[bot] <348531757+cmux-bot[bot]@users.noreply.github.com>
…t the person's priority (#32431) * test(cmux-tui): a tab created or closed elsewhere keeps the key off the session lock (TYPING FIX 2, #31940 follow-up A) The terminal kept for the person's keys (#31940) is checked against one process-wide route generation that every routing write moves. A tab create or close in any workspace (an agent's tab storm) therefore sent every attached client's next key back to the session lock: O(clients) lock takes per routing change, during the very load the lane exists for. The test counts the session lock acquisitions of the key's turn on its own thread (a test-only counter in StateMutex), so it needs no timing: after a new workspace, a tab in it and that tab's close, the next key must take the lock 0 times. Red at this commit: it takes it once. * fix(cmux-tui): a kept terminal is dropped only when its own route changes (TYPING FIX 2, #31940 follow-up A) #31940 checked the terminal kept for the person's keys against one process-wide route generation that every routing write moved. Every tab create or close anywhere (an agent's tab storm, a CLI new-workspace) therefore sent every attached client's next key back to the session lock: O(clients) lock takes per change, under the load the lane is for. Now each surface id has its own route generation (a fixed array of 1024 slots, id % 1024: bounded memory, nothing to remove, lock-free reads). A RouteTable write moves only the ids whose route it changes (RouteKey): a surfaces or terminal_catalog_by_runtime write its key, a terminal_catalog write the runtime ids of the owner it removes and the owner it puts in. terminal_catalog_by_runtime is a RouteTable now too, so every table an id routes through moves that id. Two ids in one slot only make a key look its terminal up once more, never use a stale one. The respawn, rollback and mutation tests of #31940 keep their meaning. * test(cmux-tui): a workspace size default keeps the key off the session lock (TYPING FIX 2, #31940 follow-up B) Each key notes its view's activity in the sizing engine. That mutation re-expanded the host policy, which asks whether the terminal follows a workspace default (workspace_default_of), which looks the terminal's workspace up under the session lock, and may walk every surface for its placements, as soon as any workspace has a size default. So with a workspace default every key took the session lock again. Red at this commit: with the settings owner's host policy and a default on the typing terminal's workspace, the second key takes the lock once. * fix(cmux-tui): a key does not re-expand the host size policy (TYPING FIX 2, #31940 follow-up B) Every sizing mutation re-expanded the host policy for its terminal, because participants may have come, gone or changed identity. A key's activity note changes none of them, and the expansion depends on nothing else a key changes. But the refresh first asks whether the terminal follows a workspace default (workspace_default_of), which looks the terminal's workspace up under the session lock and may walk every surface for its placements; with any workspace size default set, every key took the session lock again, behind automation. Activity now goes through mutate_terminal_sizing_keeping_participants, which is mutate_terminal_sizing without that refresh. Every mutation that adds, removes or renames a participant still refreshes it, and a workspace default, terminal override or host policy change still re-applies to the terminals it covers. * test(cmux-tui): a named key reaches its terminal without the session lock (TYPING FIX 2, #31940 follow-up C) send-key (Return, Ctrl-C and other named keys from an attached view) still looked its terminal up through the session lock on every call, while send used the kept terminal of #31940. Red at this commit: the second named key takes the lock once. The test also checks the route rule: after a respawn the next named key reaches the new shell. * fix(cmux-tui): send-key uses the kept terminal (TYPING FIX 2, #31940 follow-up C) send-key looked its terminal up through the session lock (get_surface) on every call, so a named key from the app (Return, Ctrl-C, arrows sent as send-key) still waited behind automation's lists, snapshots and durable mutations. It now goes through input_surface, as send does: an attached client's kept terminal, valid only while its id's route generation is the one it was kept at (respawn, close, move and the other route changes make the next key look it up again); any other sender looks it up. * test(cmux-tui): a connection that carries the person's keys runs at user-interactive QoS (TYPING FIX 2, F7) Under full CPU a default-QoS thread waits for a CPU. The fleet probe (STATUS 6593: 14-core macOS, 28 busy default-QoS threads) measured a default-QoS key thread's wake at p99 up to 7.7 ms, max up to 24.8 ms, and one 13.2 ms wait in the middle of a send; at user-interactive the max was about half and no send was preempted. A key crosses three threads of its connection (mux-conn reader, mux-control-dispatch, mux-line-out writer), all at the default QoS. A test-only hook makes each of those threads record its own QoS before each line, request or reply. The test attaches a terminal over a real socket and sends two keys: after the second reply all three threads must be at QOS_CLASS_USER_INTERACTIVE. A second test: a CLI connection that writes to a terminal it does not view keeps the default. Red at this commit: the first test fails (the threads stay at the default). * fix(cmux-tui): the threads that carry the person's keys run at user-interactive QoS (TYPING FIX 2, F7) The daemon set no thread QoS, so under full CPU (the L6 storms keep every core busy) a key waited for a CPU at each of its connection's three threads: the fleet probe measured a default-QoS key thread's wake at p99 up to 7.7 ms and max up to 24.8 ms, plus a 13.2 ms preemption in the middle of a send (STATUS 6593). Now a key from a client attached to its terminal (the input lane of #31940, send and send-key) marks its connection as one that carries the person's input (MessageWriter::input_lane). The thread that runs the key raises itself at once; the connection's mux-conn reader and mux-line-out writer raise themselves on their next line or reply. One atomic load per line, one system call per thread. Automation's connections (CLI, MCP, agents writing to terminals they do not view) keep the default QoS, so they never compete with the person's input. This follows Chrome, which runs the thread that carries input at the highest priority. threading-model.md records the new QoS. * fix(cmux-tui): input QoS clippy (no super:: in line_connection; the QoS read hook is macOS-only) * test(cmux-tui): input QoS test clippy on macOS (no needless qualification) * test(cmux-tui): a read-only viewer's keys do not get the person's priority (TYPING FIX 2, F7 follow-up) F7 (#32317) marks a connection as the person's input (user-interactive QoS on its threads) when a client attached to the terminal sends a key. A client attached only through read-only views (terminal.attach read_only: a watcher, an agent observing a terminal, a mirror) that sends through the legacy send or send-key is automation, yet it was marked too, so it could compete with the person's keys. Red at this commit: after a read-only viewer's send and send-key, its connection is marked. The typing client's connection must still be marked (checked in the same test). * fix(cmux-tui): only a client that can type gets the person's QoS (TYPING FIX 2, F7 follow-up) A key now marks its connection as the person's input (user-interactive QoS, #32317) only when the client can type into the terminal: AttachedSurface::can_type is true for a legacy attach with no lease and for any view that is not read-only; a client attached only through read-only views (terminal.attach read_only: a watcher, an agent observing a terminal, a mirror) is automation and keeps the default QoS, so it never competes with the person's keys. The read-only viewer's keys still use the kept terminal (no session lock); only the priority changes. The answer comes from the same registry read as the kept terminal, so a key takes no extra lock. threading-model.md: the daemon row and the QoS paragraph say what F7 does now; the stale target 'request threads = userInitiated' is replaced (the person's input threads are user-interactive, automation's request threads stay at the default, never above the person's).
…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.
… 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).
…R-DAEMON-STATE S1 step 1, behind CMUX_TUI_RDS_INPUT) (#34014) * 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). --------- Co-authored-by: cmux-bot[bot] <348531757+cmux-bot[bot]@users.noreply.github.com>
…s on the input lane (R-DAEMON-STATE S1 step 3) (#34146) * 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). --------- Co-authored-by: cmux-bot[bot] <348531757+cmux-bot[bot]@users.noreply.github.com>
…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>
… is creating; the lock becomes a leaf (R-DAEMON-STATE S1 step 4) (#34174) * 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. * 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. * 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>
…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>
…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>
…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>
…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>
… 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>
…_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…
Summary
P0 TYPING UNDER LOAD, finding F4. In the filmed L6 round (all loads at once), each key's delay splits into two parts. The app part (NSEvent to the socket submit to the daemon) was p50 3.8 ms, p95 9.2 ms, max 34.5 ms. The daemon part (socket submit to the byte arriving at the PTY) was p50 11.9 ms, p95 53.9 ms, p99 103.3 ms, max 475 ms. So the 250-400 ms freezes on the film are the key waiting in the daemon before its PTY write. Data: run feat-51b2e1d-probe-tul-20261102T053502Z-cmux20, probe.csv plus echo.jsonl (STATUS 5299).
Cause: a key's
sendlooked its terminal up throughMux::surface, which takes the global session lock (Mux.state), before the PTY write. Its sizing note (note_terminal_input) looked it up again after the write, which delays the next key on the same connection. Automation holds that lock for every list, snapshot and durable mutation:tab.list,workspace.list,session.snapshot) build the whole public snapshot under it.lock_state_pinned).So the person's typing waited behind agents, the CLI and MCP.
Fix: the person's keys never take the session lock, and a kept terminal is used only while its routing is current.
AttachedSurface::input,ClientRegistry::attached_input_surface/remember_attached_input). The handle goes with the record at the last detach and at disconnect, and a dead terminal is looked up again.note_terminal_inputtakes the terminal the bytes were written to.terminal write, keep the lookup.State::surfacesandState::terminal_catalogareRouteTables (cmux-tui-core/src/route_table.rs): reads deref to the map, and every insert or remove moves a route generation, so the compiler finds every routing write. A kept terminal carries the generation read before its lookup; a respawn, close, move, projection or rollback makes the next key look it up again.This follows Chrome's model: the person's input goes before automation, on its own path.
Testing
Regression tests (cmux-tui-core
server::tests::input_lane), commandscripts/ci/cmux-tui-rust-check.sh test input_lane(macOS, fleet):a_key_reaches_its_terminal_while_automation_holds_the_session_lock: another thread holds Mux.state for 300 ms; a key from an attached client must take under 150 ms. RED on the base (55564d1, step 4531ad570dba6876c215b0bd): the key waited 310 ms.after_a_respawn_the_next_key_reaches_the_new_terminal(lead order: the kept terminal must never outlive its routing): a new runtime takes the placement id while the old one still lives; the next key must reach the new shell. RED on the first kept-terminal cache (branch fix/p0-typing-f4-respawn-red b8891a1, step 81b3ea940914c7d231014290): "the key after the respawn did not reach the new shell".a_mutation_that_routes_nothing_keeps_the_key_off_the_session_lock(review): a pane rename and a dropped state clone must not send the next key back to the lock. RED at e306e35 (a Drop that moved the generation on every clone, step 6c9c6954e23ef09e4c6c3752).GREEN at the head:
input_lane4/4 (step 16e1b7bc0a472cf31844ed80); macOSserver::1454 tests at 8bd3d0a (step 3ed23f3eb02487c33c689a4e): all pass exceptconnection_handler_owns_every_router_connection_operation, the base red fixed by #31939. clippy -D warnings (macOS): passes at 84470f9 for this PR's code; at the head it fails only incmux-tui/src/cli/app/tests.rsandcmux-tui/src/link/mod.rs, files from the feat merge (#28950), not this PR.Full macOS test run at 8bd3d0a (step 902245a674203ade9f94de40) against the same step on the merge base 219d9bd (step 63a5b91299b92b6ab12d9bf9): 30 and 26 failures. 20 test names failed only in the head run. Each one passes when run alone at the head 15c133d (step, filter, result):
a_chat_carries_its_live_sessions_attention_and_latest_replya_chat_in_a_store_no_profile_uses_adopts_with_that_store,an_opencode_chat_opens_by_adopting_it_over_acp,chat_open_then_session_new_adopts_from_the_profile_storeadopt): 171/171 passwith_discovery_off_only_the_listed_roots_are_readstalled_turn::an_adopted_turn_whose_model_went_quiet_ends_as_stalled_and_frees_its_agentstalled_turn): 9/9 passsession_git_state::git_state_of_a_held_chat_follows_a_checkout_mid_sessionrouter_exits_when_its_daemon_is_killedcaller::supervised::tests::supervised_link_accepts_only_the_live_child_and_enables_its_stamp_policy,..._rejects_a_matching_pid_with_a_different_start_instancesupervised): 7/7 passapp::tests::pty_mouse::pty_mouse_uses_the_canonical_rendered_grid_during_resize_marginslink::e2e_tests::(4 tests)link::host_inbound::native_token_tests::native_token_receiver_daemon_stamps_backend_identity_and_signed_timeshell_integration::incognito_history_tests::(2 tests)surface::tests::child_process::default_shell_prompt_survives_rapid_resizes_after_a_partial_linesurface::tests::lifecycle::macos_surface_spawn_returns_within_deadlineNone of them is in the changed code. The head full run took 3791 s and the base run 1708 s on a loaded fleet, and that run reported 13 leaked processes; these are timing and load failures of the full run. An order dependence cannot be fully ruled out by single runs; no failing test touches the input lane.
Observed / Not run
QOS_CLASS_USER_INTERACTIVE.Changelog
Fixed: Typing in a terminal no longer freezes while agents, the CLI or MCP list, read or change the session
Proof
No capture: daemon-only change, no visual change.
Follow-ups (review, non-blocking): the generation is global, so a tab create or close anywhere sends every client's next key through the lock once (a per-id check can follow);
refresh_host_policy_locked->workspace_default_ofstill takes Mux.state per key when a workspace size default is set;send_keystill looks up through Mux.state. F7 (daemon thread QoS) is a separate finding.