Skip to content

fix(app): AppServices takes the key window and app activity from the composition root, never the global NSApp - #28321

Merged
lawrencecchen merged 7 commits into
feat-cmux-nextfrom
fix/arch-rise-appservices-no-nsapp-trap
Oct 31, 2026
Merged

lawrencecchen merged 7 commits into
feat-cmux-nextfrom
fix/arch-rise-appservices-no-nsapp-trap

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

Summary

AppServices read the global NSApp in two places that a test reaches with no application object. NSApp is NSApplication!. In a test process that never made NSApplication.shared it is nil, and the implicit unwrap traps. The trap ends the whole CmuxNextAppTests process and hides every later test. It broke #28275's green steps twice (04224ccd89106f08f50cc908 and eb20257db2553e2e4a4dd378), and #28090's green step.

Sites fixed, with the commit that added each:

  • Sources/CmuxNextApp/AppServices.swift:87 var keyWindowSource ... = { NSApp.keyWindow } (added in 7660c2b). Reached by windows.active, keyWindowRole, the quit sheet, ListHandlers and eventDidBringForward.
  • Sources/CmuxNextApp/Notifications/NotificationCenterService+ProgramStatusSeen.swift:57 guard let services, NSApp.isActive (added in e5df76a). AppServices.init starts the notifications service, and its viewed-program-status observation calls viewedTabs. So any test that builds AppServices and then awaits traps. This is what fix(quit): state files have one owner each and no quit blocks the main thread on them #28275's quit tests did.
  • Sources/CmuxNextApp/Feed/FeedService.swift:87, :127, :135 NSApp.isActive (the owner's presence). AppServices builds FeedService, and its sign-in task reaches startIfSignedIn, so a signed-in test with no application object traps there too.

The fix (composition root, PATTERNS "composition root"):

  • AppServices.init takes two required parameters with no default: keyWindowSource: @MainActor () -> NSWindow? and appActiveSource: @MainActor () -> Bool. AppDelegate passes the real ones ({ NSApp.keyWindow }, { NSApp.isActive }). It is the only production call, and it does not compile without them. AppServices itself reads no NSApp.
  • FeedService.init takes appActive with no default. AppServices passes its appActiveSource.
  • Other reads that AppServices or the tasks, subscriptions and timers it starts reach also ask the injected sources:
    • ChiefPetRoutes appIsActive: the notifications service makes the pet.
    • WindowController's first .appActive focus event.
    • NewTabSparePool's two key-window reads: the pool starts after the first frame.
    • InputObservationBuilder.keyWindow: input.start subscribes to cache.presentationChanges, and the DEBUG InputInvariantMonitor timer and the window-geometry observer run observe. It now takes the key window from services.keyWindowSource().
    • KeyRouter+Hooks keyWindowFocus (the menu key-equivalent gate that AppServices installs on the registry).
  • Reads that AppActions.bind(services) installs (AppDelegate calls it, and so do 102 test files), reached when an action is refused in a process with no window controller, now ask services.keyWindowSource() too: the refusal HUD's window in AppActionContext+Targets (the registry's refusal observers), ChatsOpenCoordinator, HomeChiefActions, LinkHandlers, ViewerService. AppServices construction does not reach these; a test that binds the actions does.
  • Reads with no injected source that a started path reaches now read NSApp? and never unwrap the global: AgentCursorSnapshotBuilder.orderedControllers (the agent cursor visibility source), PaneController.visiblePageWindowsOverContent (SurfaceInvariantMonitor's check, subscribed in AppServices.init) and SheetDismissal.endAll's default (QuitCoordinator.shouldTerminate).
  • The six notification reads ask services.appActiveSource(): viewedTabs (ProgramStatusSeen:57), isViewed (Lookup:42), the program-status visibility (ProgramStatus:67), arrive (NotificationCenterService:454), the hand-off banner (HandoffNotices:151) and the sandbox-run banner (SandboxRunNotices:83).
  • Only the test target has the convenience init(environment:launchReveal:agentPaneEnvironment:) (Tests/CmuxNextAppTests/AppServices+TestKeyWindow.swift). It passes { NSApp?.keyWindow } and { NSApp?.isActive ?? false }. These are optional reads: a test that made NSApplication.shared (197 files do) or runs in the GUI lane, where a window can really be key and the app active, behaves as before. A test with no application object gets no key window and an inactive app. No call site can trap, and no test call site changed.
  • Other NSApp uses changed (same lines, no behavior change):
    • Quit/QuitCoordinator.swift:105 and :135: NSApp.isActive and NSApp.activate() become sender.isActive and sender.activate(). sender is the NSApplication that AppKit passes to shouldTerminate.
    • Quit/QuitCoordinator.swift:245 (the quit sheet's window) and AppServices+AutoArchiveHost.swift:13 (the auto-archive toast): NSApp.mainWindow becomes NSApp?.mainWindow.
  • NSApp uses left. I scanned every NSApp. use in CmuxNextApp and traced what AppServices builds, starts and subscribes. None of these is reached from AppServices construction, the tasks, subscriptions and timers it starts, or the quit step bodies. Each runs only from a person's action, an AppKit event or delegate call, a debug socket command, or code that AppDelegate installs:
    • AppDelegate: AppDelegate.swift (the real sources), MainMenu.swift, HangReports/WatchdogReporting.swift, Control/AppControl.swift:184 (a debug call on the control socket).
    • Debug commands: Control/Debug*.swift, Focus/DebugFocus.swift, Focus/DebugKey*.swift, InputVerification/DebugMouse.swift, InputVerification/DebugNativeInput.swift, Settings/DebugSettings.swift, HostPressure/HostPressureIndicatorView+Debug.swift, RemoteView/RemoteBrowserPages.swift:257, Windows/WindowInvariants.swift, DebugNowPlaying.swift, Control/CaptureSceneRegistry+HomeAgents.swift, Control/UpdateHarnessProbe.swift (an update-harness run only).
    • AppKit events and delegate calls: Focus/ key and event handling (KeyRouter+NewTabInput, CreationInputCoordinator, FocusEffectApplier, WindowScopedShortcutHintModifierMonitor, Keybindings/KeybindingsPageProvider), FocusCoordinator.swift:266 (NSApp.currentEvent only after (NSApp as? CmuxApplication)?.isHandlingSendEvent() == true), BrowserHost/AppBrowserHost.swift:184, TerminalHostDelegate.swift:123, IconPicker/IconPickerService+BookmarkAnchors.swift, Windows/WindowController.swift:351 (windowDidResignKey), drag sessions (Drag/, Screens/ScreenDragSession.swift).
    • A person's action: handlers in Handlers/ (WindowHandlers, WindowTargeting, OnboardingHandlers, KeymapHandlers, CLIInstallHandlers, IconHistory, NowPlayingCardHandlers, TabIconHandlers, BrowserDownloadsPopup), import and onboarding sheets (Onboarding/, Bookmarks/BookmarkBrowserImport.swift, ChiefMemoryImport/), Cloud/CloudMove.swift:340, Density/DensityController.swift, Hints/WindowHintController.swift, Pins/PinCommands.swift, Remote/AddDeviceServices+App.swift, Server/ServerMenuBarController.swift, SessionTransfer/, SleepyMode/, WebInspection/WebInspectorTargets.swift, Windows/WindowController.swift:405 (a close), Control/AppBrowserPasskeyHost.swift (a page's passkey request), Notifications/HandoffNotices.swift:208 (a banner click).
    • Update relaunch behind another app only: Focus/InputVerificationService+NoActivateGuard.swift, Focus/NoActivateKeyboardGuard.swift, Windows/WindowManager+FullScreen.swift:23 (behind relaunchBehind == nil ||).
    • Optional casts: AppServices.swift:615-693 (NSApp as? CmuxApplication)?; :635 reads NSApp.keyWindow inside a closure that is set only when NSApp is a CmuxApplication.

Not in this PR: #28275 adds QuitCoordinator.steps() and the replaceable stopEngines. This PR does not copy them. #28275's own quit tests cover the quit steps end to end.

Ratchet delta

python3 scripts/cmux-next/arch_counts.py --delta origin/feat-cmux-next HEAD: app_lines -3 {'CmuxNextApp': -3}, every other row +0. The new lines are paid for by removing FilePageService.openEmpty(in:focus:) (Pages/Files/FilePageService.swift), which has no caller. No open fix/arch-rise-* branch changes that file.

Also changed: PageFactory.homeChannelsEditRoute(run:) (added in 2ede214) had no production caller, because the Channels page built its edit route inline, so HomeChannelsComposerEditTests tested a copy. Now homeChannelsEditRoute() builds the route and gives back its PageEditProvider. homeChannelsWebPage uses it and sets run once its view exists, and the test calls the same builder. arch-ratchet.py --base origin/feat-cmux-next --head HEAD: no rise.

Testing

Regression suite AppServicesKeyWindowSourceTests (Tests/CmuxNextAppTests/AppServicesKeyWindowSourceTests.swift):

  • keyWindowLookupsWithNoApplicationObjectDoNotTrap: builds AppServices with the test init and calls keyWindowSource(), windows.active, keyWindowRole and the notifications' viewedTabs and isViewed. It checks only that they return: on a GUI lane an earlier test can leave a real key window. It is the only test that is red-first in history (ef26e0f, before the fix; that version also expected nil values). On the base it traps at AppServices.swift:87. The fleet runs a suite alone in its own process (swift test --filter), where no application object exists.

  • everyLookupAndTheStartedTasksAskTheInjectedSources: this test came with the fix, because it uses the new init parameters and cannot compile on the base. It passes recording sources and checks:

    • windows.active and keyWindowRole each ask the key-window source once.
    • viewedTabs and isViewed ask the app-active source (else it fails at once and does not wait).
    • Then it awaits the next ask, which only the started viewed-program-status task can make (FirstOutcome, no clock).

    This test also fails on red when an earlier test already set NSApp, where the trap cannot show.

Red-proof ref (NEVER MERGE): fix/arch-rise-appservices-no-nsapp-trap-redproof-4 at d0bb5a7. It is the head minus the source fix. AppServices and FeedService keep their new parameters, because the test target needs them to compile, but read NSApp.keyWindow and NSApp.isActive again. Every other file whose NSApp read the fix changed is back to the base. The Channels edit route builder and the openEmpty removal stay, because they are not part of the fix. The test files are byte-identical to the head.

Fleet:

  • compile-check 26f2055725867054aa419409 at 3da79ee: failed. The test-target init file did not import LaunchReveal (CmuxNextDesign). Every other error in that log followed from it. Fixed in 6f7244f.
  • compile-check 9ea092fbd71a83b6404f3123 at 6f7244f: passed (a superseded head).
  • compile-check 55f6c038093331e9033d2a37 at 3980462: passed (a superseded head).
  • green step 51908255aa866023afb4a028 at 3980462 (a superseded head): both tests passed.
  • compile-check 234ffcea72660558d8487006 at c77f4ea: cancelled (head replaced).
  • compile-check fc9ad33311e78b79c04a19bd at 923b306: passed (a superseded head). Its green step c3687351786c347ba43922b7 and red step 2ac93f448b0e788b871f88c1 were running when the head was replaced.
  • red step fac20119f8a5376ceba7af59 at f2a9c17 (the red ref of 3980462): failed as expected, CmuxNextApp/AppServices.swift:497: Fatal error: Unexpectedly found nil while implicitly unwrapping an Optional value; the suite process ended with signal 5.
  • compile-check bc4dec3367024c2d085eefcf at c45de7b (this head): running. New green and red steps follow after it passes. The green step also runs HomeChannelsComposerEditTests.

Note: 197 of the 199 test files that build AppServices still call _ = NSApplication.shared. Two of them say that they do it for this read: ChiefPendingRemoveTests.swift:31 ("the services read NSApp") and NewTabDropdownPaletteActionsTests.swift:13 ("the palette's context reads NSApp's key window"). Most of the others also make windows or AppKit views, so this PR leaves all of them. The two files without it are BackgroundAccessoryPolicyTests.swift and HostPressureIndicatorTests.swift. They use NSApplication.shared in other ways.

Observed / Not run

Observed: the prepush gate (no new violations), arch-ratchet (no rise), arch_counts --delta (app_lines -3, others +0), the namespace lint (OK), the fleet compile-checks above, the failed compile's error log and the green step at 3980462 (2 of 2 tests passed).

Not run: No test signs in to drive FeedService's presence: the feed reads were checked by reading only. I did not run the full CmuxNextAppTests on the fleet. I did not build or test anything on the local machine (repo rule). I checked by reading, not by a test, that the NSApp uses left are not reachable from construction or the quit steps.

Fleet red / green (same focused suite, final head c45de7b)

  • Red at d0bb5a7 (branch fix/arch-rise-appservices-no-nsapp-trap-redproof-4, never merged: AppServices and FeedService read NSApp again, the other fixed reads back at base; test files byte-identical), suite AppServicesKeyWindowSourceTests, job a3809497da2962e79bc694a3: Build complete! (186.04s), then keyWindowLookupsWithNoApplicationObjectDoNotTrap() started and the process trapped: CmuxNextApp/AppServices.swift:502: Fatal error: Unexpectedly found nil while implicitly unwrapping an Optional value (the red ref's { NSApp.keyWindow }), the suite process ended with signal 5. Run without --red (the client refuses it when the red commit adds no test file).
  • Green at c45de7b, job 637d859e576ff4f60b265c0b on aws-m4pro-7: Build complete! (446.20s); AppServicesKeyWindowSourceTests: Test run with 2 tests in 1 suite passed; HomeChannelsComposerEditTests: Test run with 1 test in 1 suite passed.
  • Compile: bc4dec3367024c2d085eefcf passed at this head. Review receipt: landing/reviews/c45de7b4ec715a8213a9f827e0faa89c2d5fa153.md (posted as fix(app): AppServices takes the key window and app activity from the composition root, never the global NSApp #28321 (comment)).

Changelog

none

…n must not trap

AppServices' default key-window source implicitly unwraps the global NSApp, which is nil in a
test process that never made NSApplication.shared; the trap ends the whole CmuxNextAppTests process.
Red on the base: the suite runs alone (swift test --filter), NSApp is nil, and the first lookup traps
at AppServices.swift:87.
…n root, never the global NSApp

AppServices.keyWindowSource defaulted to { NSApp.keyWindow }. NSApp is NSApplication!, nil in a
test process that never made NSApplication.shared, so any test that built AppServices and reached a
key-window lookup (windows.active, keyWindowRole, the quit sheet, ListHandlers) trapped and ended the
whole CmuxNextAppTests process (#28275, #28090 green jobs).

- AppServices.init takes keyWindowSource with no default; AppDelegate (the composition root) passes
  { NSApp.keyWindow }, so production cannot forget it.
- The test target alone has the convenience init with no key window ({ nil }); no test call site changes.
- QuitCoordinator asks the sender (the NSApplication AppKit passed) for isActive and activate(), and
  reads NSApp?.mainWindow for the sheet; the auto-archive toast reads NSApp?.mainWindow: no implicit
  unwrap of the global on a path a test can reach.

Regression test: AppServicesKeyWindowSourceTests (red on the base: traps at AppServices.swift:87).
Compile job 26f2055725867054aa419409 failed: AppServices+TestKeyWindow.swift did not see LaunchReveal
(CmuxNextDesign), so the convenience init did not compile and every test call site fell to the
designated init (missing keyWindowSource). Every other error in that log follows from this one.
…ition root too

The second trap (#28275 green eb20257db2553e2e4a4dd378, NotificationCenterService+ProgramStatusSeen.swift:57):
AppServices.init starts notifications, whose viewed-program-status observation calls viewedTabs, which
read NSApp.isActive. Any test that built AppServices and then awaited (each quit step awaits) trapped.

- AppServices.init takes appActiveSource with no default; AppDelegate passes { NSApp.isActive }.
- The notifications' six reads (viewedTabs, isViewed, the program-status visibility, arrive, the
  hand-off and sandbox-run banners) ask services.appActiveSource().
- The test target's init reads the application only if the test made one (NSApp?.keyWindow,
  NSApp?.isActive ?? false), so tests behave as before and never unwrap the global.
- AppServicesKeyWindowSourceTests: the recording test also checks the app-active lookups and awaits the
  started observation's first ask of the injected source (no clock).
@lawrencecchen lawrencecchen added the risk:normal Risk tier (docs/ci/risk-tiers.md) label Oct 30, 2026
@lawrencecchen

Copy link
Copy Markdown
Contributor Author

Review (independent review subagent, arch-rise): SHIP at 3980462
reviewer-session: 310cca2f-aea4-45bb-a906-470252ffd272
author-session: a261583cabe88fa53
R9: pass
perf claim: none
compile: 55f6c038093331e9033d2a37 passed

PR #28321. headRefOid is equal to the reviewed head. Reviewed by reading the diff against origin/feat-cmux-next; no local build.

Checks:

  • Composition root: AppServices.init requires keyWindowSource: and appActiveSource: with no default. AppDelegate.swift:125 is the only call outside CmuxNextAppTests (git grep over the repo) and passes { NSApp.keyWindow } and { NSApp.isActive }, which are the same reads as the base. The stored properties are set on the first lines of init, before any use of self. The six notification reads (ProgramStatusSeen:57, Lookup:42, ProgramStatus:67, NotificationCenterService:454, HandoffNotices:151, SandboxRunNotices:83) call services.appActiveSource(). In the app, services is set by notifications.start before these paths run, so the ?? false fallbacks do not change behavior.
  • Test-only init (Tests/CmuxNextAppTests/AppServices+TestKeyWindow.swift): it uses NSApp?.keyWindow and NSApp?.isActive ?? false. These cannot trap. Tests that make NSApplication.shared get the same values as before. The init is only in the test target, so production code must pass both sources. This is correct.
  • QuitCoordinator: sender is the NSApplication that AppKit gives to shouldTerminate. Its only caller is AppDelegate.swift:448, so sender.isActive and sender.activate() are the same as before. NSApp?.mainWindow in sheetWindow and autoArchiveToast does not change behavior in the app.
  • Remaining NSApp uses (spot-checked): AppServices.swift:615-693 are NSApp as? CmuxApplication casts, which are safe when NSApp is nil. Line 635 runs only inside a closure set on a CmuxApplication. The only other NSApp use in the quit files is terminateFromRunLoop. Focus, debug and relaunch-behind uses run only from AppKit events or socket commands.
  • Tests: in test 1 on the base, keyWindowSource() (AppServices.swift:87) traps when NSApp is nil. In test 2, the recording sources make it fail on red also when NSApp is set (the key-window ask counts stay 0). The guard ... return stops the test before the await, so a red run cannot hang. On green, the followViewedProgramStatus task is started by notifications.start in init. ObservationStream evaluates emit when iteration starts, and the task can start only after the test's first await. Thus FirstOutcome resolves with no clock. No base test assertion changed.
  • Ratchet: arch_counts.py --delta origin/feat-cmux-next HEAD shows every row +0 (app_lines +0). arch-ratchet.py shows no rise. No new static, singleton, CI job, step or gate.

Notes (they do not block):

  • Line count: to get app_lines +0, the author joined two - Parameter doc comments (AppServices.swift:488-489, now 113 and 144 characters) and wrote one doc comment for two properties (:86). This is a small amount of line golfing. AppServices.swift already has 24 lines longer than 140 characters, so readability is almost the same. But appActiveSource has no doc comment of its own. The new text says that debug.key replaces "them", but DebugKey.swift:120 replaces only keyWindowSource. Also, the old reason "a window only becomes key in a running, active app" was removed.
  • Feed/FeedService.swift:87,127,135 read NSApp.isActive from FeedService. notifications.start builds FeedService, and its accountWatch task reaches startIfSignedIn. This read occurs only when the user is signed in, and the tests that sign in (CloudPaneTests, CloudWorkspaceRestoreTests) make NSApplication.shared. Thus no current test traps there. A follow-up can send feed presence through appActiveSource.
  • keyWindowLookupsWithNoApplicationObjectDoNotTrap expects keyWindowSource() == nil. In a GUI-lane process where an earlier test leaves a real key window, this expectation can fail.
  • The green proof (51908255aa866023afb4a028) and red proof (fac20119f8a5376ceba7af59) had no result at review time.

@blacksmith-sh

blacksmith-sh Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

Found 10 test failures on Blacksmith runners:

Failures

Test View Logs
/tmp/tmp54phx4n2/summary-fail.json (live):/summary-fail.json (live): View Logs
/tmp/tmp9j66kky1/summary-missing.json (live):/summary-missing.json (live): View Logs
[new_workspaces] debug.new_workspace timed 0 of 50 new workspace rows/
[new_workspaces] debug.new_workspace timed 0 of 50 new workspace rows
View Logs
[new_workspaces] the window draws no frames/[new_workspaces] the window draws no frames View Logs
[tabs] debug.tab_switch timed 0 of 40 switches/
[tabs] debug.tab_switch timed 0 of 40 switches
View Logs
[tabs] the window draws no frames/[tabs] the window draws no frames View Logs
main.DeveloperIdAppTests.test_download_prints_the_active_profile_only/
test_download_prints_the_active_profile_only
View Logs
stubs: /System/Library/Frameworks/Security.framework/Versions/A/Security has no stub/
Security has no stub
View Logs
stubs: CoreFoundation.tbd: install-name /System/Library/Frameworks/
CoreFoundation.framework/Versions/A/CoreFoundation, missing 1: ['_CFRetain']/
CoreFoundation, missing 1: ['_CFRetain']
View Logs
stubs: CoreFoundation.tbd: install-name /System/Library/Frameworks/
CoreFoundation.framework/Versions/A/CoreFoundation, missing 1: ['_log2']/
CoreFoundation, missing 1: ['_log2']
View Logs

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

…spares read the injected sources

Review of #28321 (SHIP with gaps at 3980462):
- FeedService read NSApp.isActive in observePresence and startIfSignedIn; its sign-in task starts with
  AppServices, so a signed-in test with no NSApplication trapped. FeedService takes appActive (no default);
  AppServices passes its appActiveSource.
- Re-scan of what AppServices builds and starts: ChiefPetRoutes' appIsActive (made from the notifications
  service), WindowController's first .appActive focus event and NewTabSparePool's key-window reads (started
  after the first frame) now ask services.appActiveSource() / keyWindowSource().
- appActiveSource has its own doc; keyWindowSource's doc says debug.key replaces it alone, with the reason.
- app_lines: the three joined doc comments are split again; paid by removing dead code instead:
  MainMenu.helpGroups, groupedItems and otherItems have no caller since #24615 (2e71847).
- AppServicesKeyWindowSourceTests: the first test checks only that the lookups return (a GUI lane can have a
  real key window); it also calls the notifications' viewedTabs and isViewed.
@cursor

cursor Bot commented Oct 30, 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.

…der, not the dead Help groups

#28266 (fix/arch-rise-glass-close) already removes MainMenu.helpGroups/groupedItems/otherItems for its own
lines; the second PR to land would lose its payment. MainMenu+Placements.swift is back to the base.
PageFactory.homeChannelsEditRoute had no production caller (the page builds its PageEditProvider route
inline); HomeChannelsComposerEditTests now builds that route the way the page does.
@github-actions

github-actions Bot commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

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

Review c45de7b: 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=pr28321 -f ref=fix/arch-rise-appservices-no-nsapp-trap -f platform=macos -f backend_mode=local

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

Shipped in:

…e Channels edit route builder

Delta review of #28321 at 923b306 (FIX):
- InputObservationBuilder.keyWindow read NSApp.keyWindow; AppServices starts input verification, whose
  presentation-change subscription runs it (DEBUG invariant monitor, window geometry). It now takes the key
  window from services.keyWindowSource().
- Re-scan of subscriptions, timers and tasks AppServices starts: KeyRouter's menu gate (keyWindowFocus),
  the refusal HUD's window (AppActionContext, ChatsOpenCoordinator, HomeChiefActions, LinkHandlers,
  ViewerService) ask services.keyWindowSource(); the agent cursor snapshot, a pane's surface diagnostics
  (SurfaceInvariantMonitor) and SheetDismissal.endAll's default read NSApp? (never the unwrapped global).
- HomeChannelsPage: PageFactory.homeChannelsEditRoute() builds the edit route and gives back its provider;
  homeChannelsWebPage uses it and sets run once its view exists, and HomeChannelsComposerEditTests calls it.
- app_lines: paid by removing FilePageService.openEmpty, which has no caller.
@github-actions

github-actions Bot commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

Default changes

This pull request changes 2 defaults at c45de7b4ec71 (what everyone gets without configuring anything):

  • tunable-source.CmuxNextApp/Handlers/LinkHandlers.swift:bind: "44a0cde256dc45567885c1b48adab12a8e63a64b76adb4e976ec915879b3bbc0" -> "2f09768ab7bc0a4e02ad839fb344001861932bf16e72ba07a9b7d28a2809704e"
  • tunable-source.CmuxNextApp/Home/HomeChiefActions.swift:stop: "376723b57ec9a52ea6aabcd92a09c72406b384e8b8742aa8cafc15fb28a4ab93" -> "e080419d4da778a44f611b9a0ff9fd524192d6df8936840895f791397afde02e"

Needs a review. It passes with an independent review receipt on this commit: a comment or review whose first line is Review (<reviewer>): SHIP at c45de7b4ec715a8213a9f827e0faa89c2d5fa153, from a member, not a self-review (the newest verdict naming the head decides; a later BLOCK beats an earlier SHIP), or with an approving review on this commit from @austinywang or @teamleaderleo (the call owners in .github/defaults-call-owners.json, never the author). A receipt posted as a review re-runs this check; after a plain comment, re-run it. The needs a call label alone does not pass, and a new push needs a new receipt.

Merge receipt: merged as e0bc02f198e2 with these defaults changed, WITHOUT a SHIP receipt or an approving call owner review on its head.

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

@lawrencecchen

Copy link
Copy Markdown
Contributor Author

Review (independent review subagent, arch-rise): SHIP at c45de7b
reviewer-session: a788701e-b6cb-47c6-8660-30a4696bb31f
author-session: a261583cabe88fa53
R9: pass
perf claim: none
compile: bc4dec3367024c2d085eefcf passed

Scope: delta review of git diff 923b306dfe0 c45de7b4ec7 (13 files). The two defects from the FIX at 923b306 are repaired.

  1. Channels edit route (defect a: repaired).

    • Base test (origin/feat-cmux-next, HomeChannelsComposerEditTests.swift:16) called PageFactory.homeChannelsEditRoute(run:). No production code called that builder: the page built its route inline. So the test checked a copy.
    • Head test (HomeChannelsComposerEditTests.swift:16-17) calls PageFactory.homeChannelsEditRoute(). homeChannelsWebPage (HomeChannelsPage.swift:42) calls the same builder. The test now checks the production builder.
    • Assertions are the same (base :17-39 = head :18-40): the route prefix admits cmux.app.edit; paste and pasteAsPlainText run on two different gestures; the same gesture again is refused; script with no gesture is refused; selectAll is refused; ran stays [.paste, .pasteAsPlainText]. The test is not weaker.
    • Page behavior is the same: edits.run = { [weak page] ... } is set after PageWebView exists, as on the base. The weak capture stays, so there is no retain cycle (page -> routes -> provider -> run -> weak page).
  2. Key-window reads.

    • InputObservationBuilder (defect b: repaired): observe now passes services.keyWindowSource() to keyWindow(_:_:). Path: AppServices.init -> input.start (AppServices.swift:676) -> presentationChanges.subscribe("input-monitor") (InputVerificationService.swift:46) -> DEBUG settle timer at 200 ms -> check() -> observe. keyWindow has no other caller.
    • KeyRouter+Hooks keyWindowFocus: AppServices.init installs registry.menuKeyEquivalentGate (AppServices.swift:618). It runs when a test validates a key-equivalent menu item during dispatchingSynthetic.
    • Refusal HUD fallbacks (windows.active?.window ?? X): AppActionContext+Targets (refusalObserver, remoteFailureObserver), ChatsOpenCoordinator, HomeChiefActions, LinkHandlers, ViewerService. AppActions.bind(services) installs these (AppActions.swift:135). AppDelegate.swift:136 calls it, and 102 test files call it after they build AppServices. A refused action in such a test, with no window controller, evaluates the fallback. On the base it was NSApp.keyWindow and it trapped. Each read was an unguarded NSApp read that a test process can reach. Now the fallback reads the same source as windows.active.
    • NSApp? reads: AgentCursorSnapshotBuilder.orderedControllers (the lazy agentCursorVisibility; it re-resolves on an observed change after a tracked input), PaneController.visiblePageWindowsOverContent (presentationChanges.subscribe("surface-invariant") at AppServices.swift:644 -> SurfaceInvariantMonitor diagnostics), SheetDismissal.endAll default (QuitCoordinator.shouldTerminate :84 and :62).
    • App behavior: AppDelegate passes { NSApp.keyWindow }. NSApp?.x ?? [] is the same as NSApp.x when NSApp exists. The only change is under debug.key: its temporary keyWindowSource override (DebugKey.swift:119-121) now also applies to the menu gate and to the HUD fallback. This agrees with that override's own comment ("rules that read the key window see it"), and windows.active and WindowKeyTable already used the override. This is a debug-only change and I accept it.
  3. FilePageService.openEmpty(in:focus:): git grep openEmpty HEAD over the whole repo finds no hit. The base had only its definition and a doc reference. show(in:...) still has a caller (:144). Of all origin/fix/arch-rise-* branches, only this PR and its redproof-4 ref change FilePageService.swift.

  4. PR body: I scanned every unguarded NSApp use in CmuxNextApp at the head again. Each file is in the "NSApp uses left" categories. AppServices.swift has one hit, :635, inside the optional-cast closure.

  5. Ratchet: arch_counts --delta origin/feat-cmux-next HEAD: every row +0, app_lines -3 (CmuxNextApp). arch-ratchet --base origin/feat-cmux-next --head HEAD: "no architecture count rises". The delta adds no static var/let and no .shared. It adds no CI job, step or gate. The 131-character doc line (HomeChannelsPage.swift:32) is acceptable: the file already has 15 lines longer than 120 characters.

Notes that do not block:

  • The PR body puts the five refusal-HUD sites under "reads that AppServices or the tasks, subscriptions and timers it starts reach". More exactly, AppActions.bind installs them (AppDelegate and 102 test files call it), and an action run reaches them. The code is correct. Only the wording is inexact.
  • The fleet red-4 and green steps at this head were still running at review time. This receipt relies on compile bc4dec3367024c2d085eefcf only.

@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 30, 2026
@lawrencecchen
lawrencecchen merged commit e0bc02f into feat-cmux-next Oct 31, 2026
216 of 238 checks passed
@lawrencecchen
lawrencecchen deleted the fix/arch-rise-appservices-no-nsapp-trap branch October 31, 2026 00:34
lawrencecchen added a commit that referenced this pull request Nov 1, 2026
…eat's #28321 test init replaced the one QuitStateFilesTests called)

Suites step 4e05c54c690b at 729a0b7: QuitStateFilesTests.swift:33 passed
agentPaneStateFiles to feat's test-only convenience init, which had no such
argument.
lawrencecchen added a commit that referenced this pull request Nov 1, 2026
…n thread on them (#28275)

* test(quit): a second quit signal arms its deadline before it waits on the agent panes' state files

The forced quit (ForcedQuitExit, a second SIGTERM/SIGINT/SIGHUP while a quit runs) flushed the agent
panes' state files in `prepare`, under allowBlocking(.terminate), before its 160 ms deadline was armed:
with a save queued behind a busy disk the exit waited up to 2 s on the main thread, and its own deadline
did not bound that wait.

This commit extracts the exit's steps into AppDelegate.secondSignalExit (same steps, injectable deadline
and end) and adds QuitStateFilesTests.theForcedQuitArmsItsDeadlineBeforeItWaitsForTheStateFiles. It is
red on this commit: the deadline is armed about 2 s after the start, and the exit ends before the queued
draft is on disk. The fix is the next commit.

Not run here (no Swift build on this VM): the fleet runs it.

* fix(quit): state files have one owner each and no quit blocks the main thread on them

Rise audit (arch-ratchet tip rises with no reviewed exception) of cmux#27749 and #25535:

- AgentPaneStateFile's process registry (`static let files = Mutex<...>`) becomes the
  AgentPaneStateFiles instance that AppServices owns and passes to AgentUnsentDrafts and
  AgentPaneScrollAnchors (composition root, PATTERNS.md). Same one-writer-per-path rule.
- MobileHostPairedDevicesFile's `static let queue` and `static let inForce` become members of one
  file object that IPhoneHostService owns per state directory (it built a new value per use before).
  This run's record of the list in force is now per file object; a relaunch is a new object.
- The three allowBlocking(.terminate) flushes are gone:
  - ForcedQuitExit `prepare` (AppDelegate): the flush ran before the 160 ms deadline was armed, so a
    second quit signal could wait 2 s. The exit now arms the deadline first and awaits the state files
    beside the Home cache's write in `save`.
  - applicationWillTerminate (AppDelegate) and IPhoneHostService.stop(): up to 2 s each, 4 s in all.
    The quit's `.terminateLater` phase (QuitCoordinator) now awaits AppServices.stateFilesDrained() in
    prepareWindows and again in stopBrowserEngines, next to home.flushCacheForQuit(): one 2 s limit
    for all the files, holding no thread.
- QueueDrain (CmuxNextWakeups): waits for serial queues with one continuation and one DemandTimer
  deadline, so no thread blocks and a quit with no backlog costs no timer fire.
- SignalRelay moves from CmuxNextApp/Quit to CmuxNextControl/Diagnostics (beside ForcedQuitExit and
  QuitDeadline), so CmuxNextApp's lines do not rise.

Ratchet (arch_counts --delta): global_mutable_statics -3, blocking_allow_escapes -3, app_lines -31.

Regression: QuitStateFilesTests.theForcedQuitArmsItsDeadlineBeforeItWaitsForTheStateFiles is red on
the previous commit and should be green here; theQuitAwaitsAQueuedDraftWithoutBlockingTheMainThread
covers the normal quit's wait. Not run here (no Swift build on this VM): the fleet runs them.
Owners told: Performance Lead (Wakeup primitives, statics), PL-threads (the terminate allowance).

* refactor(quit): the quit's steps and its engine stop are QuitCoordinator members

`steps()` builds the QuitSteps that `complete` runs, and `stopEngines` is the engine stop (the
deadline, then CefShutdown) that stopBrowserEngines ends with. Same steps in the same order; a test
can now run a step through the coordinator with its own engine stop. No behavior change.

* test(quit): a quit through QuitCoordinator waits for a queued draft before it goes on

QuitStepsStateFilesTests runs the coordinator's own steps with a new chat's draft save queued behind
a busy disk (a held file queue, released after 1 s):
- theSaveStepEndsWithTheQueuedDraftOnDisk: prepareWindows returns with the draft on disk.
- theEnginesStopAfterTheQueuedDraftIsOnDisk: stopBrowserEngines reaches the engine stop with the
  draft on disk.
Each fails when its step's `await services.stateFilesDrained()` is removed (the red-proof ref).
Not run here (no Swift build on this VM): the fleet runs them.

* refactor(wakeups): QueueDrain races its queues and its limit through FirstOutcome

One path for "first of an event and a DemandTimer limit wins" (FirstOutcome): QueueDrain keeps only
the count of queues still to answer; its own continuation and countdown class are gone.

* test seam(quit): AgentPaneStateFiles tells when a drain begins

`AgentPaneStateFiles(drainBegan:)` is told each time `drained(within:)` begins, before it waits;
AppServices takes the registry through its init (default: a plain one). A test holds a file's queue
and releases it only there, so only a real drain lets a quit step go on: no wall-clock hold decides a
test (lead rule R5). `stateFilesDrained` returns whether every file's saves ran in time.

* test(quit): only a drain of the state files lets a held save go; no clock decides the quit tests

The quit tests held a file's queue and let it go on a timer (1 s), or measured wall time, so a slow
test host could decide them (lead rule R5). DrainGate holds the queue until the registry is told a
drain began (`AgentPaneStateFiles(drainBegan:)`), and records the order of the quit's steps:
- QuitStepsStateFilesTests (both): a step that never awaits the state files never writes the draft,
  and the gate saw no drain.
- theForcedQuitArmsItsDeadlineBeforeItWaitsForTheStateFiles: the order is "deadline armed", then
  "drain began", and the exit ends with the draft on disk.
- theQuitAwaitsAQueuedDraftWithoutBlockingTheMainThread: the main actor lets the held save go once
  the drain began; a wait that blocked it would reach its limit with the draft still queued.

* test(quit): the quit tests' services have no key window (no app object in the test process)

The green step 04224ccd at 07f1b95 crashed both quit suites before any assertion:
AppServices.keyWindowSource reads `NSApp.keyWindow`, and `NSApp` is nil in a test process that never
made NSApplication.shared; the quit steps ask for the key window (WindowManager.swift:143). The
tests' services answer nil instead, with no app object.

* test(quit): DrainGate and its log are nonisolated (the drain calls them off the main actor)

The compile a1714da9 at d77dbff refused QuitStateFilesTests.swift:85: the drainBegan closure runs
in the drain, off the main actor, and called DrainGateLog.append, main-actor by the test target's
default isolation. Both helper classes are nonisolated now (a Mutex and semaphores inside).

* test(quit): the quit tests make the app object first, and the non-blocking wait has a time limit

The green step eb20257d at 57dfaa1 crashed both quit suites before any assertion: a quit step
reads `NSApp.isActive` (NotificationCenterService+ProgramStatusSeen.swift:57), and `NSApp` is nil in
a test process with no app object. The one helper makes NSApplication.shared first, as the other
AppServices tests do; the root fix of those reads is fix/arch-rise-appservices-no-nsapp-trap.
theQuitAwaitsAQueuedDraftWithoutBlockingTheMainThread gets a 1 minute time limit, so a wait that
never ends fails the test instead of the run.

* fix(quit): no bare drain limit; the last drain follows the engines' close; a lost draft is named and told

R9 review of #28275 (reviewer 0053c0be):
- R-L1 (bare 2 s): the state files' drain has no limit of its own. The quit awaits it only after its
  deadline is armed: QuitCoordinator's engine stop now arms QuitDeadline.engineStopLimit (15 s), drains,
  closes the engines (`closeEngines`, a seam for tests) and drains again, so a page that closes and
  saves its draft (the composer's page-hide flush) is on disk when the quit ends. The forced quit
  awaits it under its 160 ms deadline. The prepareWindows drain is gone (the engine stop covers it).
  QueueDrain is deleted: each file awaits its own queue (`AgentPaneStateFile.saved()`).
- R-L5 (silent loss): when a quit deadline ends the process while a state file still saves, its end
  (`AgentPaneStateLoss.deadlineEnd`) logs a fault naming the files (no content) and records them in
  `<state>/agent-state-lost.json`; the next launch takes the record and, for a new chat's unsent text,
  shows a toast ("Text typed in a new chat just before cmux quit was not saved.", 21 locales). Both
  deadline reasons name the state files.
- Tests (no clock decides any): QuitStepsStateFilesTests (the engines close after the queued draft is
  on disk; a draft saved while the engines close is on disk when the quit ends), AgentPaneStateLossTests
  (the deadline records the files still saving, the next launch takes them once, the notice is only for
  a lost draft), AgentPaneStateFileTests (a file is saving until its saves ran).
Not run here (no Swift build on this VM): the fleet runs them.

* fix(quit): a paired-phones save a quit deadline cuts off fails closed, is named and told

R9 follow-up on #28275: with the drain's 2 s limit gone, a paired-phones save the quit deadline cut
off was silent, and an unsaved removal kept the old list's seal, so a phone the person removed counted
again after a relaunch.
- MobileHostPairedDevicesFile counts its queued and running saves and withdrawals (`isSaving`), and
  `failClosedIfSaving()`, called on the deadline's thread, stops every list counting in this run and
  clears the seal when one is still saving: after a relaunch the stored list no longer matches its
  seal, so every phone pairs again (the digest rule of `load`). A save that still finishes before the
  process ends records its own new list.
- MobileHostPairedQuitWatch, owned by IPhoneHostService and set whenever its file changes, gives the
  deadline's end that file from any thread. AppServices.quitDeadlineEnd() builds the one end both
  deadlines use: it fails the paired list closed, logs a fault naming every file still saving (names
  only), and records them in agent-state-lost.json.
- The next launch logs a cut-off paired list and shows a second toast: "A change to your paired iPhones
  was cut off when cmux quit. Pair your iPhones again." (21 locales).
- Tests (no clock decides): MobileHostPairingTests.aRemovalCutOffByAQuitDeadlineIsNotTrustedAfterARelaunch,
  AgentPaneStateLossTests.aCutOffPairedListIsRecordedAndTold,
  QuitStateFilesTests.theLossRecordNamesThePairedListByItsFileName.
Not run here (no Swift build on this VM): the fleet runs them.

* fix(quit): the loss notice uses the shared toast center inside (its type is not public API here)

The compile 6c6043c8 at c85dcca refused AgentPaneStateLoss.showNotice: its `toasts:` parameter
default named CmuxToastCenter, an internal import of CmuxNextAgentPane, in a public method. The
paired-phones test waits for the held clear in a synchronous helper, not in its async body.

* fix(quit): the deadline's end never calls the Keychain; the next launch clears a cut-off paired seal

R9 review of #28275 at 96a1de8 (R-L5): the deadline's end cleared the paired list's seal
(SecItemDelete) on the deadline thread before `_exit`. A hung securityd, the case QuitDeadline exists
for, would hold the process forever, and the fault and the record came after it.
- The deadline's end only marks a paired-phones save still in flight (`paired-devices.cut-off` beside
  the file, `MobileHostPairedDevicesFile.markCutOffIfSaving()`), then logs, records and ends.
- The next launch's file object clears the seal first on its own queue, before any read
  (`distrustIfCutOff()`): no stored list counts in that run, and after it the list no longer matches
  its seal, so every phone pairs again. A Keychain that refuses the clear keeps the list refused for
  the run and the marker for the next launch (a fault).
- The pair-again toast shows only when this launch cleared the seal (`sealClearedForCutOff()`).
- The second drain's comment says it catches the page-hide flush of Chromium-hosted agent pages.
- Test: MobileHostPairingTests.aRemovalCutOffByAQuitDeadlineIsNotTrustedAfterARelaunch now checks the
  deadline's end leaves the seal untouched while a removal is held before its own clear, and that the
  next launch clears the seal before its first read and trusts no phone. No clock decides it.
Not run here (no Swift build on this VM): the fleet runs them.

* fix(quit): a cut-off paired save fails closed even when its marker is not written; notices never wait on each other

R9 review of #28275 at 67afa2c (two silent-loss notes, R-L5):
- The deadline's end ignored a failed marker write and still named the paired file, so on a full disk
  a removed phone's old list counted again after a relaunch with no notice. Now a failed write is a
  fault, and the record names the file with the note "paired-devices.cut-off (not written)"
  (`MobileHostPairedDevicesFile.CutOff.unmarked`). At the next launch the paired file fails closed when
  its marker exists OR the last quit's record names it (`lastQuitCutOff`, asked on its queue before any
  read); a refused seal clear then writes the marker for the launch after it. The record is read once
  per launch (`AgentPaneStateLoss.lastQuitNames()`), so the paired check and the notice share it in
  either order.
