Skip to content

Guard performKeyEquivalent keyDown force-dispatches against replay loops - #5891

Merged
austinywang merged 4 commits into
mainfrom
issue-5887-keydown-replay-loop
Jun 11, 2026
Merged

austinywang merged 4 commits into
mainfrom
issue-5887-keydown-replay-loop

Conversation

@austinywang

@austinywang austinywang commented Jun 11, 2026 •

Copy link
Copy Markdown
Contributor

Crash

cmux NIGHTLY 0.64.14 on macOS 26.4.1 crashed with "Thread stack size exceeded due to excessive recursion": 68,812 main-thread frames (incident 96E09E5C-19CC-49D4-B068-7A666CE784A9). A key pressed while a browser pane was focused on non-editable content looped forever between NSWindow.cmux_performKeyEquivalent and CmuxWebView.keyDown: WebKit replays the unhandled key through the responder chain, macOS 26 -[NSWindow keyDown:] re-enters performKeyEquivalent, and the swizzle force-dispatched the same event back into the web view with no replay guard.

Fixes #5887

Repro evidence

Reproduced on a debug build (r5886, current main) via the debug socket: focus a browser pane on https://example.com (web content first responder, non-editable), send Option+A. The printable-Option-text bypass force-dispatches with no guard; the app logged 1,150 performKeyEquiv: Opt+'a'(0) fr=CmuxWebView lines in 30 ms and died with the same "Thread stack size exceeded" signature (incident C9470E41-11A7-4A04-874F-C8AE5DF1CA06). Plain Return survives on main only because that one branch had a hand-rolled depth counter; the Option-text bypass, ghostty zoom, stale-menu bypass, and menu-miss dispatches had none.

Fix

One shared chokepoint, cmuxForceDispatchKeyDownOnce, now performs every direct keyDown(with:) force-dispatch in cmux_performKeyEquivalent. It tracks the identity of each event whose dispatch is on the stack (window number, type, keyCode, modifiers, timestamp; inserted before keyDown, removed via defer) and declines to dispatch the same event twice, so the caller falls through to default AppKit handling. The seven per-branch forwarding-depth counters are replaced by this one mechanism, which also covers cross-branch ping-pong they could not see (the first responder can change while a dispatch is in flight).

Composition with the parallel fix on main

While this PR was in review, #5899 landed on main and fixes the webview path of the same crash: CmuxWebView wraps WebKit keyDown dispatch in an is-active flag, and three browser branches of cmux_performKeyEquivalent return early when a webview is first responder during that dispatch. This branch merges main and keeps those guards intact.

What this PR still adds: those early-returns only fire for webview first responders, so the same-event replay loop through any other responder, and the ghostty font-zoom, stale-menu, menu-miss, command palette, text-box, omnibar, and editable-text-view dispatch sites, remained unguarded. All 13 force-dispatch sites now route through cmuxForceDispatchKeyDownOnce. Proof of the residual hole: WindowKeyDownReplayGuardTests fails on current main even with #5899 (run at 37df40d, which is main plus only the test), and passes on this branch's merge head dbae247. Both runs on a macOS 26 builder.

Principled rather than a branch patch: the invariant "never force-dispatch the same in-flight event twice" is enforced at the single point where force-dispatches happen, for present and future branches. Stack-scoping preserves WebKit's legitimate single replay of unhandled keys (it arrives after the original dispatch unwinds), key autorepeat produces distinct timestamps so repeat typing is never throttled, and the window number keeps multiple windows independent.

Test

