Skip to content

P0: Ctrl-` right after Cmd-N shows its terminal in the daemon's pane, not an empty pane (display 0e91fb59 at 88341f28932) - #36215

Merged
lawrencecchen merged 5 commits into
feat-cmux-nextfrom
fix/p0-standin-handover
Nov 5, 2026
Merged

lawrencecchen merged 5 commits into
feat-cmux-nextfrom
fix/p0-standin-handover

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

Summary

P0: Ctrl-pressed right after Cmd-N could leave the new terminal tab selected over an empty pane. Two P0 journeys on cmux23 failed this way:r3-after-switch-traced(build cd6a94aa, feat 29efa5800072, STATUS 13438) andr3-e2-reattach` (build 742dc06b, feat 7e6f2ab, STATUS 13556). Both waited 20 s for a focused terminal. Their last frames show "New Tab" and the selected terminal tab "~" in the strip over a blank pane. With this change the daemon's pane shows the person's terminal from the turn it takes Cmd-N's stand-in over.

Cause, from the traced journey's input journal: Cmd-N shows a stand-in workspace and pane at once, and the Ctrl-selects and shows its stand-in terminal there. When the daemon's workspace arrives, its pane takes the stand-in over under another layout id, so the window makes it a newPaneController. Nothing moved the stand-in's selection or pending select to that controller, so it started on the daemon's default tab, the New Tab page. The terminal showed there only if the focus expectation landed later. Also, the stand-in's controller had already left the layout, but it still ran the create's end (selectWhenReported). It then showed its own New Tab page, which took that page's view from the daemon's pane (journal seq 134: show pane=pane:6917… from=- pending_tab_21f0…). r3-e2-reattach` recorded no trail, but its frames match the traced failure, so this fix covers both journeys.

The fix makes the hand-over carry the state, with no retry, timer or second show:

  • DaemonStore.daemonPane(takingOver:) names the daemon pane that took a stand-in over. It uses the same lookup as daemonTarget.
  • A pane's waiting select is now the value type CmuxNextDaemon.PendingTabSelect (it was PaneController.pendingSelectSurface and pendingSelectTab). take(shown:record:) is the rule PaneController.apply used for them. takeOver(_:selected:replacement:) is the hand-over: the stand-in's waiting surface select, else its waiting tab select, else its selected tab (as the daemon's tab in its place, once listed), as one value. A second value would select later what nobody chose then.
  • The window makes the daemon pane's controller while the stand-in's controller is still mounted, because a new screen takes its panes before the old screen leaves. The new controller calls takeOver in its init, so its first apply selects and shows what the stand-in showed. A new stand-in pane skips this lookup.
  • A create made on the stand-in that ends after the hand-over selects in the daemon pane's controller (successor), not in the stand-in's. The Ctrl-` create and the browser tab opener read that controller's strip.
  • CmuxNextApp is 1 line below the base (no arch exception).

The split, New Column and New Row stand-ins do not change. Their daemon pane keeps the same layout id and controller (the record is updated in place), and daemonPane(takingOver:) is nil for a daemon handle.

Testing