- The launch awaited the paired seal check (a Keychain call) before it showed the unsent-draft notice.
  `showNotices` shows the draft notice at once and the pair-again notice when its own check answers.
- Tests (no clock decides): MobileHostPairingTests.aCutOffRemovalWithoutItsMarkerIsNotTrustedAfterARelaunch
  (an injected disk refuses the marker), the deadline-end test counts every seal call (zero during the
  end), AgentPaneStateLossTests.thePairedCheckAndTheNoticeShareOneReadOfTheRecord.
Not run here (no Swift build on this VM): the fleet runs them.

* fix(quit): no public default names an internally imported symbol; the launch notice takes a closure

The compile c822c59f at fc54bef refused MobileHostPairedDevices.swift:198: the public init's
`writeMarker` default called `blockingWrite` (an internal import of CmuxNextCompat). The parameter is
optional now and the init picks the default inside its body. AppDelegate passes the paired seal check
to `showNotices` as a closure literal, not as a partially applied main-actor method.

* fix(quit): a launch told only by the record keeps the cut-off fact on disk until its seal clear ends

R9 review of #28275 at e22c234 (R-L5): the record is read once and removed, so on the record-only
path (the marker write failed at quit) a launch whose seal clear hung, then a quit or crash, left the
launch after it with no record, no marker and the old seal: the removed phone was trusted silently.
- When only the record names the cut-off save, `distrustIfCutOff` writes the marker BEFORE the clear;
  a failed write is a fault of its own.
- The clear counts as a save in flight (`saving`), so a quit deadline during a hung clear marks and
  names the file again.
- A refused clear keeps the marker for the next launch (its fault says so).
- Test (no clock decides): MobileHostPairingTests.aRecordOnlyLaunchWhoseClearHangsStillLeavesTheListUntrusted:
  a record-only launch whose clear is held; a deadline during it finds `.marked`; a third file object
  with no record trusts no phone. aCutOffRemovalWithoutItsMarker... drains its first file object's
  launch step before the marker can exist.
Not run here (no Swift build on this VM): the fleet runs them.

* fix(quit): the record keeps naming a cut-off paired list until the fact is safe; no clock in the removal tests

R9 review of #28275 at d6f29ce (R-L5): the launch removed the record at its first read, so when
the launch's own marker write failed too, the fact lived only in that process: a refused clear (then a
normal quit) or a hung clear (then a crash) left the next launch with no record, no marker and the old
seal, and the removed phone was trusted silently.
- AgentPaneStateLoss keeps naming the paired list in the record after the launch reads it (only that
  name: a lost draft is told once), a quit deadline in that run writes the name beside its own, and
  `pairedCutOffSettled()` removes it.
- MobileHostPairedDevicesFile tells `cutOffSettled` once the fact is safe without the record: its
  marker is on disk (written at launch before the clear) or the seal is cleared. IPhoneHostService
  takes the app's record (`pairedQuitRecord`) for both questions.