WindowKeyDownReplayGuardTests drives the real chokepoint: a window whose first responder re-invokes performKeyEquivalent with the same event from keyDown (bounded, so the pre-fix failure is a clean assertion rather than a stack overflow). Commit 1 adds the failing test only, commit 2 adds the fix. Note on CI: the GitHub tests job stayed green at the test-only commit because the app-host unit suite crashed and hit its 900s timeout before reaching this class and the job still passed (tracked in #5903). The deterministic red/green proof ran on a macOS 26 builder: WindowKeyDownReplayGuardTests fails at b655fed and passes at c4f61dd. Two companion tests pin the non-regression behavior: distinct events each dispatch (autorepeat), and the same event dispatches again once the prior dispatch has unwound (the legitimate WebKit replay).

🤖 Generated with Claude Code


View with Codesmith Autofix with Codesmith
Need help on this PR? Tag /codesmith with what you need. Autofix is disabled.


Summary by cubic

Stops a stack-overflow crash on macOS 26 by guarding performKeyEquivalent keyDown force-dispatches so the same event isn’t re-dispatched while in flight. Fixes #5887 and adds regression tests.

  • Bug Fixes

    • Route all keyDown(with:) force-dispatches through NSWindow.cmuxForceDispatchKeyDownOnce.
    • Track event identity (window, type, keyCode, modifiers, timestamp); if in flight, skip and fall through to default AppKit handling.
    • Preserve WebKit’s legitimate replay (after unwind) and key autorepeat; guard is per-window.
    • Replace per-branch depth counters and cover previously unguarded paths (printable Option text, terminal zoom, stale‑menu/menu‑miss, browser arrows/Return, omnibar marked text, command palette, text‑box input, editable text).
    • Add WindowKeyDownReplayGuardTests for once-per-event dispatch, distinct events (autorepeat), and replay-after-unwind.
  • Refactors

    • Move the replay guard into Sources/App/WindowKeyDownReplayGuard.swift and wire into the project; no behavior change.

Written for commit dbae247. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • Bug Fixes

    • Adds a unified guard to prevent duplicate/re-entrant keyboard dispatch so shortcuts, printable Option-text, arrow keys, Enter, and related navigation behave consistently and avoid unintended repeated actions.
  • Tests

    • Adds regression tests covering key-down replay and re-entrancy scenarios to ensure reliable keyboard dispatch and correct behavior across edge cases.

austinywang and others added 2 commits June 11, 2026 06:57
NSWindow.cmux_performKeyEquivalent force-dispatches certain key events
straight into the focused responder's keyDown. When the responder does
not consume the key, AppKit routes the same event back into
performKeyEquivalent while the first dispatch is still on the stack
(WebKit replays unhandled keys, and on macOS 26 -[NSWindow keyDown:]
re-enters performKeyEquivalent). The printable-Option-text bypass has no
re-entry guard, so the event ping-pongs between the swizzle and the
responder until the main-thread stack overflows.

The test drives the real chokepoint: a window whose first responder
re-invokes performKeyEquivalent with the same event from keyDown,
bounded so the pre-fix failure is a clean assertion instead of a crash.
It asserts the force-dispatch happens exactly once per event, that
distinct events (autorepeat) still each dispatch, and that the same
event may dispatch again once the prior dispatch has unwound (WebKit's
legitimate replay).

Repro of #5887: Option+A with
a browser pane focused on non-editable content crashes with "Thread
stack size exceeded due to excessive recursion" (incident
C9470E41-11A7-4A04-874F-C8AE5DF1CA06 on a debug build, mirroring
incident 96E09E5C-19CC-49D4-B068-7A666CE784A9 from cmux NIGHTLY
0.64.14).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…y loops

Replace the seven per-branch forwarding-depth counters in
NSWindow.cmux_performKeyEquivalent with one shared chokepoint,
cmuxForceDispatchKeyDownOnce. The helper tracks the identity
(window number, event type, keyCode, modifiers, timestamp) of every
key event whose force-dispatch is currently on the stack and refuses
to dispatch the same event a second time, returning false so the
caller falls through to default AppKit handling.