CmdNThenNewTerminalTabTests.theDaemonsPaneThatTakesCmdNsStandInOverShowsTheTerminalTheStandInShowed fixes the order the trail shows (PendingTabSelectTests covers the value type's rules):

  1. It stops the daemon, presses Cmd-N, then Ctrl-. A gate on the workspace's pendingCreate holds the Ctrl-'s create.
  2. It checks that the stand-in shows its stand-in terminal, then resumes the daemon.
  3. When the daemon's pane mounts, the test requires that pane to select and show that terminal. At that point no daemon terminal and no focus expectation can show it there.
  4. It opens the gate. The daemon's terminal must then show in the daemon's pane, and the stand-in's controller must show nothing more.
What Commit Fleet job Result
Red on the base (the test alone) 6c2716b efa15d48 (aws-m4pro-1) red: the daemon's pane took the stand-in over selecting and showing the daemon's New Tab page tab_9392…, not the stand-in terminal pending_tab_53ec… (lines 128 and 130); the suite's other 3 tests passed
Red, 10 rounds (never-merge) 3c1bb21 88ed2f4f (aws-m4pro-1) red in 10 of 10 rounds at lines 128 and 130; in rounds 7-10 also at line 146: the stand-in's controller, out of the layout, showed its New Tab page stand-in again after the hand-over (the trail's seq 134)
Green at the head, with the suites of the paths it touches 88341f2 b642d397 (aws-m4pro-7) green: all 14 suites passed, 88 tests, 0 skipped; the ordered test passed in 0.51 s
Green, 10 rounds (never-merge) 39de72e 0c1c735e (aws-m4pro-1) green in 10 of 10 rounds
Display run at the exact head 88341f2 0e91fb59 (display worker cmuxs-Mac-mini-3, console session) green: all 4 tests of the suite passed, none skipped
Compile check, CmuxNext and app (Linux) red 6c2716b / head 88341f2 916201d1 / 45685209 passed / passed
Test targets compile (package-test-lane.sh compile-tests) red 6c2716b / head 88341f2 a661353c / a590e2c1 passed / passed

The same build of the failed journey (cd6a94aa) passed r3-after-switch-traced 15 of 15 times on cmux22 and cmux23 with the P0 runner's kit and flags. A 20-round soak of the old, unordered test passed 20 of 20. So a soak does not show this race; the ordered test above does.

Observed / Not run

  • Observed: the rows above, and the input journal and pane views of the failed r3-after-switch-traced run (/work/wt/chrome-parity-design/p0-journey-runner/runs/cd6a94aa/).
  • Not run: the two P0 journeys on a dogfood build of this head. The P0 runner runs them on the next dogfood build after this merges.
  • Bench (instant-new-surfaces, Cmd-N and Ctrl-, 12 runs per phase): a same-host A/B on cmux24-perf in the order base, head, base, head (base 94b22d2354d: d0b1ae51, a6a93662; head 88341f28932: 184618ee and the fourth round). Every key-turn figure of the head is inside the spread of the two base rounds: idle key p95 Cmd-N 16.4 / 15.7 ms base, 16.0 / 15.7 head; Ctrl- 13.2 / 12.8 base, 13.1 / 13.1 head; under load Cmd-N 20.4 / 19.2 base, 17.6 / 15.9 head; Ctrl-` 8.4 / 9.1 base, 6.5 / 9.4 head. Visible-frame p95 swings more between two rounds of the same arm (Cmd-N idle 96.2 / 69.6 ms base) than between the arms. Cmd-N's key turn is over the 10 ms budget on the base too; that is the CMD-N FIRST FRAME owner's work (its view reuse follows this PR). The first pair, on two hosts, compares nothing.

Changelog

Fixed: Pressing New Terminal Tab right after New Workspace shows the new terminal in the new workspace, not an empty pane.

Proof

No capture: the change is which tab the daemon pane's controller selects at the hand-over. A still image cannot show the race; the ordered test and the two journeys check it.

… over shows the terminal the stand-in showed (P0 STATUS 13438, red)

Cmd-N, then Ctrl-`: the stand-in pane selects and shows its stand-in terminal; then the daemon's pane takes
the stand-in over under another layout id and the window makes it a new pane controller. The test holds the
Ctrl-`'s create behind a gate until that controller mounted, so the order is fixed on every run: no daemon
terminal and no focus expectation can show the terminal there first. It checks that the daemon's pane selects
and shows the person's terminal from the turn it mounts, then that the daemon's terminal shows there when the
create goes on, and that the stand-in's controller, out of the layout, shows nothing more.
…over selects and shows what the stand-in showed (P0 STATUS 13438)

Cmd-N's stand-in pane and the daemon's pane that takes it over have different layout ids, so the window
makes the daemon's pane a new PaneController, and nothing moved the stand-in's view state to it: it started
from the daemon's default tab (the New Tab page) while the person had selected the Ctrl-`'s terminal on the
stand-in. The terminal then showed only if the focus expectation landed later, and the stand-in's controller,
already out of the layout, still ran the create's end (selectWhenReported) and showed its own New Tab page,
taking that page's view from the daemon's pane: the terminal tab selected over an empty pane for 20 s.

The hand-over itself now carries the state, with no retry, timer or second show:
- DaemonStore.daemonPane(takingOver:) names the daemon pane that took a stand-in over (the store already
  knew it for daemonTarget).
- The daemon pane's controller, made while the stand-in's is still mounted (a new screen takes its panes
  before the old one goes), takes the stand-in's selected tab (the daemon's tab in its place once listed)
  or a select still waiting there as its own pending select, so its first apply selects and shows it.
- A create made on the stand-in that lands after the hand-over selects in the daemon pane's controller
  (successor), never in the stand-in's, and the Ctrl-`'s end reads that controller's strip.
…the other creates on a stand-in read the daemon pane's strip

Review of 7a26c7a:
- The daemon pane takes the stand-in's waiting surface select, else its waiting tab select or selected
  tab, never both: apply uses the surface first and would leave a tab select set for a later apply.
- A new stand-in pane skips the walk of the workspace's panes (it takes nothing over).
- The browser tab opener and a remote chat's create, which now forward their select to the daemon pane
  after Cmd-N's hand-over, read that pane's strip and waiting select first, as the Ctrl-` create does: a
  tab the person picked there after the hand-over keeps the selection.
@github-actions

github-actions Bot commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

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

Review 88341f2: 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=pr36215 -f ref=fix/p0-standin-handover -f platform=macos -f backend_mode=local

Shipped in:

@lawrencecchen lawrencecchen added the risk:normal Risk tier (docs/ci/risk-tiers.md) label Nov 5, 2026
…n hand-over are a store value type (PendingTabSelect); no app_lines exception

Lead on #36215: meet app_lines by moving the logic to its package. PaneController's two waiting selects
(pendingSelectSurface, pendingSelectTab), the apply rule that takes them, and the hand-over carry are now
CmuxNextDaemon.PendingTabSelect (take(shown:record:) and takeOver(_:selected:replacement:)), with unit
tests (PendingTabSelectTests). PaneController keeps only the calls; CmuxNextApp is 1 line below the base,
so the arch exception goes. No behavior change from 2c93f4b.

The remote chat's successor read (AgentTabs+Lifecycle) goes back to the base: Cmd-N stand-ins exist only
on the local daemon, so a remote create never has a successor there (review of 2c93f4b).
lawrencecchen added a commit that referenced this pull request Nov 5, 2026
…n hand-over are a store value type (PendingTabSelect); no app_lines exception

Lead on #36215: meet app_lines by moving the logic to its package. PaneController's two waiting selects
(pendingSelectSurface, pendingSelectTab), the apply rule that takes them, and the hand-over carry are now
CmuxNextDaemon.PendingTabSelect (take(shown:record:) and takeOver(_:selected:replacement:)), with unit
tests (PendingTabSelectTests). PaneController keeps only the calls; CmuxNextApp is 1 line below the base,
so the arch exception goes. No behavior change from 2c93f4b.

The remote chat's successor read (AgentTabs+Lifecycle) goes back to the base: Cmd-N stand-ins exist only
on the local daemon, so a remote create never has a successor there (review of 2c93f4b).

(cherry picked from commit 88341f2)
@blacksmith-sh

blacksmith-sh Bot commented Nov 5, 2026

Copy link
Copy Markdown

Found 1 test failure on Blacksmith runners:

Failure

Test View Logs
test_every_compile_job_names_its_warm_sources (main.ThisTree.test_every_compile_job
_names_its_warm_sources)/
test_every_compile_job_names_its_warm_sources (main.ThisTree.test_every_compile_job
_names_its_warm_sources)
View Logs

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

@lawrencecchen lawrencecchen changed the title P0: Ctrl-` right after Cmd-N shows its terminal in the daemon's pane, not an empty pane (display 3d42445f at 464eea6afdd) P0: Ctrl-` right after Cmd-N shows its terminal in the daemon's pane, not an empty pane (display 0e91fb59 at 88341f28932) Nov 5, 2026
@lawrencecchen

