Repository navigation
fix(next): Ctrl-`, Cmd-D and New Row always give a terminal; a refusal is never silent; no stand-in id reaches the daemon (P0) - #28331
Conversation
…(P0 new terminal) Fleet repro on v2.2.9-opt (cmux17, cmux20, cmux21): Cmd-T then Ctrl-`. The new-tab missed the client's 5 s start deadline behind the New Tab page's new-conversation-tab, and the app took the Ctrl-` stand-in back. Red on the base: newTab throws terminalStartTimedOut at the client deadline.
…in back on a client clock (P0 new terminal) v2.2.9-opt P0 (Lawrence): at times Ctrl-` made no new terminal. Fleet repro (4/4 on cmux17, cmux20, cmux21): Cmd-T, then Ctrl-`. The New Tab page's new-conversation-tab got no reply for seconds; the new-tab queued behind it on the app's connection missed the 5 s spawn deadline; DaemonService rejected the intent and the stand-in vanished, while cmux-tui still made the tab. The keys typed for the new terminal went to the old one. A new-tab with terminal-placement-env-v1 carries the stand-in's client-chosen terminal id, and cmux-tui does not replay a new-tab sent again (splits re-send with their client keys; new-tab cannot). So it now waits for its reply or the connection's end (LineTransport fails pending requests on close): the daemon's outcome decides, no client deadline does (principled-audit R5, R-L5). The shell integration arguments are added through one helper for both paths (R1). Regression test: NewTabLateStartTests (red on the base at 7340d85).
…n a client deadline Ctrl-` and Cmd-D must always give a terminal that takes keys (principled-audit R3, P0 2026-10-30). The one rollback point of every intent (DaemonService.intendNowReporting) now stops DEBUG builds when it takes a terminal stand-in (a New Terminal Tab's provisional tab, a terminal split's pane) back for a client deadline: only the daemon's answer or the connection's end may do that. TerminalStandInRollbackInvariantTests pins the judgement.
…mmand), not an app-internal failure type
… back (P0 review)
…its like a split, the late-start wait has no give-up Review of the P0 fix (FIX at 9dc00b5): the DEBUG R3 check would stop the app on two paths that still rolled a terminal stand-in back on a client clock: - New Row (row-client-keys-v1) sent new-row with the 5 s start deadline: it now waits through a late start under its client keys, as a split does. - The keyed late-start wait (splits, columns, panes, create-terminal) gave up after a fixed 120 s budget and on cancellation threw the timeout: it now ends on the reply, the daemon's refusal, the connection's end, or CancellationError (principled-audit R5, R-L5). The daemon replays each keyed pass, so a re-send changes nothing. Also: the newTab doc moved above its attribute. Regression test: NewRowLateStartTests (red at the commit before this one).
…d for, never closed (P0 R3)
…lies; R3 covers every new terminal Lead decision on the P0: Cmd-N is a new terminal too. Its create-workspace asked again only until a fixed 10 s budget, and its first create-terminal was sent once with the 5 s start deadline; either failure closed the workspace the person already saw (WorkspaceCreation.withTerminal) while the daemon still made it. Both steps are keyed (mutation id; terminal id + mutation id) and the daemon replays them, so both now wait through late replies until the reply, a refusal, the connection's end or cancellation (R5, R-L5): - DaemonConnection.createWorkspaceWaitingForReply, createTerminalWaitingForStart(_:) and one shared loop, waitingThroughLateReplies. - WorkspaceCreation.create (every workspace with a terminal) uses them; its rollback is checked in DEBUG (DaemonService.checkTerminalRollback). - The R3 predicate covers .createWorkspace with a terminal tab. Regression test: WorkspaceWithTerminalLateCreateTests (red at the commit before).
…al; R3 checked where the wait ends (review FIX at 70e92ae) Review: WorkspaceCreation.create's DEBUG check judged every error of the caller's terminal closure, but four callers still made their first terminal with the 5 s start deadline (Open Folder / createAndShow and its callers, agent fork, agent teams, incognito window), and a later step's 2 s timeout (configure, incognito tab) would also have tripped it: DEBUG builds would stop there. - Those callers make their first terminal with createTerminalWaitingForStart (keyed, replayed by the daemon), as Cmd-N does. - The R3 check moves to the two waits themselves (requestWaitingForStart, createWorkspaceWaitingForReply): DEBUG stops if either ever ends on a late start, which their loop never does. A caller's own later step is not judged. - WorkspaceWithTerminalLateCreateTests also makes the first terminal start late (the workspace part's red is ceac05e).
…e first-terminal path for every new workspace Lead decisions on the P0 (R-L4, R1): - waitingThroughLateReplies keeps waiting with no end, but a re-send (it only repairs a lost request; the daemon replays keyed creates) is spaced by full-jitter exponential backoff from 1 s, capped at 30 s (Backoff.fullJitter, the shared retry type), not at the fixed 2 s / 5 s deadline: no retry storm on a hung daemon. Cancellation ends it during the backoff wait. - WorkspaceCreation.firstTerminal is the one way the first terminal of a new workspace is made (Open Folder, fork, teams, incognito); the test uses it. - Tests: the scripted daemons of NewRowLateStartTests and WorkspaceWithTerminalLateCreateTests end their start once and replay later asks at once, as cmux-tui does (before, each ask was answered a fixed delay after it arrived, which no deadline shorter than that delay could ever see). LateReplyBackoffTests pins the spacing.
…ate-reply backoff)
…ane_id_exists), as cmux-tui does (third review)
…s not replay new-row (third review FIX at e4e6774) The third review found that a keyed new-row sent again after a late start is refused pane_id_exists by cmux-tui (mux/rows.rs commits it unkeyed; resource_topology/client_ids.rs refuses a held pane id), so the re-send made the app take the row back on that refusal. New Row now goes the way new-tab goes: sent once, answered by its reply or the connection's end (requestAwaitingStart, R5, R-L5). requestPaneCreation is again split, new-pane, new-pane-right only. Regression test: NewRowLateStartTests (its daemon now refuses a second ask; red at the commit before this one).
…pane (P0; bench-arms 02dcba2e: 12/12 made none)
…e, not its stand-in Lead P0 evidence (bench-arms 02dcba2e1c9e447c7cad0594): in a quick pair of splits the second Cmd-D / Cmd-Shift-D made no pane within 5 s, 12 of 12. Cause: the second split targets the focused pane, which is the first split's stand-in (ProvisionalPane), and PaneSplitCommand sent that stand-in's handle, which the daemon does not know; the refusal took the second pane back. New Column and New Row already resolve the target at send time and run in the workspace's create order; the split did neither. - PaneSplitCommand resolves its target at send time (DaemonStore.daemonHandle(showing:)) and waits until the store has applied its own reply, as NewColumnCommand / NewRowCommand do. - PaneHandlers.split's optimistic path joins the workspace's create chain (WorkspaceContentController.pendingCreate), as createInKeysTurn does: it goes out once the create before it is in the store (R1, one path). The wait is the earlier request's own outcome, no timer (R5). Regression test: SplitOfPendingSplitTests (red at the commit before).
… hop): pendingCreate is an ActionWork
…d-D and checks it runs in that pane's shell (the P0 invariant: a terminal that takes keys)
…aceWithTerminalLateCreateTests imports it)
…eads main-actor pane state)
…-keys-v1 as cmux-tui does (the app sends a row's client keys under it)
…o stand-in; Ctrl-` on a pending split's pane opens there and takes keys (P0 rules a, b)
…es for the daemon (P0 rules a, b)
cmux-ad's two rules for the P0, both in this PR:
(a) A refusal is never silent. A person's optimistic create (Cmd-D split,
Ctrl-` New Terminal Tab, New Row, New Column) the daemon refuses rolls its
stand-in back AND shows the refusal where refusals show (refusal HUD) with a
log line: ActionRegistry.trackRollingBack, the legacy handlers' form of the
ActionEffect.applied failure path (remoteFailureObserver). A create the
person closed first (PaneSplitCancelled) is not a failure.
(b) A stand-in id never leaves for the daemon. Every create that targets a
pane resolves it at send time with DaemonStore.daemonTarget(_:) (the daemon's
pane that took the stand-in over; PendingPaneNotCreated when its create was
never made) and runs in the workspace's create chain (pendingCreate), so it
goes out after the create that makes its target: split, New Column, New Row
and now New Terminal Tab. The New Terminal Tab overlay on a stand-in follows
it to the daemon's pane (ProvisionalSplit.rememberStandInTarget/shownPane).
DEBUG builds stop at a typed request that names a stand-in pane or surface
(DaemonRequestExecutor.standInReference).
Tests: RefusedCreateIsNeverSilentTests, NewTerminalOnPendingSplitTests (red at
the commit before), StandInReferenceTests.
…PendingPaneNotCreated, 21 locales); the English text stays for logs Lead decision: the refusal HUD is this PR's visible error; its text reaches the person, so PendingPaneNotCreated maps to MiscHandlerStrings.pendingPaneNotCreated (MiscHandlers.xcstrings: en and ja translated, the other 19 locales needs_review as the table's other keys) in ActionWorkFailure.create, the one failure builder of Cmd-D, Ctrl-`, New Row and New Column. Test: CreateFailureTextTests.
… main-actor strings; every caller is main-actor)
…@testable import (as other app tests do)
…eation hold ended (a test window is never key, as the journeys' outwait_hold) and says where a lost line went Green 87ad5a04 at 98b377d: 10 of 11 suites passed; this one failed at the typed line (wait-for timeout), every earlier check holding (two terminals in the confirmed pane, the new one selected, focus and responder in the pane). Feat's creation hold replays held keys with NSApp.sendEvent, which reaches only the key window. CreationInputCoordinator.isHolding(in:) (as perf has it) lets the test type after the hold; on a miss it records each terminal's screen, the responder and the hold.
…Reached | focusLeft | windowClosed) and logs a limit end; NewTerminalOnPendingSplitTests asserts focusLanded Lead decision (R-L5: a hold's end is a signal): waiting in the test for the hold to end could hide keys a person feels up to 2 s late. The hold now records its end per window (CreationInputCoordinator.lastHoldEnd(in:)): unforced means every creation replied and the focus it asked for holds with a responder that takes keys (focusLanded); the two bound timers are limitReached and log a notice (window, held key count, last action) so a person's log shows it; a key leaving the content is focusLeft. The test asserts the Ctrl-` on a pending split's pane ended its hold with focusLanded before it types.
…the service (compile e35af5f2: sending 'service' risks data races)
|
Review (independent review subagent #8, Claude Opus 5.5): SHIP at 69167de Scope
Review #7 findings
BLOCKINGNone. NON-BLOCKING
Ordering, isolation, reentrancy
Known limits (true; one is missing from the body)
Proof jobs (checked with
|
|
Review #8 addendum (independent review subagent #8, reviewer-session a62554ee-b4da-41b3-ab45-99b673c217f5). This is a note on the proof job. It does not change the receipt above. The green job
|
|
Checks on head 69167de, read with
Pending: Fleet test results at this head (not a GitHub check): the 12-suite green |
|
Red check classification:
|
…t (owner a2c6445561ebeab90, expiry 2026-11-06) The perf port of feat #28331 (the P0: a quick second Cmd-D / Cmd-Shift-D made no pane) adds 62 CmuxNextApp lines: the create-and-wait chain in the App's own controllers (pendingCreate and the creates that join it, PaneSplitCancelled handling, trackRollingBack of a refused create), the creation-hold end log, and the pane-not-ready hook wiring. What could move already moved (refusal strings and ActionWorkFailure.create to CmuxNextDaemonService, the hold-end record to CmuxNextFocus, the waiting creates to DaemonConnection). Allowed by cmux-ad item 727 for this port after the perf suites passed (job 0d3a1c41b49ef8665e62b106 at b52e9f7: the 6 P0 suites, 13 tests, all pass). The independent exception review checked each of the 62 lines as the P0 fix itself: "Review (independent review subagent, Claude Opus 5.5, app-lines exception): SHIP at b52e9f7". The follow-up that moves the chain out is STATUS item 810 (owner a2c6445561ebeab90, due 2026-11-06).
One hand-resolved hunk, Packages/macOS/CmuxNext/Sources/CmuxNextApp/PaneController+Intents.swift,
in newTerminalTabNow (Ctrl-` with a stand-in):
- feat-cmux-next: `selectWhenReported(surface: pending.surface)` became
`if selectHere { selectWhenReported(surface: pending.surface) }` (a Home dock placement
that owns its selection does not select here), with HomeDockOpens.cancel in both catch
branches; the task was still `services.registry.track(Task { ... })`.
- this branch: the task joins the workspace's create chain (`let previous =
self.workspace?.pendingCreate`, `await previous?.value`, `let work: ActionWork = Task`,
`self.workspace?.pendingCreate = work`, `services.registry.trackRollingBack("newSurface",
work)`) and returns `.create("new-tab", error)`.
Resolution keeps both: the `if selectHere` guard from feat, then this branch's chain lines;
the HomeDockOpens.cancel calls from feat merged without a conflict in both catches, and
this branch's `.create` return and trackRollingBack stay. No other conflict.
|
Review (independent review subagent): SHIP at 47a86e1 Scope: the merge 47a86e1 (parents 69167de = PR head with SHIP #8, 5b65b83 = origin/feat-cmux-next tip), read with
No blocking findings. |
|
The |
…r 0.5 s of quiet (#28425) (#28526) * cmux-next: classic upgrade checks wait for placements to land, not for 0.5 s of quiet (#28425) ClassicUpgradeSimulator.settle() treated 0.5 s with no visible change as done. Since #26153 a classic import places each new workspace at its slot through the sidebar document (a `sidebar.apply` move, not the personal placement queue that #28362 waits for), so on a loaded runner a move or an adopt could still be on its way after the quiet window: the check then read an order or a group the late move changed (#28331 attempts 1 and 2, #28425). Product: SidebarDocStore counts the changes and adopts it sent and not yet answered, and exposes isSettled and settled() (event-driven: each reply and load end resumes the waiters). The classic import waits for it after the personal queue, so a workspace's group write is sent after its slot move is committed on the daemon, on the document path too. Tests: settle() and the ClassicMigrationTests launch helper read the tree only once every completion signal holds: the personal queue through, the document settled, the store applied through an event barrier taken after those replies, no live workspace still waiting for its new-workspace placement, and the document projection current. No quiet window; a hang guard records an issue naming what is still in flight. SidebarDocSettledTests covers the new signal. SidebarDocStore.swift: the remote protocol, its reply and SidebarDocUnsettled move to SidebarDocRemote.swift (no change), so the file shrinks. Sidebar-Look: no visual change Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * cmux-next tests: time the classic import's wait for the sidebar document (#28425) The import now waits for its slot move's reply before each group write. ClassicUpgradeSimulationPerfTests records that wait per imported workspace through a DEBUG bench span (BenchSpans, nothing recorded in release) and prints n, sum and p50/p95/max per profile (CLASSIC-SIM-PERF-SETTLE). The perf suite also holds the one SidebarDocStore (.ownsSidebarDocStore), as the other live-daemon suites do. Sidebar-Look: no visual change Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * cmux-next tests: the parked check waits for the shells to exit, not for settle()'s quiet window (#28425) backgroundWorkspacesStartTheirTerminalsWhenFirstShown read the parked state right after settle(). `park-tabs` answers before the shells exit; a tab shows dead with `relaunch` only once its host exited. The 0.5 s quiet window that settle() no longer has covered that by chance; without it the last workspace (scratch) read "not parked: [(false, nil)]" in 5 of 10 runs at 7f80b7d (step 815e7963). The check now reads once every background tab shows parked (the daemon's tab-exit events), with a 60 s hang ceiling. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * cmux-next tests: forced-order red for #28425, sidebar document moves held past any quiet window ClassicUpgradeHeldDocumentTests holds every sidebar document change and adopt (the classic import's slot moves) for 4 s before it reaches the daemon, as a loaded runner's slow reply does. The check must read the tree only once those moves have landed. The test seam is a `wrapping:` parameter of SidebarDocWiring.start (default: the daemon remote as is), passed by ClassicUpgradeSimulator(documentRemote:). The red is on a scratch never-merge ref (this test over the base logic: settle() with its 0.5 s quiet window and the import without the document wait); this commit and the branch head are the green. Sidebar-Look: no visual change Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * cmux-next tests: the held-document red holds each move until someone waits for it, with no clock (#28425) The 4 s hold was still a clock. HeldRemote now holds every sidebar document change and adopt until someone waits in SidebarDocStore.settled() (the store's onSettledWait test seam releases them; one sent while someone waits goes on at once). The test reads, in the turn settle() returns in, how many are still held: a settle that does not wait on the document returns with moves held (red, every time); the new one cannot return until they are released and answered (green). placementsSettled() now awaits the store's settled() instead of polling isSettled. Sidebar-Look: no visual change Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * cmux-next tests: a placement wait that hangs names what is pending (#28425) placementsSettled() keeps the event-driven SidebarDocStore.settled() wait, with a 60 s hang guard (a ceiling, not a quiet window): if the store has not settled by then, the recorded issue says what is pending, from the store's new pendingDescription (changes and adopts unanswered, a running load, changes a load must settle, waiters). The held-document test prints it in its failure too. Sidebar-Look: no visual change Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * cmux-next tests: fix the compile of the held-document test and the placement wait (#28425) Compile 6af24f9b at 08910ed failed: - ClassicUpgradeHeldDocumentTests.swift:73: the trailing closure matched ClassicUpgradeSimulator.init's `ssh:` parameter (String), not `documentRemote:`; it is passed by label now. - AppServices+PlacementsSettled.swift:41,50: `pendingDescription` is internal to CmuxNextSidebar; the helper imports it @testable. Also: the held remote's `waiting` is typed @mainactor, the local in release() no longer shadows it, and after the read nothing more is held (shutdown's changes go through). The await on the settled wait is its own statement. Sidebar-Look: no visual change Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
feat (#28331, P0 2026-10-30) already makes split, New Column and New Row wait for their reply with no client clock: split and new-pane-right through waitingThroughLateReplies (unbounded, jittered re-send), new-row through requestAwaitingStart (sent once, no deadline). Conflicts resolved to feat's side: - DaemonConnection+LateReplies: feat's requestWaitingForStart and waitingThroughLateReplies; this branch's budgeted retryingLateStarts and creatingShown are dropped (their only caller, New Row, now awaits its start with no deadline, so the wrapper could never retry). - PaneSplit/NewColumn/NewRow commands: feat's daemonTarget and intendNowReporting. - CreationInputCoordinator: feat's comment (no late-reply budget remains). Kept from this branch: cmux-tui row-client-replay-v1 (a keyed new-row sent again replays), its capability on both sides, the New Tab page split's late-reply retry, and SplitSlowDaemonKeepsPaneTests. RowCommands' comment now names the replay.
Summary
P0 from dogfood v2.2.9-opt (Lawrence, a 4 h session): "at some points I can no longer make new terminals via Ctrl-` or Cmd-D".
What the person saw. A new terminal (Ctrl-`), a split (Cmd-D) or a new row appeared for a moment and vanished, or the keys typed for it ran in the old terminal.
Cause, three faults in one family: the app took a new terminal back while the daemon still made it, or never asked for it correctly.
shows a stand-in tab at once and sendsnew-tabwith a client-chosen terminal id. That request had the 5 s spawn deadline. When the reply came later (it queued behind a slow request on the app's connection),DaemonService.intendNowReporting` took the stand-in back while cmux-tui still made the tab. New Row had the same 5 s deadline. Splits, columns and a new workspace's first terminal gave up after fixed 120 s or 10 s budgets.refusals []).Fix.
new-taband anew-row(cmux-tui replays neither when sent again) are sent once and wait for their reply or the connection's end:DaemonConnection.requestAwaitingStart(CmuxNextDaemon/Connection/DaemonConnection.swift:154), used bynewTab(DaemonConnection+Commands.swift:136) andRowCommands.newRow(RowCommands.swift:28).waitingThroughLateRepliesandlateReplyBackoff(DaemonConnection+LateReplies.swift:101,:116,Backoff.fullJitter). Every new workspace with a terminal (Cmd-N, Open Folder, agent fork, agent teams, incognito) makes its first terminal through one path,WorkspaceCreation.firstTerminal(WorkspaceCreation.swift:78).Checklist (cmux-ad rules for this P0)
ActionRegistry.trackRollingBack(CmuxNextActions/ActionRegistry+Effects.swift:80), used atHandlers/PaneHandlers.swift:174,PaneController+Intents.swift:202andWorkspaceContentController+Intents.swift:173.ActionWorkFailure.create(Handlers/MiscHandlerStrings.swift:120), keyhandlers.misc.failed.pendingPaneNotCreatedin 21 locales.PaneSplitCancelled) is not a failure.RefusedCreateIsNeverSilentTests.DaemonStore.daemonTarget(CmuxNextDaemon/Store/DaemonStore+Intents.swift), used byPaneSplitCommand,NewColumnCommand,NewRowCommandandNewTerminalTabCommand. It returns the daemon's pane that took the stand-in over, or throwsPendingPaneNotCreated. These creates run in the workspace's create chain (pendingCreate), so each goes out after the create that makes its target; the wait is the earlier request's own outcome, no timer. A Ctrl-tab on a stand-in follows it to the daemon's pane (IntentOverlay`).daemonTargetalso throwsPaneNotReadywhen the pane it finds is still a stand-in (the pending pane of a Cmd-N workspace has no resource id yet).DaemonRequestExecutor.performscans each encoded line before it is written (one byte search for a decimal number >= 2^62, a JSON parse only on a hit, keys pane/surface/target/anchor) and throwsPaneNotReadyfor any request that names a stand-in, creates included, in every build. There is no daemon round trip and no DEBUG stop (the earlierTargetResolvedCreateexemption is removed: it crashed DEBUG on Split Left of a pending split and sent the stand-in in release). Cmd-N's workspace stand-ins (2^62 + 2^61) are caught too.DaemonConnection.request(reported), the one place every request goes through: theDaemonServicefunnels and the handlers that call the connection themselves (a typed New Tab page command or a cmux.jsonnewTabInCurrentPane, New Tab in Group, the non-optimistic split/column/row, agent fork). With no open action scope (DaemonCommandScope.isOpen), the connection calls its observer, whichDaemonServicesets on each connection it makes (observePaneNotReady): the notice shows on the next frame through the drain batcher (log line + HUD "This pane is still opening; try again in a moment.",handlers.misc.failed.paneNotReady, 21 locales), and the error is marked shown (PaneNotReady.shown, carried asActionWorkFailure.shown). The registry (ActionRegistry+Effects), the handler reporters (AppActionContext.report,StashHandlers.report, agent create) and agent fork skip a shown failure, so it never shows twice. An open scope (CLI, MCP) answers the typedunavailablerefusal in the app's language (DaemonService.workFailure) instead. The optimistic creates passPaneNotReadyup, not the generic "the daemon refused the split". Clear history does not report it to the crash reporter.SplitOfPendingSplitTests,NewTerminalOnPendingSplitTests,StandInReferenceTests(withStandInLineTests),PaneVerbOnPendingPaneTests(zoom shows once; Split Left on a pending split shows once, sends nothing, makes no pane;requestandperformeach show once).02dcba2e1c9e447c7cad0594, benches: warm, two-quick-presses and cold arms in six felt benches; the headline is the worst arm #28221 (comment); red proofs89d99910andbc080642above). With the fix: 2 failures in 13 runs, about 1 in 6 to 1 in 7. At head 69167de:05563df226b8bea654bde31f1 fail,216230c32be6cf456369136d5 pass,17418039cdae019ea11d141f1 pass. At 3776515:99c595e46b863d756e5dd3541 fail and 4 pass, green79fe4d7f1 pass. At 642e879: greenf2d57d1a1 pass. At the merged head 47a86e1 (feat-cmux-next merged in, 2026-10-31):25b930845f86c6948f4e3ffc1 fail,82b745f89e19448e860924433 pass (1 in 4). Re-run at the pre-merge head 69167de the same hour:54119ff5351b870500661fc12 fail, 1 pass. Every failure is the same race (SplitOfPendingSplitTests.swift:14:6, wait-for timeout after both splits confirmed), so the merge made it no worse. All feat runs with the fix: 5 failures in 20.focusLandedorlimitReached), whether the surface was attached when each key was sent, and the HUD count.SplitOfPendingSplitTests.aSecondSplitOnAPendingSplitsPaneMakesASecondPane, owner the p0-port agent (a2c6445561ebeab90), P0: a line typed after a quick double Cmd-D can miss the new pane (rare race, after #28331) #28436, expiry 2026-11-01. The suite stays enabled (no skip,#iforwithKnownIssue). CmuxNext Swift suites have no per-suite known-failure registry (scripts/cmux-next/quarantine.jsonholds fast-tier entries and car-guard entries per test target), so the entry is here and in P0: a line typed after a quick double Cmd-D can miss the new pane (rare race, after #28331) #28436.TabGroupHandlers.swift:114-125) and the non-optimistic split / new column / new row (WorkspaceContentController+Intents.swiftspawnPane, :178-190) call the connection in a plain task with no ticket and only log a failure, so the run answers "ok" when the refusal comes, and the app shows no notice while that run is open (the connection leaves an open scope's refusal to the run). This was true before this PR for every error on these paths. Follow-up: cmux-next: a CLI/MCP pane create answers ok after a late refusal (New Tab in Group, non-optimistic split/column/row) #28394 (send them through aDaemonServicefunnel and track the work; a red CLI test for each of the 4 actions first).DaemonService+StandInInvariant.swift:33), and no late-reply wait ever ends on a late start (checkNoLateStart).CreationInputCoordinator.HoldEnd: focusLanded, limitReached, focusLeft, windowClosed, creationFailed). AlimitReachedend is logged (:211).NewTerminalOnPendingSplitTestsasserts the Ctrl-hold ended withfocusLanded`.Testing
All reds failed on their own assertion (not on build or runner), except c0fe852f, which failed by the base crash (the bug) as its row says. The zoom row is a guard test, not a red.
NewTabLateStartTeststimed out: new-tab (no reply within 0.2 seconds)NewRowLateStartTestsproof/p0-newterm-row-redc83d584timed out: new-row (no reply within 0.3 seconds)WorkspaceWithTerminalLateCreateTestsproof/p0-newterm-firstterm-red20230e76create-terminal ... 0.5 secondsSplitOfPendingSplitTestsproof/p0-newterm-twosplit-base(origin/feat-cmux-next + test) 9081d39new confirmed terminal panes 1 of 2; refusals []SplitOfPendingSplitTestsproof/p0-newterm-twosplit-red2(head minus the two-split fix) c0cd1a7RefusedCreateIsNeverSilentTestsproof/p0-newterm-rule-a2f00df28the refused split showed nothing (HUD shown 0, was 0)StandInReferenceTests,NewTerminalOnPendingSplitTestsproof/p0-newterm-rule-b2938bd6ePaneVerbOnPendingPaneTestszoom on the pending pane showed nothing (HUD 0, was 0)NewTerminalOnPendingSplitTests(with the hold-end check),StandInReferenceTestsproof/p0-newterm-rule-b39214505PaneVerbOnPendingPaneTests, Split Left (review #6 B1)splitLeftOnAPendingSplitsPaneShowsOneNoticeAndSendsNothinghitDaemonRequestExecutor.swift:68: Fatal error: split names stand-in pane=...(the DEBUG stop review #6 B1 named; the crash is the bug: Split Left sent a stand-in). It killed the process, so the funnel test did not run in this job.PaneVerbOnPendingPaneTests, every funnel (review #6 B2)proof/p0-newterm-r6-b2-redd8cb74d6902 (head minus its rule (a) part:DaemonService.ended,ActionWorkFailure.shown, the reporters, the create rethrow; the B1 boundary kept; test files identical to the head's)everyFunnelShowsAPaneNotReadyRefusalOncefailed on its own assertion (PaneVerbOnPendingPaneTests.swift:81:44, theuntilcondition:requeston a pane still opening showed no notice), no crash. Zoom and Split Left passed there (zoom goes throughDaemonService.send, Split Left throughperformandtrackRollingBack; both showed the refusal before this fix).PaneVerbOnPendingPaneTests, typed new tab on a pending pane (review #7 B1)aCommandTypedIntoANewTabOnAPendingPaneShowsOneNoticefailed on its own assertion (PaneVerbOnPendingPaneTests.swift:110:44, theuntilcondition: the typed new tab on the pending pane showed no notice), no crash; the other 3 tests of the suite passedPaneVerbOnPendingPaneTests, zoomendedplus the registry.Green at the earlier head 670131c: cba1a2416c5fb6b6c721bf25, 11 suites, 28 tests, none skipped. Green at 321277d: c311521d (12 suites). Green at 3776515: 79fe4d7f (12 suites). Green at 642e879: f2d57d1a (12 suites, all ran, 22 tests by the run summaries, none skipped). Green at this head 69167de: 05563df226b8bea654bde31f (12 suites; result added when it ends). Suites:
NewTabLateStartTests,NewRowLateStartTests,SplitLateStartTests,WorkspaceWithTerminalLateCreateTests,TerminalStandInRollbackInvariantTests,LateReplyBackoffTests,SplitOfPendingSplitTests,RefusedCreateIsNeverSilentTests,NewTerminalOnPendingSplitTests,StandInReferenceTests,CreateFailureTextTests,PaneVerbOnPendingPaneTests. Compile (CmuxNext + app, test targets) at this head 69167de: b3fd053c6fe4868e52de9d99 passed (e35af5f2 at aa8769f failed on one Sendable error, fixed in 69167de).The proof refs are test-only
proof/*branches listed in stopped.txt as never merge; each has test files identical to the head's.Observed / Not run
Observed:
, the typed line ran in the old terminal or no terminal showed, 5 times. App log each time:new-conversation-tab ... no reply within 2.0 seconds, thennew-tab rejected: timed out ... 5.0 seconds, thennew-tab failed: the daemon refused the new tab`.TerminalEventQueue.parkReader()for the whole 3 s, so replies waited unread behind one terminal's output. Filed for pa-b1 as TRANSPORT MULTIPLEX (handoff/followups.md); not fixed here.Not run / only read:
newterm-after-new-tab-page(Ctrl-, Cmd-D and terminal-first Cmd-N right after Cmd-T) is on the perf portcp/fix-p0-new-terminal-always`, where the journeys live; it runs there on an optimized build.NSApp.sendEvent(keys typed while a creation hold waits) is not covered in a non-key test window; the test types after the hold and asserts the hold ended withfocusLanded.Follow-ups (separate PRs, mine): one seam where every pane verb (zoom, rename, swap, a New Tab page, the creates on a pending Cmd-N pane above) waits on the create chain for its pane instead of being refused, and where a create waits only when its target is a stand-in (review #4 N2, N3); a per-pane error state with its own strings and capture; a visible "starting…" label on a pending stand-in; the scripted newTab that made no sidebar row (followups.md, STATUS 578).
Performance
No perf numbers: this PR changes when a new terminal is made or refused (no client deadline, a stand-in id never sent), not how fast anything runs; "perf" here names the perf/chrome-parity branch, where the port runs the journey
newterm-after-new-tab-pageon an optimized build. The request boundary adds one byte search per request line (a JSON parse only on a hit), with no allocation on the common path.Capture
No capture: the change is when a new terminal is made or refused under a slow daemon reply or a split still in flight, a race of a few milliseconds that no capture scene or tour holds; a still would show a normal terminal. The only new pixels are the existing refusal toast with the new localized text, which the tests assert by string key (
MiscHandlerStrings.paneNotReady). The journeynewterm-after-new-tab-pageon the perf port drives the real keys on an optimized build.Changelog
Fixed: Ctrl-`, Cmd-D and New Row always give a new terminal, also when the daemon answers slowly or a split is still being made; a refused one now says why.