This closes the unguarded printable-Option-text bypass that crashed
cmux NIGHTLY 0.64.14 (#5887):
WebKit replays an unhandled key through the responder chain, macOS 26
-[NSWindow keyDown:] re-enters performKeyEquivalent, and the bypass
force-dispatched the same event back into CmuxWebView.keyDown forever
until the main-thread stack overflowed. It also guards the previously
unguarded ghostty zoom, stale-menu-bypass, and menu-miss keyDown
dispatches, and protects against cross-branch ping-pong that
per-branch counters cannot see (the first responder can change while
a dispatch is in flight).

The guard is stack-scoped (insert before keyDown, remove via defer),
so WebKit's legitimate single replay of an unhandled key, which
arrives after the original dispatch has unwound, still force-dispatches
normally. Key autorepeat produces distinct events with fresh
timestamps, so repeat typing is never throttled, and the dispatching
window's number is part of the identity so multiple windows cannot
suppress each other.

Fixes #5887

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Jun 11, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cmux Ready Ready Preview, Comment Jun 11, 2026 7:40pm
cmux-staging Building Building Preview, Comment Jun 11, 2026 7:40pm

@coderabbitai

coderabbitai Bot commented Jun 11, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a unified in‑flight forced‑dispatch identity guard in AppDelegate to prevent re‑entrant keyDown replay loops, updates multiple forwarding sites to use the guard, and adds regression tests plus Xcode project wiring verifying exactly‑once forced dispatch per in‑flight event, independent dispatch for distinct events, and re‑dispatch after unwind.

Changes

Key-event replay guard and tests

Layer / File(s) Summary
Test class infrastructure and helpers
cmuxTests/WindowKeyDownReplayGuardTests.swift
Defines WindowKeyDownReplayGuardTests with nested ReplayingKeyDownView that replays via window?.performKeyEquivalent(with:), plus factories for creating test windows and Option+A NSEvent instances.
Replay guard behavior assertions
cmuxTests/WindowKeyDownReplayGuardTests.swift
Three tests assert: printable Option+A is force-dispatched exactly once per in‑flight event; distinct events are each force-dispatched; the same event can be force-dispatched again after the prior dispatch unwinds.
Core in‑flight identity guard and tracking
Sources/App/WindowKeyDownReplayGuard.swift, Sources/AppDelegate.swift
Introduces CmuxForceDispatchedKeyEventIdentity, a main‑thread in‑flight Set, cmuxFirstResponderGuard* context variables, and implements NSWindow.cmuxForceDispatchKeyDownOnce(_:to:reason:) as the guarded forced‑dispatch chokepoint.
Forwarding updates (per‑route replacements)
Sources/AppDelegate.swift
Replaces per‑feature reentry‑depth guards with cmuxForceDispatchKeyDownOnce(...) across many key‑forwarding routes and preserves prior fallback semantics where appropriate.
Project file registration
cmux.xcodeproj/project.pbxproj
Adds PBXFileReference, PBXBuildFile, group membership, and sources‑build‑phase entries to include the new source and test files in the cmux and cmuxTests targets.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • manaflow-ai/cmux#5899: Also adjusts keyboard routing and reentry‑guard approaches around AppDelegate forwarding paths.
  • manaflow-ai/cmux#4186: Modifies performKeyEquivalent/keyDown routing and replay suppression logic in AppDelegate.
  • manaflow-ai/cmux#3981: Related changes around printable Option+character routing and keyDown forwarding.

Suggested reviewers

  • jesstelford

Poem

🐰 I hopped through event loops, nose to the stack,

Counting each key so none would bounce back,
One dispatch per flight, then unwind — set free,
The window listens, the responder sings,
A gentle guard keeps loops from breaking things.

🚥 Pre-merge checks | ✅ 20 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (20 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main change: adding a guard to prevent keyDown force-dispatch replay loops in performKeyEquivalent.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Cmux Swift Actor Isolation ✅ Passed New guard globals/contexts are confined to main-thread event-dispatch methods (cmux_sendEvent / cmux_performKeyEquivalent); no added async/service protocols, Sendable shared mutable refs, or backgr...
Cmux Swift Blocking Runtime ✅ Passed Checked Sources/App/WindowKeyDownReplayGuard.swift and cmux_performKeyEquivalent logic in Sources/AppDelegate.swift for semaphores/locks/sleeps/sync waits/polling timers; no such blocking primitive...
Cmux Expensive Synchronous Load ✅ Passed Production Swift changes add the keyDown replay guard; the new guard file has no RestorableAgentSessionIndex.load. In AppDelegate, any RestorableAgentSessionIndex.load fallback is guarded via Share...
Cmux Cache Substitution Correctness ✅ Passed Guard is in-memory (Set of in-flight key identities) and not persistence; nearby close-history cache uses cold fallback + documented stale-acceptable rationale in Workspace.swift comments.
Cmux No Hacky Sleeps ✅ Passed PR #5891 only changes Xcode project.pbxproj and Swift source/test files; no TS/JS/shell/build/runtime scripts, so runtime-no-hacky-sleeps rules aren’t triggered.
Cmux Algorithmic Complexity ✅ Passed New guard in WindowKeyDownReplayGuard.swift uses O(1) Set contains/insert/remove (no loops/sorts/filters), and tests are constant-size with no scans.
Cmux Swift Concurrency ✅ Passed Checked updated Swift sources for cmuxForceDispatchKeyDownOnce / guard logic and scanned for DispatchQueue/Task/Combine/completionHandler patterns; found none in the relevant changed regions and ne...
Cmux Swift @Concurrent ✅ Passed Swift diff adds only synchronous keyDown replay-guard logic and tests; no new @concurrent or nonisolated async annotations/call sites appear, so rules are not violated.
Cmux Swift File And Package Boundaries ✅ Passed PASS: Only adds a small single-purpose guard file plus focused AppDelegate routing updates and a unit test; no oversized file growth or mixed responsibilities beyond existing app-target glue.
Cmux Swift Logging ✅ Passed PR commits add only #if DEBUG cmuxDebugLog calls; no added/changed app/runtime Swift print, debugPrint, dump, NSLog, or file-scoped Logger constants in the diffs.
Cmux User-Facing Error Privacy ✅ Passed Checked updated production files: user-facing alert/localized default strings contain no upstream/vendor names (WebKit/Ghostty/Sentry); new logic only adds debug-only logging and test assertions.
Cmux Full Internationalization ✅ Passed Inspected added/modified Swift: WindowKeyDownReplayGuard debug logs only (#if DEBUG) and AppDelegate passes non-localized reason strings solely for those debug logs; no user-facing or web i18n stri...
Cmux Swiftui State Layout ✅ Passed PR diff only adds an AppKit keyDown replay guard, AppDelegate routing, and tests; no SwiftUI state/layout patterns (ObservableObject/@Published/GeometryReader/Lazy/List row store refs) appear in th...
Cmux Architecture Rethink ✅ Passed Guard centralized via NSWindow.cmuxForceDispatchKeyDownOnce uses a stack-scoped in-flight Set with defer removal; no sleeps/asyncAfter/polling/locks/observers added, and routing goes through one...
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed Bot rule targets new standalone NSWindow/NSPanel/WindowGroup without cmux.* id+registration. PR adds only keyDown replay guard + test-only NSWindow; no cmuxAuxiliaryWindowIdentifiers/aux close-shor...
Cmux Source Artifacts ✅ Passed git diff main..HEAD --name-only shows only 4 paths: Sources/App/WindowKeyDownReplayGuard.swift, Sources/AppDelegate.swift, cmux.xcodeproj/project.pbxproj, cmuxTests/WindowKeyDownReplayGuardTests....
Description check ✅ Passed PR description is comprehensive with detailed crash explanation, repro steps, fix details, testing approach, and interaction with parallel PR #5899.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch issue-5887-keydown-replay-loop

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@greptile-apps

greptile-apps Bot commented Jun 11, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

Fixes a main-thread stack overflow (issue #5887) caused by an infinite loop between NSWindow.cmux_performKeyEquivalent and responder keyDown force-dispatches. The fix introduces a single, unified guard (cmuxForceDispatchKeyDownOnce) that tracks in-flight events by field-tuple identity and refuses to re-dispatch the same event while it is still on the stack.

  • New file WindowKeyDownReplayGuard.swift: defines CmuxForceDispatchedKeyEventIdentity (window, type, keyCode, modifiers, timestamp) and cmuxInFlightForceDispatchedKeyEventIdentities, with a stack-scoped defer-remove pattern that correctly allows WebKit's legitimate post-unwind replay.
  • AppDelegate.swift: removes seven per-branch depth counters and routes all 13 keyDown(with:) force-dispatch sites through cmuxForceDispatchKeyDownOnce; previously unguarded paths (printable Option text, Ghostty font-zoom, stale-menu bypass, browser-surface-to-terminal, omnibar arrow restore, and more) are now covered.
  • WindowKeyDownReplayGuardTests.swift: three regression tests pin once-per-event dispatch, distinct autorepeat events each dispatching, and same-event dispatch after the prior call unwinds.

Confidence Score: 5/5

Safe to merge. The guard is principled and covers all 13 force-dispatch sites; the stack-overflow crash path is closed and no regression to WebKit's legitimate post-unwind replay.

The unified chokepoint correctly replaces seven per-branch depth counters that could not see cross-branch ping-pong. Field-tuple identity (window, type, keyCode, modifiers, timestamp) is stable across event copies and correctly distinguishes autorepeat. The defer-based remove preserves WebKit's legitimate replay once the original dispatch unwinds. Fallthrough behavior when the guard declines — either return false or delegating to cmux_performKeyEquivalent — matches the prior per-branch semantics in each affected path. Three regression tests pin the invariant deterministically.

No files require special attention.

Important Files Changed

Filename Overview
Sources/App/WindowKeyDownReplayGuard.swift New file: clean implementation of the unified replay guard. Field-tuple identity, stack-scoped Set entry/removal via defer, and @autoclosure reason string are all correct. No isolation or logic issues.
Sources/AppDelegate.swift Removes seven per-branch depth counters and routes all 13 force-dispatch sites through cmuxForceDispatchKeyDownOnce. Stale-menu, Ghostty zoom, omnibar-restore, browser arrow, Return/Enter, text-box, editable-text, and command-palette paths are all correctly wired. Fallthrough behavior when the guard declines matches prior branch semantics.
cmuxTests/WindowKeyDownReplayGuardTests.swift Well-structured regression suite. ReplayingKeyDownView correctly simulates bounded re-entrancy. Three tests cover dispatch-once, autorepeat (distinct timestamps each dispatch), and replay-after-unwind. installWindowResponderSwizzlesForTesting is present in AppDelegate at line 12055.
cmux.xcodeproj/project.pbxproj Adds WindowKeyDownReplayGuard.swift to the app target Sources phase and WindowKeyDownReplayGuardTests.swift to the test target. References look correct.

Reviews (5): Last reviewed commit: "Merge branch 'main' of https://github.co..." | Re-trigger Greptile

Comment on lines +1 to +5
import AppKit
import XCTest

#if canImport(cmux_DEV)
@testable import cmux_DEV

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Production fix absent — crash is still live on current HEAD

The cmuxForceDispatchKeyDownOnce guard described in the PR is not present in any committed file. AppDelegate.swift line 17252 still calls textInputTarget.keyDown(with: event) with no replay guard in the shortcutRoutingShouldBypassForPrintableOptionText branch, and the seven per-branch depth counters remain unchanged. As submitted, testPrintableOptionTextKeyDownIsForceDispatchedExactlyOncePerEvent will trip on the real crash path (bounded to 6 dispatches by replaysRemaining = 5), failing CI with keyDownEvents.count == 6 != 1. The fix described in the PR description — a unified cmuxForceDispatchKeyDownOnce wrapping all keyDown dispatch sites — needs to land in the same merge commit (or as the follow-on squash before merge) for this PR to accomplish what it claims.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct at review time, and thank you for catching it: the branch head you reviewed (b655fed) was the intentionally failing first commit of the two-commit regression structure, and the fix commit had not been pushed yet due to an orchestration slip. The fix is now on the branch as c4f61dd (cmuxForceDispatchKeyDownOnce, routing every force-dispatch site).

Verified on a dev build of the fix: with web content focused on https://example.com, Option+A x3, Return, and Down all leave the app alive. The identical Option+A setup crashed an unguarded build (current main) in under a second this morning with the issue's exact signature (incident C9470E41-11A7-4A04-874F-C8AE5DF1CA06).

One correction to the prediction that the test would trip CI: the tests job at b655fed stayed green because the app-host suite crashed and hit its 900s timeout before reaching the W* classes, and the job still passed. That CI gap is now tracked in #5903. The deterministic red/green proof ran on a macOS 26 builder instead: WindowKeyDownReplayGuardTests fails at b655fed and passes at c4f61dd.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the full context — the two-commit structure makes sense now, and the orchestration slip explains the timing. My original comment was accurate for the branch head I reviewed; it's moot now.

The fix at c4f61dd looks correct. A few observations from reading it:

Identity struct is sound. Using timestamp: TimeInterval (a Double) for equality is safe here because the comparison is always of the same-object timestamp against itself, not a computed value — same bits, exact equality holds.

@autoclosure on reason: is a nice touch; the string interpolation never runs in release builds.

Previously-unguarded paths are all covered. The stale-menu bypass and browser-surface-shortcut-to-terminal paths were genuinely unguarded in the old code and are now routed through cmuxForceDispatchKeyDownOnce. That's the most important correctness gain beyond the nominal crash path.

One subtle difference to be aware of in the browser-arrow omnibar-restore sub-path: In the old code the depth check short-circuited before the makeFirstResponder(focusedOmnibarField) call. In the new code, if cmuxForceDispatchKeyDownOnce returns false on re-entry, makeFirstResponder may have already been called for that call frame. In practice this is benign — on re-entry the field editor is already first responder so the currentEditorResponder == nil || firstResponder !== currentEditorResponder condition evaluates false and makeFirstResponder is skipped — but it's a semantics difference worth knowing.

The CI gap (test-suite crash swallowing the test failure) being tracked in #5903 is the right call; the macOS 26 builder red/green proof is sufficient evidence for this PR.

LGTM from my side. The invariant is right, the chokepoint is clean, and the three regression tests cover the cases that matter (guard fires, guard is per-event not global, guard is stack-scoped).

Comment on lines +65 to +94
) -> NSEvent? {
NSEvent.keyEvent(
with: .keyDown,
location: .zero,
modifierFlags: [.option],
timestamp: timestamp,
windowNumber: windowNumber,
context: nil,
characters: "å",
charactersIgnoringModifiers: "a",
isARepeat: false,
keyCode: 0
)
}

func testPrintableOptionTextKeyDownIsForceDispatchedExactlyOncePerEvent() {
_ = NSApplication.shared
AppDelegate.installWindowResponderSwizzlesForTesting()

let (window, responder) = makeWindowWithReplayingResponder()
guard let event = makeOptionTextKeyDownEvent(windowNumber: window.windowNumber) else {
XCTFail("Failed to construct Option+A key event")
return
}

XCTAssertTrue(window.performKeyEquivalent(with: event))
XCTAssertEqual(
responder.keyDownEvents.count,
1,
"The same in-flight key event must not be force-dispatched into keyDown again " +

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Two other unguarded keyDown dispatch sites in cmux_performKeyEquivalent are not covered

The test exercises the printable-option-text path but two additional keyDown dispatch sites in cmux_performKeyEquivalent also have no replay guard in the current code: the Ghostty font-zoom path (ghosttyView.keyDown(with: event) at line 17317) and the shouldForwardBrowserSurfaceShortcutToTerminal menu-miss path (firstResponderGhosttyView.keyDown(with: event) at line 17597). If commit 2 routes those calls through the same cmuxForceDispatchKeyDownOnce chokepoint, adding test variants for a GhosttyNSView-as-first-responder scenario (zoom shortcut) would confirm the guard is wired there too and prevent silent regression if a future branch adds another unguarded dispatch.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in c4f61dd (now pushed): the Ghostty font-zoom and menu-miss dispatches, plus the stale-menu bypass, command palette arrows, text-box input, editable-text-view, omnibar, and browser Return paths, all route through the single cmuxForceDispatchKeyDownOnce chokepoint. There are 13 call sites and zero direct keyDown(with:) dispatches left in cmux_performKeyEquivalent; the only one remaining in that region is inside the helper itself.

On the suggested GhosttyNSView-as-first-responder test variant: a real GhosttyNSView needs the embedded terminal runtime, which the headless unit host cannot bring up, and a stub standing in for it would re-test the same helper the existing three tests already cover (dispatch-once, distinct events each dispatch, same event again after unwind). Since every branch now shares that helper, per-branch variants would duplicate the chokepoint coverage rather than add protection, so we skipped them per the repo's low-value-test guidance.

…file length budget

No behavior change. The guard helper, identity struct, and in-flight set
move from Sources/AppDelegate.swift (which the fix had pushed 32 lines
over its 18057-line budget) into Sources/App/WindowKeyDownReplayGuard.swift,
leaving AppDelegate.swift 35 lines under budget. The new file stays below
the 500-line tracking threshold.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…-5887-keydown-replay-loop

# Conflicts:
#	Sources/AppDelegate.swift
@austinywang
austinywang merged commit 275f2da into main Jun 11, 2026
20 checks passed
hhsw2015 pushed a commit to hhsw2015/cmux that referenced this pull request Jun 12, 2026
…ops (manaflow-ai#5891)

* Add failing regression test for the keyDown replay loop

NSWindow.cmux_performKeyEquivalent force-dispatches certain key events
straight into the focused responder's keyDown. When the responder does
not consume the key, AppKit routes the same event back into
performKeyEquivalent while the first dispatch is still on the stack
(WebKit replays unhandled keys, and on macOS 26 -[NSWindow keyDown:]
re-enters performKeyEquivalent). The printable-Option-text bypass has no
re-entry guard, so the event ping-pongs between the swizzle and the
responder until the main-thread stack overflows.

The test drives the real chokepoint: a window whose first responder
re-invokes performKeyEquivalent with the same event from keyDown,
bounded so the pre-fix failure is a clean assertion instead of a crash.
It asserts the force-dispatch happens exactly once per event, that
distinct events (autorepeat) still each dispatch, and that the same
event may dispatch again once the prior dispatch has unwound (WebKit's
legitimate replay).

Repro of manaflow-ai#5887: Option+A with
a browser pane focused on non-editable content crashes with "Thread
stack size exceeded due to excessive recursion" (incident
C9470E41-11A7-4A04-874F-C8AE5DF1CA06 on a debug build, mirroring
incident 96E09E5C-19CC-49D4-B068-7A666CE784A9 from cmux NIGHTLY
0.64.14).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Guard every performKeyEquivalent keyDown force-dispatch against replay loops

Replace the seven per-branch forwarding-depth counters in
NSWindow.cmux_performKeyEquivalent with one shared chokepoint,
cmuxForceDispatchKeyDownOnce. The helper tracks the identity
(window number, event type, keyCode, modifiers, timestamp) of every
key event whose force-dispatch is currently on the stack and refuses
to dispatch the same event a second time, returning false so the
caller falls through to default AppKit handling.

This closes the unguarded printable-Option-text bypass that crashed
cmux NIGHTLY 0.64.14 (manaflow-ai#5887):
WebKit replays an unhandled key through the responder chain, macOS 26
-[NSWindow keyDown:] re-enters performKeyEquivalent, and the bypass
force-dispatched the same event back into CmuxWebView.keyDown forever
until the main-thread stack overflowed. It also guards the previously
unguarded ghostty zoom, stale-menu-bypass, and menu-miss keyDown
dispatches, and protects against cross-branch ping-pong that
per-branch counters cannot see (the first responder can change while
a dispatch is in flight).

The guard is stack-scoped (insert before keyDown, remove via defer),
so WebKit's legitimate single replay of an unhandled key, which
arrives after the original dispatch has unwound, still force-dispatches
normally. Key autorepeat produces distinct events with fresh
timestamps, so repeat typing is never throttled, and the dispatching
window's number is part of the identity so multiple windows cannot
suppress each other.

Fixes manaflow-ai#5887

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Move the keyDown replay guard into its own file to satisfy the Swift file length budget

No behavior change. The guard helper, identity struct, and in-flight set
move from Sources/AppDelegate.swift (which the fix had pushed 32 lines
over its 18057-line budget) into Sources/App/WindowKeyDownReplayGuard.swift,
leaving AppDelegate.swift 35 lines under budget. The new file stays below
the 500-line tracking threshold.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

This branch was successfully deployed

1 active deployment
Preview – cmux — dbae247c Deployed Jun 11, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Crash: infinite key-event routing loop between NSWindow performKeyEquivalent swizzle and CmuxWebView.keyDown (main-thread stack overflow)

1 participant