Skip to content

fix(cmux-tui-core): a regression test for the poisoned arm of the creating-input check; it loads the routes before its poison check (#34174 review follow-ups) - #35127

Merged
lawrencecchen merged 4 commits into
feat-cmux-nextfrom
fix/rds-s14-followups
Nov 4, 2026
Merged

lawrencecchen merged 4 commits into
feat-cmux-nextfrom
fix/rds-s14-followups

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Follow-ups from the last review of #34174 (R-DAEMON-STATE S1.4, merged as eaa00d9; review comment "Review (claude-reviewer-rds-s14): SHIP at 5aa3ec4", non-blocking notes 1-4). Behind the input lane switch CMUX_TUI_RDS_INPUT (default off on feat-cmux-next).

  • A regression test for the poisoned arm (note 3). 3b2e29f made the creating-input live check take the old read on a poisoned session state, with no test. New test mux::tests::signaled_mutex::a_poisoned_session_state_answers_the_creating_input_checks_from_the_state: with the lane on, a create's commit puts its terminal in the state and then panics. The cut-short guard publishes no routes, so knows_terminal_input_target must answer from the state.
  • Load order (note 1). live_surface_for_terminal now loads the published routes before its poison check, as Mux::surface does (poison never clears, so routes it uses come from a change that finished before any panic). Its old read still recovers the poison. The reviewer found no wrong answer with the old order; this makes the two readers follow one rule, and the comment says why the old read differs.
  • Comments (notes 2 and 4). The creating-input lock is a leaf only while the state is not poisoned: on a poisoned state its holders take the session lock under it, so no code may take it while it holds the session lock (doc of live_surface_for_terminal, and DESIGN S1.4). begin_creating_input: on both arms the live read ends before the insert, so a commit can make the id live in between; that entry is then written to the live terminal in order.

Testing

Focused command: ./scripts/verify-cmux-tui-hosted.sh --filter a_poisoned_session_state_answers_the_creating_input_checks_from_the_state (Linux test (linux)).

Compile: fleet job 97d92dbabbb6c17adeb0633a at 71f21cd: CmuxNext, app and cmux-tui passed.

Observed / Not run

Changelog

none

Proof

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

…put checks from the state, not the published routes (#34174 review follow-up)

The poison check of 3b2e29f had no test. Lane on: a create's commit puts its terminal in the state and panics;
the cut-short guard publishes no routes, so knows_terminal_input_target must answer from the state.
…fore its poison check, as Mux::surface does; the comments state the leaf rule and the insert race (#34174 review follow-ups)

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

Test: mux::tests::signaled_mutex::a_poisoned_session_state_answers_the_creating_input_checks_from_the_state.
@lawrencecchen lawrencecchen added the risk:normal Risk tier (docs/ci/risk-tiers.md) label Nov 4, 2026
@cursor

cursor Bot commented Nov 4, 2026

Copy link
Copy Markdown

Bugbot needs on-demand usage enabled

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

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

@github-actions

github-actions Bot commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

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

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

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

gh workflow run reload-build.yml -R manaflow-ai/cmux --ref feat-cmux-next -f tag=pr35127 -f ref=fix/rds-s14-followups -f platform=macos -f backend_mode=local

red: cmux-next checks (god files, concurrency, crash safety, l10n); no finished base run on 3a5484f to compare; do not merge until green; see lane-rules

Shipped in:

@blacksmith-sh

blacksmith-sh Bot commented Nov 4, 2026 •

Copy link
Copy Markdown

Found 2 test failures on Blacksmith runners:

Failures

Test View Logs
nightly cmux-tui resolve/nightly cmux-tui resolve View Logs
the short nightly decide gate must use the pooled Linux route with a Blacksmith fallbac
k/
the short nightly decide gate must use the pooled Linux route with a Blacksmith fallbac
k
View Logs

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

@lawrencecchen

Copy link
Copy Markdown
Contributor Author

Review (claude-reviewer-rds-s14): FIX at 71f21cd
reviewer-session: 28d328fb-296f-470d-8b3b-6a14b496a6b4
author-session: ae25c6d154f94c01a
R9: pass | layer-ownership.md row "Terminals: PTY, process, output, cwd, title, exit, grid size" (Rust, session host). The PR adds no writer. It reorders one read in live_surface_for_terminal, changes comments and adds a test. The route tables and the catalog host index keep their only writers, State::insert_catalog_terminal and remove_catalog_terminal, under the session state guard. The creating-input map keeps its only writers, CreatingInputs::insert and remove, under its own lock.
perf claim: none

One defect must be fixed before the merge. The product change and the test are correct. I traced both in the head checkout. I did not build or run anything locally.

Defect

  1. The new test fails clippy with -D warnings: unused_qualifications at cmux-tui/crates/cmux-tui-core/src/mux/tests/signaled_mutex.rs:137, :138, :141 and :143.
    • use super::*; (:3) already brings these into scope: crate::terminal_host_runtime::TerminalHostIdentity (:137), crate::terminal_host::TerminalId (:138), crate::Surface (:141) and crate::SurfaceOptions (:143). cargo clippy -p cmux-tui-core --all-targets -- -D warnings stops with 4 "unnecessary qualification" errors.
    • This red comes from the PR. It fails these jobs:
      • clippy (changed cmux-tui packages): run 41799062615, job 126470077693;
      • lint (linux) in run 41797172027 (job 126464553933), run 41797179957 (job 126464747615) and run 41797164005 (job 126464554854).
    • After the merge it would make feat's lint red, and a base lint red skips the tests after it in the same job (ci(web): lint and budget gates no longer skip the webview and sidecar tests #22197).
    • Fix: remove the crate::...:: prefixes at those four places. Keep crate::terminal_host::HostIncarnation at :139: it is not in scope, and clippy does not flag it. fix(cmux-tui-core): link peers and host proxy authors are published; S1.1 review follow-ups (R-DAEMON-STATE S1 step 2) #34171 had the same lint in the same file, at :120 (fixed in 10f1669).
    • The fleet compile cannot catch this, because it runs cargo check on lib and bin targets, not on tests.

Answers to the hand-off questions

Does the test prove what it claims? Yes.

  • Before the panic, the lane read answers false, because the routes and the state both lack the terminal.
  • In the closure, surfaces.insert copies the map that the published snapshot shares. Then the panic cuts the change short. The cut-short guard publishes nothing (signaled_mutex.rs:425), and its MutexGuard poisons the state.
  • So the last knows_terminal_input_target must take the old read. It finds the placeholder by its host identity, which is a plain field (surface/metadata.rs:98).
  • On 248abb0 (poison check removed), the lane reads the routes, answers false and fails at :156 with the stated message (run 41797164005, job 126464554986). The panic at :153 in that log is the caught one.
  • These limits are not defects:
    • The test calls only knows_terminal_input_target. begin_creating_input and finish_creating_input use the same live_surface_for_terminal, so this is enough.
    • The red arm can catch the regression only if no other thread takes the poisoned lock between the panic and the check, because a guard that recovers the poison publishes the half-made tables. A test mux has an in-memory registry, so it starts no journal maintenance, and it has no terminal threads. The red run did fail.

Can the new load order give a different answer? It cannot give an answer that the old path cannot give.

  • If the state is not poisoned during the call, both orders read the same routes. If it was poisoned before the call, both orders take the old read.
  • The two orders can differ only if another thread poisons the state during the call:
    • The new order answers from routes loaded before any poisoning (a finished change), or from the old read.
    • The old order can also answer from half-made tables, if a guard that recovered the poison published them after the check.
    • In each case, the answer is the state at some instant in the call, before or after the change that panicked. The old path also reads that state, because it recovers the poison.
  • The arc-swap guard drops at the end of the if block, before the old read takes the session lock.

Can the drop of the poisoned test mux hang or panic? No.

  • Mux::drop calls finalize_terminal_journal, which takes the state with unwrap_or_else(PoisonError::into_inner) (mux/lifecycle.rs:184), as its comment at :180 says.
  • The placeholder's shutdown_for_daemon returns at PtyRuntime::ExitedHosted. finish_terminal_reader finds no reaper thread and no reader thread, and the capture gate closes at once. None of those locks is poisoned, because the panic held only the session state.
  • The output store has no directory, because the in-memory registry has no database path. So output_store.shutdown returns at once (output_store.rs:1004).
  • Evidence: each green run drops the mux at the end of a passing test. The red run drops it while the failed assertion unwinds, and it does not abort ("finished in 0.02s").
  • Not from this PR: a persistent mux can still panic there. output_store.shutdown calls final_pass, which calls Mux::terminal_resource_surface (mux.rs:949) for each dirty output log, and that function uses self.state.lock().unwrap(). This contradicts "cleanup must not panic again" (lifecycle.rs:180-183). If it runs while the panic that poisoned the state unwinds, the process aborts. A follow-up should fix this.

Are the comments exact? Almost. Three small wording points (non-blocking):

  1. creating_input.rs:386 says "a create whose commit panicked has its terminal in the state and none in the routes". That is true only until a guard that recovers the poison drops. Such a guard publishes the half-made tables, because the state changed and the guard was not cut short. The test's own last read is such a guard. Suggested text: "and, until a guard that recovers the poison publishes, none in the routes". The StateGuard drop comment ("the half-made tables are never published", signaled_mutex.rs:420-423) has the same limit, but that code is from S1.2.
  2. creating_input.rs:378 says "The creating-input lock is a leaf only while the state is not poisoned". Off the lane, it is never a leaf. Suggested text: "On the lane, the creating-input lock is a leaf only while ...". The rule after it ("no code may take the creating-input lock while it holds the session lock") holds today: no caller does, and this PR adds none.
  3. creating_input.rs:135 says "the entry is then written to the live terminal in order". The bytes can also go back to their senders, as for any queue: on a cancelled creation (failure "cancelled"), at the expiry and on a write failure. Suggested text: "written to the live terminal in order (or given back, as any queue)".
  • DESIGN.md S1.4 now has the qualifier. The S1 target summary at DESIGN.md:195 does not: it still says "The creating lock becomes a leaf: its holders read the published routes, never Mux::state".

Evidence at the head

… mux scope (clippy unused_qualifications); comment wording (#35127 review 1)

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

Copy link
Copy Markdown
Contributor Author

Review (claude-reviewer-rds-s14): SHIP at 4e5eb5f
reviewer-session: 98f6aac3-7aed-4548-8817-bf501dbecf3c
author-session: ae25c6d154f94c01a
R9: pass | layer-ownership.md row "Terminals: PTY, process, output, cwd, title, exit, grid size" (Rust, session host). The PR adds no writer. It reorders one read in live_surface_for_terminal, changes comments and adds a test. The route tables and the catalog host index keep their only writers, State::insert_catalog_terminal and remove_catalog_terminal, under the session state guard. The creating-input map keeps its only writers, CreatingInputs::insert and remove, under its own lock.
perf claim: none

No defect blocks the merge. 4e5eb5f fixes the defect from my FIX at 71f21cd. It changes only the test and comments. I did not build or run anything locally.

The defect is fixed

The rest of the commit

  • In creating_input.rs, only comments changed. So the product code is the code I reviewed at 71f21cd, and my answers about the test, the load order and the drop still hold. Fleet compile job 97d92dbabbb6c17adeb0633a covers that code.
  • The new comments are exact:
    • creating_input.rs:137: "or given back, as any queue";
    • :378: "On the lane, the creating-input lock is a leaf only while ...";
    • :386-387: "until a later change that recovers the poison publishes them".
  • Optional nit at :386-387: the half-made tables are published by any later holder that recovers the poison, a read included (the test's own last knows_terminal_input_target is one). "a later holder" would be more exact than "a later change". This nit does not need a new review.
  • Corrections to my FIX comment:
    • DESIGN.md already had the poison qualifier at lines 196-198 when I reviewed. I read only line 195, so my point 4 was wrong.
    • The PR body now names compile job 97d92dbabbb6c17adeb0633a. That edit came after my comment.

Evidence at the head

  • test (linux) in run 41801819083 (job 126480556428, filter creating_input): 13 passed, 0 failed. These include the five process_support::creating_input tests, a_poisoned_session_state_answers_the_creating_input_checks_from_the_state and rds_input_lane::a_key_waits_for_no_creating_input_lock.
  • Fleet compile job 97d92dbabbb6c17adeb0633a at 71f21cd: CmuxNext, app and cmux-tui ok, exit=0. 4e5eb5f changes no product code.
  • macOS lint and tests was still queued when I posted.

@lawrencecchen lawrencecchen added the ready-to-land Reviewed at this head; next-autoland merges it into feat-cmux-next when its only reds are base reds label Nov 4, 2026
@lawrencecchen

Copy link
Copy Markdown
Contributor Author

At 4e5eb5f: receipt #35127 (comment) (SHIP at this head). Focused run https://github.com/manaflow-ai/cmux/actions/runs/41801819083 at this head (--filter creating_input): test (linux) 13 passed, 0 failed. Fleet compile job 97d92dbabbb6c17adeb0633a passed at 71f21cd; the head changes only the test's imports and comments. The clippy red is link_push_bench.rs:59 (base, also red on feat run 41801255441). ready-to-land added.

@cmux-bot
cmux-bot Bot temporarily deployed to ci-routing-agent November 4, 2026 09:23 Inactive
@lawrencecchen
lawrencecchen merged commit 5de31bb into feat-cmux-next Nov 4, 2026
207 of 218 checks passed
@lawrencecchen
lawrencecchen deleted the fix/rds-s14-followups branch November 4, 2026 09:39
lawrencecchen added a commit that referenced this pull request Nov 4, 2026
…d state, so a session with a poisoned state still shuts down (#35127 review note)

Mux::drop -> finalize_terminal_journal -> the output store's last write (snapshot::final_pass, and its memory
budget) looks each dirty terminal up here. The plain unwrap() panicked inside the drop on a poisoned state lock,
so the rest of the shutdown (the journal writer's stop, the browser runtime's shutdown) never ran.
finalize_terminal_journal itself already recovers the poison; this lookup now does the same.

Test: mux::tests::agent_roster_poison::a_persistent_session_with_a_poisoned_state_still_shuts_down (red at
7ef8ee6: panicked at output_store/memory_budget.rs:143).
lawrencecchen added a commit that referenced this pull request Nov 4, 2026
…(terminal_resource_surface recovers the poisoned guard) (#35279)

* test(cmux-tui-core): a persistent session with a poisoned state still shuts down (red)

Mux::drop -> finalize_terminal_journal -> output_store.shutdown -> snapshot::final_pass looks each dirty
terminal up with Mux::terminal_resource_surface, whose plain unwrap() panics on a poisoned state lock; the
rest of the drop then never runs (#35127 review note).

* fix(cmux-tui-core): Mux::terminal_resource_surface recovers a poisoned state, so a session with a poisoned state still shuts down (#35127 review note)

Mux::drop -> finalize_terminal_journal -> the output store's last write (snapshot::final_pass, and its memory
budget) looks each dirty terminal up here. The plain unwrap() panicked inside the drop on a poisoned state lock,
so the rest of the shutdown (the journal writer's stop, the browser runtime's shutdown) never ran.
finalize_terminal_journal itself already recovers the poison; this lookup now does the same.

Test: mux::tests::agent_roster_poison::a_persistent_session_with_a_poisoned_state_still_shuts_down (red at
7ef8ee6: panicked at output_store/memory_budget.rs:143).

* test(cmux-tui-core): a terminal launch credential is refused on a poisoned session state (#35279 review, red)

With terminal_resource_surface recovering a poisoned state, check_launch_credential (an authorization decision)
verifies a terminal from a catalog a panicked change may have left half made; before #35279 it panicked. The
other auth checks fail closed on a poisoned lock (origin_gate peer_or_local).

* fix(cmux-tui-core): a terminal launch credential fails closed on a poisoned session state; the drop test drops the last handle itself (#35279 review)

- Should-do 1: check_launch_credential refuses a terminal credential (credential_closed) while the session state is
  poisoned: the daemon cannot show the terminal is still open, and the catalog lookup now recovers the poison instead
  of panicking. The same rule as the other auth checks (origin_gate peer_or_local). credential_closed is an existing
  wire word, so the resource catalog does not change.
- Should-do 2: the output store's writer holds a mux handle during its pass, so drop(mux) could run the drop on the
  writer thread, outside catch_unwind. The test now takes the last handle (Arc::try_unwrap, at most 5 s) and drops the
  Mux inside catch_unwind.

Test: a_terminal_credential_is_refused_on_a_poisoned_state (red at 70f614b).

* test(cmux-tui-core): the poison tests name Instant, Duration and CredentialCheck through the module scope (clippy unused_qualifications)

This branch was previously deployed

3 inactive (2 outdated) deployments
ci-routing-agent — 77ce030c Deployed Nov 4, 2026 by cmux-bot[bot] via cmux-tui tree preflight #110918
artifacts — 4e5eb5fc Deployed Nov 4, 2026 by lawrencecchen via publish tree-addressed cmux-tui #110758
sentry-dsyms — 4e5eb5fc Deployed Nov 4, 2026 by lawrencecchen via upload cmux-tui dSYMs to Sentry #110758
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-to-land Reviewed at this head; next-autoland merges it into feat-cmux-next when its only reds are base reds risk:normal Risk tier (docs/ci/risk-tiers.md)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant