Skip to content

Fix desync after Cmd-N: a late Chromium page gets the keys through the focus model; G1 counts cmux pages - #29272

Merged
cmux-bot[bot] merged 5 commits into
feat-cmux-nextfrom
fix/desync-cmd-n-page-window
Oct 31, 2026
Merged

cmux-bot[bot] merged 5 commits into
feat-cmux-nextfrom
fix/desync-cmd-n-page-window

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Oct 31, 2026 •

Copy link
Copy Markdown
Contributor

Summary

The action fuzzer (#29071) recorded three debug.desync findings after Cmd-N (STATUS 1002, 1022). This PR classifies each one and fixes it at its cause.

W1 and W4: a real desync. At the first Cmd-N of a launch, no New Tab spare exists and Chromium is not started, so the New Tab page (an agentPage) shows before its Chromium page exists. The focus applier makes the page's WebKit stand-in first responder. When the Chromium page attaches, PageWebView.attachChromium granted Chromium focus by itself, behind the focus model. The stand-in stayed first responder, and the applier never recorded the focused page. When the app is active, the page window's key change (childWindowDidBecomeKey) makes the applier apply the model again. When the app is not active (the fuzzer's capture slot, or a person who switched away before the page attached), nothing applies it again. The world check then keeps W1 appkit content:pane_* (PageWebView) != model agentPage and W4 chromium page nil != model tab_*. The window becoming key again heals it.

Fix: when a page's Chromium page attaches, the page asks its Chromium host (PageChromiumHosting.focusViewChanged, default: no focus model). The app's host finds the pane whose own Chromium page (TabContent.childWindowTab) this is and sends contentPresented. The applier then applies the model as it does for any newly presented content: no responder in the cmux window, the page recorded and granted, in one key change. Any other page (the docked Home's Channels page, a page that no pane shows, a parked spare) keeps its own grant, as on the base. A fall back to WebKit is unchanged.

G1: a false finding of the check. ChildPageGeometry.sample counted only browser tabs as hosts. The page window of every shown cmux page in Chromium (the New Tab page, an agent chat) was then reported as page window (618, 268, 892, 692) covers no pane. Hosts now come from TabContent.childWindowTab, the rule that the focus applier and the input observation already use. The same sample drives WindowOverlayLayer.pageWindowDidChangeFrame, which forced a page update on each frame change of such a page window. ChildPageGeometry.check had no caller and is removed.

Testing

Red first: PageWebViewFocusViewChangeTests (CmuxNextPagesTests), added at a346e23.

  • Red at a346e23: fleet step 7639b30f74c1aeb8dd556828 failed with 5 issues on its own assertions (the host was not told; the page granted focus itself: page.focusAsks → [true]).
  • Green at 976b215: step d15a4188247c739fcd544de5; at 5ceaf20: step 095abe66ff1b6e4e1e31afd7. Both passed PageWebViewFocusViewChangeTests, PageWebViewChromiumTests and ChromiumPageFocusInvariantTests. The head ad21c67 only removes a no-op call and one test case of it, and corrects docs.
  • Compile of the head ad21c67: job 155f35f2347c86058d4061c8 (CmuxNext with tests, app, cmux-tui) passed.
  • Journey found-desync-cmd-n-page (journeys(fuzz): red journey for the desync findings G1, W1, W4 after Cmd-N #29274): one Cmd-N, a 3 s settle, then debug.desync check must list no G1, W1 or W4.

Observed / Not run

Observed (journey, cmux19, exclusive capture lease, diagnostic mode):

  • v2.2.11-opt (78ca62c225bb0943594e3cd2), app inactive: RED, W1 + W4 + G1 13 s after Cmd-N; debug.focus: first responder PageWebView, model agentPage.
  • feat-tip-fixes-v2 (f2f3083bac80ab71101d03ac, feat acfd693), app active: RED, G1 only (twice).
  • This PR at 976b215 (build 568ad9fbd033e4ba0ac995fb), app active: PASS (no G1, W1 or W4).
  • Correction (coordinator, 11:4xZ): build 2db9e5949fd59e393aa1bab7 (5ceaf20) did NOT run this journey. It ran a later dropped "-inactive" variant, which FAILED on its own app_active == false precondition. Do not count it.
  • The reviewed journey (journeys(fuzz): red journey for the desync findings G1, W1, W4 after Cmd-N #29274), whole host with the app active, through the mini scheduler (spec desync-g1w1w4-journeys), 12:11-12:19Z:
    • feat base f2f3083bac80ab71101d03ac, cmux20: RED, G1 only (no window unsettled; key window CefNSWindow).
    • this PR's head ad21c67 (build ed24b7b111a5fe9b44eaf569), cmux20: PASS (no G1, W1 or W4, no window unsettled).
  • Home dock Channels, first open (the review's FIX case at 976b215), journey found-desync-channels-type: the SAME state on the feat base f2f3083b (cmux19), on 976b215 (build 568ad9fbd033e4ba0ac995fb, cmux19) and on this head (build ed24b7b1, cmux20): Channels in Chromium never gets the keyboard; the key window is ShellWindow and the first responder is the hidden PageWKWebView. This is a pre-existing bug on feat, not caused by this PR, and this PR does not fix it. The journey cannot tell 976b215 from the head; the identity match is shown by the code reading and the review only.

Not run, or read only:

  • W1/W4 on a feat build with the app inactive. In the capture slot, feat builds become active after Cmd-N, so the journey cannot make that state there. The inactive path on feat is shown by the Swift test and by the code reading above, not by a journey.
  • No app-level test of the pane lookup closure in AppDelegate (the arch ratchet refused the App lines for a testable extension). The review accepted this.

Changelog

none

Proof

No capture: there is no visible change. The fix changes which view holds the keyboard after a cold New Tab page attaches while cmux is in the background, and what a debug-only check reports.

…showed must reach the pane's focus model

PageWebViewFocusViewChangeTests: a page shown and focused before its Chromium page exists (the
first Cmd-N of a launch) must tell its host's focus model when Chromium takes its keys, and when it
falls back to WebKit, not grant Chromium focus behind the model. Fails on the base: the page grants
focus itself and the host is never told (the action fuzz's W1/W4 desync, 2026-10-31). The test
seam: PageChromiumHosting.focusViewChanged (default: no focus model) and FakeChromiumPage.focusAsks.
…ne's focus model; G1 counts cmux pages as hosts

W1/W4 (a real desync). A cold New Tab page (the first Cmd-N of a launch) shows before its Chromium
page exists, so the focus applier made its WebKit stand-in first responder. When Chromium attached,
PageWebView granted the Chromium page focus itself: the stand-in stayed first responder and the
applier never recorded the focused page. While the app is active the page window's key change
makes the applier re-apply; when it is not (the action fuzz's capture slot, or a person who left
the app before the page attached), nothing re-applies: debug.desync kept W1 'appkit content:pane
(PageWebView) != model agentPage' and W4 'chromium page nil != model tab' (action fuzz 2026-10-31,
v2.2.10-opt 2efedfc3, v2.2.11-opt 78ca62c2), and on return to the app the keys went to the cmux
window, not the page (reclaimKeyAfterActivation skips a Chromium agentPage).
Now the page asks its Chromium host (PageChromiumHosting.focusViewChanged) when its keyboard view
changes (Chromium attached, or a fall back to WebKit); the app's host finds the pane that shows the
page and sends contentPresented, so the applier applies the model again: first responder nil, the
page recorded and granted, in one key change. A page outside a pane keeps its own grant.

G1 (a false finding of the check). ChildPageGeometry.sample listed only browser tabs as hosts, so
the page window of every shown cmux page in Chromium was 'page window (618, 268, 892, 692) covers
no pane' after each Cmd-N (x11 per round on feat-tip-fixes-v2 f2f3083b, x10 on v2.2.10-opt).
Hosts now come from TabContent.childWindowTab, the rule the focus applier and the input
observation use. ChildPageGeometry.check had no caller and is removed.
@lawrencecchen lawrencecchen added the risk:normal Risk tier (docs/ci/risk-tiers.md) label Oct 31, 2026
@github-actions

github-actions Bot commented Oct 31, 2026 •

Copy link
Copy Markdown
Contributor

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

Review ad21c67: 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=pr29272 -f ref=fix/desync-cmd-n-page-window -f platform=macos -f backend_mode=local

Default changes

This pull request changes 1 default at ad21c67e7589 (what everyone gets without configuring anything):

  • tunable-source.CmuxNextApp/Windows/ChildPageGeometry.swift:sample: "38ca6760a105bf9cf035470585c772959506c41df88a088c4cddc51143b077e4" -> "d3a708627824ada552969e13942e81b1829733ebdd68c99fe3693d9d6e838e93"

Signed off on this commit by review (claude-opus-reviewer).

Merge receipt: merged as f0e2d0e45ff9 with these defaults changed, signed off by review (claude-opus-reviewer).

Regenerate the snapshot with python3 scripts/ci/defaults_guard.py snapshot.

Shipped in:

@lawrencecchen

Copy link
Copy Markdown
Contributor Author

Review (claude-opus-reviewer): FIX at 976b215
reviewer-session: 08a67960-956f-4f82-8def-620ddc63f05e
author-session: desync-owner-g1w1w4
perf claim: none

Finding 1 (correctness, must fix): the pane lookup takes over pages that the focus applier cannot focus. Result: the docked Home's Channels page loses the keyboard when its Chromium page attaches.

AppDelegate.swift:234 finds the pane with page.isDescendant(of: $0.view) and returns pane != nil. PageWebView+Chromium.swift:198 then skips the page's own grant. But the applier hands the keys to the Chromium page only when the page is the content's own page. TabContent.childWindowTab (CEFPageChromiumPage.swift:133-139) reads .page(view) as view.content as? PageWebView and reads .agent(view) as view.chromiumPage. A PageWebView nested deeper in a pane's content matches the lookup, but the applier cannot focus it.

A real case is the docked Home in Channels mode:

  • The Home dock tab is an InternalPageView whose content is a HomeDockView (HomeDockTab.makeView). The Channels PageWebView sits inside it (HomeDockView.makeContent(.channels)). .homeChannels is in FirstPartyPageSchemes.descriptors, so with the default engine it runs in Chromium.
  • On the first switch to Channels (HomeDockSwitchView.switchTo -> show -> focusPrimaryInput), HomeDockView.focusContent(.channels) calls channels.focusPage() before makePage returns. With no session yet, this makes the hidden WKWebView first responder (PageWebView.swift:508-514). openChannelsRoute calls it a second time, also before the attach.
  • At the base, attachChromium then took the self-grant, because firstResponder === webView, so the Chromium Channels page got the keys.
  • At the head, the closure finds the dock pane and returns true, so the self-grant does not run. contentPresented resolves .page, and childWindowTab is nil because the content is a HomeDockView. landKeyboardInPrimaryInput returns false because HomeDockView.primaryInputView is nil in Channels. responder(of:isInside:) is true because the hidden WKWebView is inside the view, so the applier changes nothing (FocusEffectApplier.swift:135-143). The hidden WKWebView keeps first responder, and typing into Channels is lost until a click. If the dock pane is not the focused pane, contentPresented does nothing, and the grant is still suppressed.

Fix: return true, and send contentPresented, only for the pane whose content the applier focuses through this page. Match by identity, not by descendant: case .agent(let view) where view.page === page, or case .page(let view) where view.content === page. For any other match, return false so the page keeps its own grant. Add an app-level regression test for the closure. The current tests stub the host's focusModel to true, so they do not cover the pane lookup or the contentPresented send.

Checked, no defect found

  • Claim 1 (G1): sample now uses TabContent.childWindowTab. ChildPageGeometry.check had no caller (verified with git grep). DebugAXFrame and InputInvariantMonitor now count cmux pages as hosts. WindowOverlayLayer.pageWindowDidChangeFrame no longer forces a page update when a cmux page window's frame matches its host, and still forces one when the frame is off its host. That is correct. Note, debug only, not blocking: the host check ignores ancestor alpha, but setChromiumVisibility hides the page window under a paint-hold ancestor with alpha 0. So during a paint hold a cmux page can now report pane X: page missing != host. The monitor's settle may absorb it.
  • Claim 2 (agent and New Tab path): ContentPresented sets force_responder only for the focused pane (reducer.rs:183). finish then emits RevealPane and MoveResponder (reducer.rs:589-598). This is the same send that PaneController makes at :451, with the same isParked guard. .agentPage resolves childWindowTab after chromiumSession is set, so focusPage records the page and makes one key change. Hidden pages keep the ask (CEFTab.setFocused checks isShown), so I found no new focus theft or key-window loop beyond the existing presentation path. The parked New Tab spare (in window.contentView, NewTabSparePool.park), top pages (controller.content == nil), and parked or companion workspaces do not match the lookup, so they keep the old self-grant. Everything runs on the main actor. The walk is O(panes) once per attach or fallback. Calling fallBackToWebKit before router.reset() is harmless: the applier blurs the ended CEF tab and makes the WebKit view first responder.
  • Tests: the red commit fails at the base for the right reason (the host is not told, and the page grants focus itself). Coverage gap: as described in Finding 1, no test covers the app closure.

…el; a nested page keeps its own grant

Review of #29272 (FIX at 976b215, claude-opus-reviewer): the pane lookup matched any page inside
a pane's content (page.isDescendant(of: content.view)). The docked Home's Channels page sits inside
HomeDockView: its attach then skipped its own focus grant, and the applier cannot focus it
(childWindowTab is nil for it), so typing went nowhere. The lookup now takes only the pane whose
TabContent.childWindowTab is this page's Chromium tab, the exact page the applier gives the keys
to; any other page keeps its own grant, as on the base. A fall back to WebKit (no Chromium page)
matches no pane and changes nothing, as on the base.
lawrencecchen added a commit that referenced this pull request Oct 31, 2026
…y with cmux in the background

W1/W4 need the app inactive while the New Tab page's Chromium page attaches; debug.window.focus
(smoke_make_key) activates the app on feat builds (the feat-tip-fixes-v2 and #29272 runs had
app_active true and showed only G1). This variant skips that step and expects debug.focus
app_active false, so a pass is about the inactive case.
@lawrencecchen

Copy link
Copy Markdown
Contributor Author

Review (claude-opus-reviewer): SHIP at 5ceaf20
reviewer-session: 08a67960-956f-4f82-8def-620ddc63f05e
author-session: desync-owner-g1w1w4
perf claim: none

I reviewed the delta 976b215..5ceaf20 (one commit, AppDelegate.swift only). It fixes Finding 1 of my earlier review.

Finding 1 is fixed

  • The lookup now matches only the pane whose TabContent.childWindowTab is this page's own CEF tab ((page.chromiumPage as? CEFPageChromiumPage)?.tab). That is the object that FocusEffectApplier gives the keys to in its .agentPage and .page cases. When the closure returns true, the applier can now always finish the grant.
  • Home dock Channels: the dock's .page content is a HomeDockView, so childWindowTab is nil and no pane matches. The closure returns false, and attachChromium takes the base self-grant again (PageWebView+Chromium.swift:198).
  • Attach order: chromiumSession is set before focusViewChanged is called, so page.chromiumPage is not nil there. Agent and New Tab pages (.agent(view), view.chromiumPage) and direct page tabs (.page(view), view.content === page) match as intended. A .browser tab can never be identical to a cmux page's tab.
  • Spares, top pages and parked or companion workspaces do not match, as at 976b215.

Notes (not blocking)

  • The fall back to WebKit is now a no-op in the app. fallBackToWebKit sets chromiumSession = nil before it calls focusViewChanged (PageWebView+Chromium.swift:211 and :219). So page.chromiumPage is nil there, no pane matches, and the result is discarded. The behavior is the same as at the base, so there is no regression. But three texts still say that the app re-applies focus on a fall back: the comment at :218, the protocol doc in PageChromiumHosting.swift, and the PR body. The test aFallBackToWebKitTellsTheHostThatWebKitTakesTheKeys checks only the seam. Correct the docs or remove the call in a follow-up. Any fall-back desync from before this PR stays as it was.
  • No app-level test for the closure. I do not block on this. The red/green rule is met: the seam test fails at the base and passes at the head, for the right reason. The closure is now a single identity match against childWindowTab, the same rule that the applier uses. Please add the result of the journey found-desync-cmd-n-page-inactive on a build of this head to the PR's Observed section before landing. If you can, also run one journey that opens the docked Home's Channels view for the first time in Chromium and types into it, because the defect in 976b215 lived in this closure.

…y what the code does

Review at 5ceaf20 (SHIP, note): fallBackToWebKit clears the Chromium session before it called
focusViewChanged, so the app's lookup (the pane's own Chromium tab) never matched and the call did
nothing. It is removed; a fall back keeps the base behavior. The PageChromiumHosting and
PageWebView docs and the test file now describe only the attach case.
lawrencecchen added a commit that referenced this pull request Oct 31, 2026
… app turns active in the slot

Takes back 97c4519. On feat builds (feat-tip-fixes-v2 f2f3083b; #29272 head 5ceaf20, build job
2db9e5949fd59e393aa1bab7) the app is active after one Cmd-N even without smoke_make_key, so the
variant's app_active == false never holds there and it can only fail. W1/W4 need an inactive app
while the page attaches; on feat the Swift test PageWebViewFocusViewChangeTests (#29272) covers that
path. found-desync-cmd-n-page stays.
@lawrencecchen

Copy link
Copy Markdown
Contributor Author

Review (claude-opus-reviewer): SHIP at ad21c67
reviewer-session: 08a67960-956f-4f82-8def-620ddc63f05e
author-session: desync-owner-g1w1w4
perf claim: none

I reviewed the delta 5ceaf20..ad21c67 and the updated PR body.

Delta

  • fallBackToWebKit no longer calls focusViewChanged. At 5ceaf20, the app closure could not match a pane on that path, because chromiumSession was already nil. So removing the call changes no behavior: the fall back is the same as on the base.
  • The removed test aFallBackToWebKitTellsTheHostThatWebKitTakesTheKeys tested only that call. The two remaining cases still test the attach behavior that this PR fixes: the host is told, and there is no grant behind the model. Without a host, the page still grants focus itself. FocusViewRecord is still used.
  • The docs in PageChromiumHosting.swift and PageWebView.swift now describe only the attach case. They agree with the code. The parameter doc at ChromiumPageHost.swift:35 ("the pane showing the page") is still correct.

PR body

  • It describes the identity match and says the fall back is unchanged. It records the red and green steps, the compile of the head, and the journey results per build.
  • It says that W1 and W4 with the app inactive on feat were not reproduced by a journey, and that the closure has no app-level test. Both disclosures are acceptable.
  • Small gap, not blocking: the body does not say that no journey typed into the docked Home's Channels page. The code reading closes that case: the Channels page matches no pane, so its own grant runs as on the base. You can add one line under "Not run".

No correctness defect found.

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

Copy link
Copy Markdown
Contributor Author

Checks at ad21c67 before ready-to-land: no failed check. defaults guard was red before the receipt (it waits for a SHIP at the head: tunable-source of ChildPageGeometry.sample changed); after the receipt, its rerun passed. Pending: cmux-next swift test, fast tier, ios-tests, ci-status, guard status. Receipt: #29272 (comment) and landing/reviews/ad21c67e7589386862d108d4086a027a92a6a7a6.md.

lawrencecchen added a commit that referenced this pull request Oct 31, 2026
…esync-channels-type

Review of #29274 (FIX at 6e1b67e, comment 6542870799):
1. found-desync-cmd-n-page is first_in_app with a fresh_app step: only the first Cmd-N of a launch
   starts a cold New Tab page; an earlier journey in the lease could start Chromium and hide W1/W4.
2. It asserts debug.desync check.unsettled is empty: W1-W4 are skipped for an unsettled window.
3. Its why says W1/W4 do not occur on feat builds (the window becomes key in the slot); only G1
   makes it red on the feat base.
4. fuzz_daemon_agrees comes last in the expect (it fails on any desync violation too).

found-desync-channels-type (coordinator 2026-10-31, the FIX case of #29272's review at 976b215):
opens the docked Home's Channels page for the first time in a fresh launch, waits for its
Chromium page, then sends real key events through the app's event queue (Cmd-K, then 'zq7');
the page's palette must open and the text must be in its focused field. Needs run.py --whole-host
(a key window). Expected red on build 568ad9fb (976b215), green on #29272's later heads and feat.
lawrencecchen added a commit that referenced this pull request Oct 31, 2026
… typed keys

Its first round (build 568ad9fb = #29272 at 976b215, cmux19, whole host, 12:09Z) stopped in the
harness: debug.key sequence mode returned 'no event timestamp to start the sequence from' on the
capture mini, before any key. The steps before it already showed the defect: Channels in Chromium,
its page window visible over the dock, key window ShellWindow, first responder PageWKWebView (the
hidden WebKit stand-in). The journey now asserts that state directly after the page paints: the
key window is a CefNSWindow and the first responder is not PageWKWebView. The palette click and the
typed keys are gone. finally's home.switchView uses focus: true (review note).
lawrencecchen added a commit that referenced this pull request Oct 31, 2026
…e-existing bug, not of #29272

Rounds through the mini scheduler (spec desync-g1w1w4-journeys, whole host, app active) gave the same
state on the feat base f2f3083b, on #29272 at 976b215 (build 568ad9fb) and on #29272's head ad21c67
(build ed24b7b1): Channels in Chromium never gets the keyboard at its first open (key window
ShellWindow, first responder the hidden PageWKWebView). The why now says so, with the builds, and no
longer claims that the journey tells 976b215 from a fixed build.
@lawrencecchen

Copy link
Copy Markdown
Contributor Author

Note from the #29274 review (#29274 (comment)): on build ed24b7b1 (this head), the app's own monitor recorded one G1 at launch, about 2 s before the journey's first step: report desync-20261031T121749-0001, a page window at (-99590, -99733) 'covers no pane'. That frame is the New Tab spare parked off-screen (NewTabSpareParking puts it 100 000 pt outside the content). This PR does not cause it: the page list of ChildPageGeometry.sample is unchanged, and the host list only grew (childWindowTab still includes every Chromium browser tab), so the set of 'covers no pane' results can only shrink. The live check at 3 s after Cmd-N was empty on this head. I could not read the report file: it stays in the capture slot on cmux20, and the rules forbid connecting to a capture mini. Open item, outside this PR: G1 may need to skip a parked spare's page window.

@cmux-bot
cmux-bot Bot merged commit f0e2d0e into feat-cmux-next Oct 31, 2026
276 of 296 checks passed
@cmux-bot
cmux-bot Bot deleted the fix/desync-cmd-n-page-window branch October 31, 2026 14:16
cmux-bot Bot added a commit that referenced this pull request Oct 31, 2026
…rst open (#29386)

* test(home): red: the docked Home's Channels page must be found as the page its tab shows

PageWebViewShownInContainerTests: PageWebView.shown(in:) must name the page a container shows now (the
docked Home's Channels page inside HomeDockView). It fails here: shown(in:) keeps the old rule (only a
content that IS a page), the rule TabContent.childWindowTab has on feat. Because of it, Channels in
Chromium never gets the keyboard at its first open (STATUS 1582): journey
scripts/journeys/journeys/fuzz/found-desync-channels-type.json, red on feat f2f3083b (cmux19), on
#29272 at 976b215 and at ad21c67 (whole host, mini scheduler, 2026-10-31 12:09-12:20Z): key window
ShellWindow, first responder the hidden PageWKWebView.

* fix(home): the docked Home's Channels page in Chromium gets the keyboard at its first open

Channels in Chromium never got the keyboard at its first open (STATUS 1582; red on feat f2f3083b,
#29272 at 976b215 and ad21c67, whole host, 2026-10-31). The dock focused the page before its
Chromium page existed, so the hidden WebKit stand-in became first responder. At attach the page
granted Chromium focus itself and its page window became key, but TabContent.childWindowTab saw
only a page tab whose content IS a page. For the docked Home (an internal page whose content is
HomeDockView) the focus applier found no Chromium page: it took the key change as unchosen,
applied the model again, blurred Chromium pages and reclaimed the keys for the cmux window.

Now a container view names the page it shows (PageWebViewContainer; HomeDockView: Channels while it
shows), and childWindowTab finds it through PageWebView.shown(in:). The applier then gives the
Channels page the keyboard like any page tab in Chromium (one key change, the page recorded), the
page window's key change is the pane's own, and G1 counts that page window as its pane's.

---------

Co-authored-by: cmux-bot[bot] <348531757+cmux-bot[bot]@users.noreply.github.com>
cmux-bot Bot added a commit that referenced this pull request Oct 31, 2026
…Cmd-N (#29274)

* journeys: the action fuzzer as a mode of the smoke-gate driver (ext_fuzz.py, fuzz.py)

Brings scripts/journeys (the journeys runner and the dogfood smoke gate, from
cp/pa-d-dogfood-smoke-gate at 1222849) to feat-cmux-next, and adds the
action fuzz on top of it (cmux-ad 2026-10-31, fuzz-catalog rows a and m):

- ext_fuzz.py: step fuzz_run plays a seeded plan of user actions through
  debug.key, ext_window's drags and the driver's relaunch, at mixed timings,
  with a model of workspaces/tabs/terminals/panes. Oracles after each burst:
  create (or a visible refusal), R3a and R3b through ext_smoke's own
  smoke_r3 / column readings, focus, daemon == app topology, stall > 100 ms
  in cmux code, crash, leak after close-all. A failure is shrunk in the
  same app with ddmin. Speed marks per create: shown_ms, typeable_ms.
- fuzz.py: rounds through run.py (exclusive, pool perf-lead, never cmux17),
  --base A/B on the same mini (ABBA), confirms each shrunk repro in a fresh
  lease and writes it as a regression journey; verdict CLEAN/RED/NOT
  JUDGED/DIAGNOSTIC, .clean file only for a judged clean run.
- Sets: layout (row m: New Column, Cmd-D, Split Down, Ctrl-`, close pane,
  divider drag, window resize, relaunch) and full.
- journeys/fuzz/: the fuzz journey and Lawrence's reported bugs as fixed
  sequences (lawrence-*.json).
- test_fuzz.py (plan, ddmin, speed table, verdicts with a stand-in run.py)
  and test_fuzz_sim.py (the oracles against a simulated app with planted
  bugs): python only, no fleet.

Tests: python3 scripts/journeys/test_fuzz.py (16 OK),
test_fuzz_sim.py (8 OK), test_smoke_gate.py (OK), run.py --check (59 valid).

* journeys fuzz: the corpus plays as one round per set (a close-all between Lawrence's reports)

* journeys fuzz: review of 0224106 (BLOCK 1-3) and the first fleet trial

- A round whose journey failed outside the fuzz verdict (smoke_ready, the
  final expect's other checks, a harness error) is RED, never skipped.
  An action whose input path fails is recorded and judged by the oracles
  (crash first) instead of ending the step.
- predict: a workspace create resets the pane count (delta counts the
  shown workspace only).
- paste: a comment line plus Return, so the next probe's line is clean.
- desync, stall and focus are recorded findings (once per signature, with
  the seed and the actions before), not stops: the first trial on
  v2.2.11-opt and v2.2.10-opt stopped at the first Cmd-N on desync W1/W4/G1
  and a 190 ms CefInitialize stall on both builds. A finding only on the
  build (not on the base) is RED; one on both is a base finding.
- R3b read through settle_check; launch stalls after a relaunch skipped;
  shrink replays add no speed samples or findings.
- Verdict: CLEAN needs --base, --minutes > 0, the full fuzz time and no
  replay; cmux20 (the quiet bench host) out of the default hosts.

Tests: test_fuzz.py 19 OK, test_fuzz_sim.py 9 OK, 80 sim seeds clean.

* journeys fuzz: R3b final scope (cmux-ad 08:06Z) and a fresh start for every run and shrink try

- R3b: New Column (Ctrl-Cmd-D) keeps every old column's exact width; Cmd-D
  and Split Down split inside a column and no OTHER column changes width.
  Cmd-D is no longer a column action. New Column always starts its burst,
  so a red names the action that changed the column.
- The layout set starts each run and each shrink try with every workspace
  closed and one fresh Ctrl-` workspace: the first fleet runs' shrink tries
  started in a workspace an earlier try had split, so ddmin could not shrink.
- Corpus lawrence-column-widths: New Column twice, Cmd-D, Split Down, a
  New Column pair.
- Sim: inside splits; planted 'a split resizes another column' is r3b, a
  split alone is not.

Tests: test_fuzz.py 19 OK, test_fuzz_sim.py 12 OK.

* journeys fuzz: review of 12857a6 (BLOCK: window ids in finding signatures); the first red repro

- signature() drops UUIDs, window ids and long hex: one focus or desync
  finding on the build and the base is one signature (no false RED).
- paste only into a focused terminal (Return would submit a New Tab field).
- The corpus round runs in the chosen set (the layout corpus gets the
  fresh start).
- journeys/fuzz/found-r3b-new-column-narrows.json (P0, red on
  feat-tip-fixes-v2 f2f3083bac80ab71101d03ac): Cmd-N, Ctrl-`, New Column;
  the old column went 872 -> 291 pt. Owner a03ef38e4dc40283d.

Tests: test_fuzz.py 20 OK, test_fuzz_sim.py 12 OK, run.py --check 60.

* journeys fuzz: red repro found-r3a-split-fit-grid-stale (v2.2.10-opt: Cmd-N, Ctrl-`, Cmd-D; fit_grid 110x40 vs grid 36x40)

* journeys fuzz: size trace (daemon size, app grid/fit/drawn, SIGWINCH) per terminal, on in replays; Cmd-N first needs no Ctrl-` set-up

* journeys: a capture lease is held only while journey steps run (cmux-ad 09:05Z)

- run.py: SIGTERM raises Terminated, so run_lease's finally releases the
  lease; an acquire stopped by a signal is ended (capture-host unreserves).
- run.py: the build comes from a per-mini cache outside the slot
  (~/.cache/cmux-journeys/builds/<job>/, newest 3), filled atomically by
  the first lease of that job; later rounds download nothing.
- fuzz.py: ddmin runs outside the round, one short replay lease per try
  (--shrink-tries 10); the in-app shrink is off by default. fuzz.py passes
  its own SIGTERM to run.py as TERM (never KILL) and waits.
- test_run_release.py: release after an error inside the round, after
  SIGTERM while driving, the acquire ended after SIGTERM while acquiring,
  and fuzz.py's SIGTERM reaching run.py as TERM.

Tests: test_run_release.py 4 OK, test_fuzz.py 20 OK, test_fuzz_sim.py 12 OK,
test_smoke_gate.py OK, run.py --check 61.

* journeys fuzz: cmux21 (the tip watcher's smoke candidates) out of the default fuzz hosts

* journeys run.py: a second stop never kills the lease release (review of 22115f3, BLOCK)

- SIGINT and SIGTERM are blocked from before the acquire until the lease
  is inside run_lease's try, and again for the whole clean-up (hog stop,
  release); a stop that came meanwhile is raised after the release. While
  acquiring, a pending stop ends the acquire (its trap unreserves); the
  acquire child runs with the signals unblocked.
- The release itself cannot raise past the finally (OSError, timeout).
- Build cache: the job id must be a plain id; pruning by use (the dir is
  touched on a hit); stale .tmp folders over 60 min removed.
- test_run_release.py DoubleStop: TERM during the driver, then TERM and
  INT during a 3 s release: release-done must be logged. Red on
  22115f3, green here.

Tests: test_run_release.py 5 OK, test_fuzz.py 20 OK, test_smoke_gate.py OK.

* journeys fuzz: normal mode is --whole-host HOST (the app active, its window key); without a key window it is 'not run: known gap'

Normal mode types at once after each create and judges focus against
AppKit's first responder, which needs a key window: fuzz.py refuses it
without --whole-host (use --diagnostic in a slot), passes --whole-host
to run.py (whole-mini exclusive lease, app launched active), and
ext_fuzz.Fuzz raises NotRun when smoke_make_key did not make the window
key, so a slot's limit is never a product red.

Tests: test_fuzz.py 22 OK, test_fuzz_sim.py 13 OK, test_run_release.py 5 OK.

* journeys run.py: a lease the acquire printed as the stop arrived is released (cmux19 09:36Z leak); fuzz default hosts: cmux19 only until each Cua grant

- On a stop while acquiring, run.py reads what the ended acquire printed
  and releases each lease in it (review note on 22115f3; it happened
  on cmux19 at 09:36Z: released by hand after 61 s).
- test_run_release.py LateLease: red on 4c17e14, green here.
- DEFAULT_HOSTS = cmux19: cmux18/22/23/24 wait for their 'CUA GRANTED'
  line (lead 09:3xZ), cmux20 is row m only, cmux21 the smoke candidates.

* journeys(fuzz): found-desync-cmd-n-page, the red journey of the fuzz desync findings G1, W1 and W4 after Cmd-N

One Cmd-N in a fresh launch, a 3 s settle, then debug.desync check must list no G1, W1 or W4
(STATUS 1002, 1022). Red on v2.2.11-opt (job 78ca62c225bb0943594e3cd2, cmux19, diagnostic mode):
W1 'appkit content:pane (PageWebView) != model agentPage', W4 'chromium page nil != model tab' and
G1 'page window (618, 269, 892, 692) covers no pane', still there 13 s after the key. debug.focus:
app_active false, first responder PageWebView, model agentPage. The fix is #29272 (feat-cmux-next).

* journeys(fuzz): found-desync-cmd-n-page-inactive, the same red journey with cmux in the background

W1/W4 need the app inactive while the New Tab page's Chromium page attaches; debug.window.focus
(smoke_make_key) activates the app on feat builds (the feat-tip-fixes-v2 and #29272 runs had
app_active true and showed only G1). This variant skips that step and expects debug.focus
app_active false, so a pass is about the inactive case.

* journeys(fuzz): drop found-desync-cmd-n-page-inactive: a feat build's app turns active in the slot

Takes back 97c4519. On feat builds (feat-tip-fixes-v2 f2f3083b; #29272 head 5ceaf20, build job
2db9e5949fd59e393aa1bab7) the app is active after one Cmd-N even without smoke_make_key, so the
variant's app_active == false never holds there and it can only fail. W1/W4 need an inactive app
while the page attaches; on feat the Swift test PageWebViewFocusViewChangeTests (#29272) covers that
path. found-desync-cmd-n-page stays.

* journeys(fuzz): review fixes for found-desync-cmd-n-page; add found-desync-channels-type

Review of #29274 (FIX at 6e1b67e, comment 6542870799):
1. found-desync-cmd-n-page is first_in_app with a fresh_app step: only the first Cmd-N of a launch
   starts a cold New Tab page; an earlier journey in the lease could start Chromium and hide W1/W4.
2. It asserts debug.desync check.unsettled is empty: W1-W4 are skipped for an unsettled window.
3. Its why says W1/W4 do not occur on feat builds (the window becomes key in the slot); only G1
   makes it red on the feat base.
4. fuzz_daemon_agrees comes last in the expect (it fails on any desync violation too).

found-desync-channels-type (coordinator 2026-10-31, the FIX case of #29272's review at 976b215):
opens the docked Home's Channels page for the first time in a fresh launch, waits for its
Chromium page, then sends real key events through the app's event queue (Cmd-K, then 'zq7');
the page's palette must open and the text must be in its focused field. Needs run.py --whole-host
(a key window). Expected red on build 568ad9fb (976b215), green on #29272's later heads and feat.

* journeys(fuzz): found-desync-channels-type types plain letters into a field the page focused itself

Review of #29274 (FIX at c5cba01, comment 6542986147):
1. Cmd-K is an app shortcut over a Chromium page (agentPane.searchChats) and never reached the page.
   Now a click in the page's own script (debug.page click .lg-search: no AppKit event, no key-window
   change) opens the page's palette with its field focused in the DOM (checked before any key), and
   plain letters 'probe' go through the app's event queue to whatever window is key.
2. smoke_make_key added: fuzz_run (the closing daemon health check) needs it.
3. frame_text reads the typed word from the window; why says the closing daemon check is the
   harness's required evidence, not the claim.
4. finally switches the dock back to Messages and unsets home.channelsEnabled.

* journeys(fuzz): found-desync-channels-type reads the focus state, not typed keys

Its first round (build 568ad9fb = #29272 at 976b215, cmux19, whole host, 12:09Z) stopped in the
harness: debug.key sequence mode returned 'no event timestamp to start the sequence from' on the
capture mini, before any key. The steps before it already showed the defect: Channels in Chromium,
its page window visible over the dock, key window ShellWindow, first responder PageWKWebView (the
hidden WebKit stand-in). The journey now asserts that state directly after the page paints: the
key window is a CefNSWindow and the first responder is not PageWKWebView. The palette click and the
typed keys are gone. finally's home.switchView uses focus: true (review note).

* journeys(fuzz): found-desync-channels-type is the red journey of a pre-existing bug, not of #29272

Rounds through the mini scheduler (spec desync-g1w1w4-journeys, whole host, app active) gave the same
state on the feat base f2f3083b, on #29272 at 976b215 (build 568ad9fb) and on #29272's head ad21c67
(build ed24b7b1): Channels in Chromium never gets the keyboard at its first open (key window
ShellWindow, first responder the hidden PageWKWebView). The why now says so, with the builds, and no
longer claims that the journey tells 976b215 from a fixed build.

* journeys(fuzz): move found-desync-channels-type out of this PR

Coordinator decision: the Channels journey is the red test of a pre-existing feat bug (STATUS 1582:
the Home dock Channels page in Chromium never gets the keyboard at its first open), so it goes with
that bug's fix PR. This PR keeps only found-desync-cmd-n-page.

* journeys(fuzz): found-desync-cmd-n-page waits for a settled window before it checks G1, W1, W4

One expect step held all conditions, and step_expect locks in a positive condition (a
`lacks` too) the first time it holds. While the window was still unsettled the app skips
W1-W4, so "no W1" and "no W4" passed at the first poll and a W1 that came one poll later
did not make the journey red (review of 416219a). Now one step waits for
check.unsettled to be empty, and the next one checks no G1, W1, W4, the frame and the daemon.

---------

Co-authored-by: cmux-bot[bot] <348531757+cmux-bot[bot]@users.noreply.github.com>

This branch was previously deployed

1 inactive deployment
ci-routing-agent — ad21c67e Deployed Oct 31, 2026 by lawrencecchen via namespace autofix #9604
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