Copy link
Copy Markdown
Contributor Author

Review (independent review subagent): SHIP at 88341f2

Read-only correctness review in three rounds: 7a26c7a (SHIP), 2c93f4b plus the arch exception at 464eea6 (FIX for one false phrase in the exception's why; the exception is gone since), and 464eea6..88341f2 (SHIP).

  • The hand-over runs in the right order: the new screen takes its panes before the leaving screen is torn down, so the daemon pane's controller is made while the stand-in's controller is still mounted. The daemon pane enters the layout in the same batch that makes daemonPane(takingOver:) non-nil.
  • PendingTabSelect.take matches the old apply rule exactly (surface first, then tab; only the taken part is cleared). takeOver carries one value (the waiting surface, else the waiting tab, else the selected tab as its replacement).
  • Split, New Column, New Row and dock stand-ins keep one controller, whose model updates in place, so successor is nil there.
  • successor cannot loop: in the split case it could name the same controller only if handles mapped the daemon handle while that controller still held the provisional model. adoptHandles installs the map and adopts the model in one synchronous call, so that state cannot be reached.
  • Not covered: a direct apply (resyncStrip, a route-title apply) on the stand-in in the short gap after the store hand-over and before the layout apply makes the carry read the stand-in's New Tab page. That falls back to the base behavior: the terminal shows when the Ctrl-focus expectation lands.selectWhenReportedKeepingFocus` is not forwarded.

@lawrencecchen

Copy link
Copy Markdown
Contributor Author

Base finding, not this PR: nightly-guards / workflow-guard-tests / release-notary (and Guard status, its wrapper) fails at 88341f2 in test_every_compile_job_names_its_warm_sources on .github/workflows/cu-driver-eval.yml, a workflow this PR does not touch. The same guard run fails on feat-cmux-next's own pushes (run 41979439376 at da793ba also fails on cu-driver-eval.yml's retention-days 90), and the same test fails on unrelated PRs (runs 41979341325 and 41979325252). cmux-next daemon tests shows CANCELLED: a superseded or infrastructure run, rerun on the same sha if it stays so.

@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 5, 2026
@lawrencecchen

Copy link
Copy Markdown
Contributor Author

ready-to-land at 88341f2: Review (independent review subagent): SHIP at 88341f2 (receipt above); compile 45685209 and test-target compile a590e2c1 passed; green b642d397 (14 suites, 88 tests, 0 skipped), 10-round green 0c1c735e, display run 0e91fb59 (4 of 4, none skipped); red at 6c2716b (efa15d48) and 10 of 10 red rounds (88ed2f4f). Non-green checks: nightly-guards is the base finding linked above; cmux-next daemon tests was cancelled with no runner and no step (job 127078922873, 05:57-06:15Z), an infrastructure cancellation; its rerun waits for the run's other job to end.

@lawrencecchen
lawrencecchen merged commit 1e6a5a6 into feat-cmux-next Nov 5, 2026
281 of 288 checks passed
@lawrencecchen
lawrencecchen deleted the fix/p0-standin-handover branch November 5, 2026 07:40

This branch was previously deployed

1 inactive deployment
ci-routing-agent — 88341f28 Deployed Nov 5, 2026 by lawrencecchen via cmux-tui tree preflight #114727
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