- The refused-clear fault says what keeps the fact: the marker, or the record when no marker exists.
- R5: aRemovalTakesTheOldDigestAwayAfterTheSavesBeforeIt and aRemovalNeverWaitsOnTheCallersThread
  release their held save on a signal (after the removal is queued), not after 0.3 s / 0.6 s; the
  second no longer measures time: a removal that waited on the caller's thread would never return,
  and its time limit fails it.
- Tests (no clock decides): MobileHostPairingTests.aRecordOnlyLaunchWithNoMarkerAndARefusedClearKeepsTheRecord,
  AgentPaneStateLossTests.theRecordKeepsNamingACutOffPairedListUntilItIsSettled.
Not run here (no Swift build on this VM): the fleet runs them.

* fix(quit): quitSteps takes the quit's choice (endLocalSessions ends with it); PhoneAdmissionStoreTests need no forgetThisRun

Fleet compile 6ade1fafcc18 at b062435: QuitCoordinator.swift:215 had no
'choice' in quitSteps(), and two feat tests called the static per-path
forgetThisRun, which the per-file inForce record replaces: the restored store's
new list file remembers no digest, as a relaunch does.

* test(quit): the test AppServices initializer takes the state files (feat's #28321 test init replaced the one QuitStateFilesTests called)

Suites step 4e05c54c690b at 729a0b7: QuitStateFilesTests.swift:33 passed
agentPaneStateFiles to feat's test-only convenience init, which had no such
argument.

* test(drafts): feat's UnsentDraftTests share one AgentPaneStateFiles per test (the per-app writer registry)

Compile 7b999930c9b2 at 0ff4259: UnsentDraftTests.swift built
AgentUnsentDrafts(file:) with no files. Each test now gives its instances one
AgentPaneStateFiles, so a fresh instance reads after the queued save as the
static registry made it do. A grep of the package finds no other caller of a
changed initializer without its new argument.

* fix(quit): the state-file drain before the engines close is held to the time they do not need

Review of e79b754 (comment 6554069134): an unbounded drain before
closeEngines could use up feat's engineCloseWait (a hung Keychain call on the
paired-phones queue, a loaded disk), so CefShutdown lost its flush. The first
drain is now a QuitStepLimiter step limited to the deadline's time left less
the reply margin and engineCloseWait; past it, it runs beside the close and the
quit awaits it in finishOverruns. The drain after the close is unchanged.

This branch was previously deployed

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