R-INPUT S2b (FFI step A): the app's one input handle cmux_input_* over the input router, with no copy per focus event
REWRITE R-INPUT stage S2, step A of the FFI two-step (DESIGN.md section 5, /Users/lawrence/fun/wt/handoff/rewrites/R-INPUT/DESIGN.md): one input handle for the app, cmux_input_*, over cmux-focus-core's input router, which owns every window's focus (WindowFocus, S2a #34929). Rust only: no Swift caller yet. Step B (after the app-ffi re-pin) moves the Swift FocusMachine onto it, one window id per call; the per-window cmux_focus_* handle is deleted in that stage's cleanup.
What changes:
- One reply writer for both handles. The window focus handle's reply buffers move to
focus/reply.rs(FocusReply) and its wire decoding tofocus/codec.rs.focus.rskeeps thecmux_focus_*ABI unchanged. So a second handle can answer with the same bytes. input.rs: the app's input handle.Inputholds the router and one boxedFocusReplyper open window (the windows' map moves its entries when it grows; a reply the app holds for a window stays where it is until the next call for that window: one allocation per window open, none per event).cmux_input_window_open/close, andcmux_input_focus_*= eachcmux_focus_*call with a window id (send, set_topology, current, set_close_focus, recent_panes, remembered, export/import, the Close Tab aim calls). The C header declares them; their press arguments are the same two bytes ascmux_focus_*'s.- No copy on the way through the router (second commit).
InputRouter::send_focustakes a focus event by reference;set_focus_topologytakes a topology by value and callsWindowFocus::set_topology(no clone). An equal topology does not run the reducer, so the router rebuilds no tab index for it. A person's move out of a content pane keeps the pane in a buffer the window owns, not a copy of the resolved target per event. The handle writes each reply from the router's own effects slice (InputRouter::effects,FocusReply::finishtakes an iterator), not a copy of the effects. A topology report has no event, sosession_after_focustakes anOption. Every topology report still judges the sinks that ended (track_gone), equal or not; only the index rebuild is skipped for an equal one (the router fuzz caught my first version, which skipped both: seed 217, a closed terminal that no window showed stayed gone after an equal report, so a later key for a new terminal with its id was dropped).
Tests
cmux-layout-reducer-ffi/tests/input_handle.rs (new):
the_input_handle_replies_to_every_corpus_step_as_the_window_handle: the recorded focus corpus (tests/focus/corpus.json, the one the focus rules are checked against) replays through the window handle and the input handle. Every step's reply must match: all fields, the decoded strings, the effects, the overlays, and the raw string bytes. Then the focus history, the window record, the journal export and the Close Tab aim must match. It also counts this thread's allocations: over the corpus the input handle makes no more allocations per focus event than the window handle (a small warm-up per sequence), and per topology report at most the router's tab index on top.an_equal_topology_report_adds_no_allocation_in_the_router: an equal report of a corpus layout with at least 4 tabs; the input handle makes no more allocations than the window handle (it fails if the router rebuilds its tab index for an equal report). Both handles clone the resolved target once per topology report inWindowFocus::set_topology: a shared cost since S2a, not this PR's.a_windows_reply_does_not_move_while_other_windows_open_and_close(review of this PR): window A's reply and string-list pointers stay the same while 40 other windows open and 8 close.the_c_abi_refuses_bad_input_with_its_documented_values(review of this PR): everycmux_input_*edge through the C ABI: a null handle, null pointers, bad UTF-8, a window that is not open (and no call opens it), an event that does not decode (ok0), a checkpoint that does not decode (ERR_INVALID, the state kept).a_window_that_is_not_open_has_no_reply_and_another_windows_focus_is_its_own.
The FFI crate is its own Cargo workspace, so the fleet's cmux-tui-rust-check.sh does not build it. The proof refs add it to the cmux-tui workspace (a never-merge commit, as for S2a):
- Red,
fix/r-input-s2b-proof-red2-nomerge100f98253bde (the first commit c667ee47683, which still copies, plus the proof commits),cmux-tui-rust-check.sh test input_handleon Linux, step 965caba8b86cb2e9ea6567a5: every corpus step's reply matched (2,468 focus events and the topology reports of every sequence), then the allocation check failed: "the input handle made 24441 allocations over 2468 focus events, the window handle 19009" (about 2.2 more per event: the copy of the event, of each effect and of the resolved target). - Green,
fix/r-input-s2b-proof-green4-nomerge923bc717e508 (this head a200b26051d plus the proof commits),cmux-tui-rust-check.sh allon Linux, step 23318abadb2afcdd4249134d: fmt, clippy -D warnings and doc tests pass; every test ofcmux-focus-core(with the router fuzz) and ofcmux-layout-reducer-ffi(withinput_handle) passes. The workspace tests step is red only in other crates: 29 failures are exactly the base list of dd03fbf83bb4, and 3 are not in it (below).
Tests outside the two changed crates: no crate in cmux-tui/Cargo.lock depends on cmux-focus-core (the FFI crate is its own workspace), so this PR cannot change them.
- 29 failures of the green step are exactly the base list of the base-red owner's whole-suite run dd03fbf83bb4 at b657cec72db (acpmux, cmux-agent-pane-policy, cmux-tui).
- Failed in a loaded full-suite run, not reachable from this change (each passed in the base run dd03fbf83bb4; the lead gives them to the base-red owner). Each passes alone on this head:
cmux-tui-core mux::layout_invariants::tests::layout_ops_conserve_tabs(failed in 23318abadb2afcdd4249134d: "scoped projection differs from the full projection"; alone on this head: step 5ee410f973b271b96b0b3200, passed: it fails only in the loaded full-suite run).cmux-tui-core server::tests::outbound_and_scheduler::wait_for_off_dispatch_refuses_over_capacity_without_queueing(failed in 23318abadb2afcdd4249134d: reply order; it also failed in the full-suite run 0b8375bbfca4 of another owner and passed in its base; alone on this head: step 534559f2a90f4996e6f622dc, passed: it fails only in loaded full-suite runs).cmux-tui-core::activity v2_terminal_input_counts_only_from_a_persons_attached_client(failed in 23318abadb2afcdd4249134d: EAGAIN; alone on this head: step ce342ce97b965b19d4ab951b, passed: it fails only in the loaded full-suite run).acpmux::session_control_peer_cli lifecycle_commands_use_peer_stable_ids_and_refuse_ambiguous_names(failed in 80cacd1a0d34b969fc69e4f5, passed in 23318abadb2afcdd4249134d; alone at the merge base 439ed237ac31: step 4f44d4a6435f7e0a0c4c8732, passed; alone on this head: step 8fbed11e7354c9877e465980, passed: it fails only in the full-suite run and passes alone on the base and on this head).cmux-tui coderouter_usage::tests::poller_publishes_readout_then_clears_it_when_the_endpoint_disappears(failed in 80cacd1a0d34b969fc69e4f5, passed in 23318abadb2afcdd4249134d; alone at the merge base: step 1e5f0ac2eb13cf588a481509, passed; alone on this head: step 526ada2a4a3726e403f4ad14, passed: it fails only in the full-suite run and passes alone on the base and on this head).
Review round 1 (FIX at a200b26051d: a window's reply was kept inline in the windows' HashMap, so opening more windows moved it and the app's pointer for another window read freed memory, against the header). Red test 1dd26d04384; fix 79eeff1c877 (out: Box<FocusReply>; the header also names ERR_INVALID for the import call and ERR_NULL for a null handle). Red step 0bc5ba4214b37456737801a1 on fix/r-input-s2b-proof-red5-nomerge 683bd0dc9a6c: the reply test fails on its assertion ("the window's reply moved": 0x7f2ae4002548 at the open, 0x7f2ae4007e28 after the other windows). Green step 8bd5e053ead82e13329ff2b4 on fix/r-input-s2b-proof-green5-nomerge 8eecefe9e1fc (this head + the proof commits), Linux all: fmt, clippy -D warnings and doc tests pass; every cmux-focus-core test (with the router fuzz) and every cmux-layout-reducer-ffi test (with the five input_handle tests) passes. Workspace tests are red only in other crates: 28 of the dd03fbf83bb4 base list, wait_for_off_dispatch_refuses_over_capacity_without_queueing again, and three that passed in dd03fbf83bb4 and fail in this loaded run: acpmux::daemon_idle acpmux_daemon_idle_has_no_periodic_wakeups, acpmux::send_attach_cli send_attach_sends_files_of_any_size_and_number_by_reference, cmux-tui-core server::tests::identify_and_protocol::title_changed_event_includes_authoritative_surface_title (none reachable from this change; alone on this head: idle step 2d2b65f90927b3d43a080483 passed, attach step 7e4fb028b99da10bf756949b passed, title step 5fcd4fb3eba36b019081d15e passed). For S3: import_focus does not clear the router's effects first (no reader of them follows an import today).
Sanitizer (lead): no ASan, miri or valgrind path exists for a Rust FFI crate on the fleet. cmux-tui-rust-check.sh has no sanitizer mode, and the controller's step environment allowlist has no RUSTFLAGS (hq build-fleet/README.md: the only sanitizer toggle is CMUX_SWIFT_SANITIZE for Swift steps). The repo's one valgrind job (cmux-tui.yml valgrind-leak-check-shard) runs only in CI full mode, on the cmux-tui and terminal startup tests. So no sanitizer ran here. The reply test does not depend on luck: it compares addresses and never reads freed memory. On the unboxed head, the map's growth must move window A's entry, because the new table is allocated while the old one is still live (the red step shows it). The ABI edge test reads each reply before the next call only. The lead accepted this plan (no new sanitizer mode now): the app's own reads of a reply get a sanitizer in step B: the Swift FocusMachine on cmux_input_* (after the re-pin) runs its focus and creation-hold suites with CMUX_SWIFT_SANITIZE=address, whose malloc and free interceptors also cover the Rust library's heap.
Review: SHIP at 79eeff1c877 (https://github.com/manaflow-ai/cmux/pull/35641#issuecomment-6638261510). Its two optional points (the C ABI test does not call every export, and the bad-checkpoint case does not compare export_json before and after) go into step B's PR, which calls these exports from Swift.
CI: the one red check is "Check the app FFI pin" in cmux-next checks: the FFI two-step's step A changes the FFI sources, so the pin's source sha no longer matches until the release bot publishes cmux-app-ffi-<merge sha> and its re-pin PR lands (the same red as S2a #34929).
Observed / Not run
- Observed: the fleet steps above.
- Not run: any sanitizer on this crate (none exists on the fleet for it, above).
- Not run: a Swift caller (step B) and its KeyRouterBench A/B; the router fuzz at 1,000,000 cases runs on the merged commit as the stage's test phase (DESIGN.md section 6).
Changelog
none
No capture: no visible change (Rust only, no caller).