Fix iOS render-grid input replay flake - #6953
austinywang wants to merge 36 commits into
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughThe PR updates terminal subscription repair so repaired subscriptions replay mounted surfaces and refresh workspace state. Test support now tracks ChangesTerminal subscription repair and replay
Mobile UI compatibility guards
Sequence Diagram(s)sequenceDiagram
participant MobileShellComposite
participant subscribeRpc as mobile.events.subscribe
participant replayHelper as replayAfterRepairedTerminalEventSubscription
participant workspaceRefresh as scheduleWorkspaceListRefreshFromEvent
MobileShellComposite->>subscribeRpc: refreshTerminalEventSubscription(reason, replaySurfaceIDsIfRepaired)
subscribeRpc-->>MobileShellComposite: ack.alreadySubscribed
alt alreadySubscribed == false
MobileShellComposite->>replayHelper: replay mounted surfaceIDs
replayHelper->>workspaceRefresh: schedule refresh
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested reviewers
Poem
Important Pre-merge checks failedPlease resolve all errors before merging. Addressing warnings is optional. ❌ Failed checks (1 error, 2 warnings)
✅ Passed checks (22 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
…dergridterminalinputwaitsforliveeve
Greptile SummaryFixes #5911: iOS render-grid input replay stall when
Confidence Score: 5/5Safe to merge; the repair logic is well-bounded and all four race-condition paths are covered by new regression tests. The core subscription-repair change is mechanically sound: it checks the ack after the subscribe returns, uses the captured surface list to avoid re-scan races, cancels the coalescing guard before each replay, and then refetches workspace state. The didMoveToWindow cleanup path fully covers the removed deinit in TapInstallerView. The compiler-version guards around iOS 26 glass APIs are the correct way to avoid build failures on older SDKs. No unhandled concurrency hazards or data-loss paths were identified. No files require special attention. Important Files Changed
Sequence Diagram%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant App as iOS App
participant MSC as MobileShellComposite
participant Sub as refreshTerminalEventSubscription
participant Mac as Mac Host
App->>MSC: submitTerminalRawInput(surfaceID)
MSC->>MSC: "input_seq_wait detected (localSeq < remoteSeq)"
MSC->>Sub: refreshTerminalEventSubscription(reason: input_seq_wait, replaySurfaceIDsIfRepaired: [all mounted])
Sub->>Mac: requestTerminalEventSubscription(topics)
Mac-->>Sub: "ack { already_subscribed: false }"
Note over Sub: Registration was lost - repair path
Sub->>MSC: replayAfterRepairedTerminalEventSubscription(surfaceIDs: [all mounted])
loop For each mounted surface
MSC->>MSC: cancelTerminalReplayInFlight(surfaceID)
MSC->>Mac: requestTerminalReplay(surfaceID, resolvedWorkspaceID)
Mac-->>MSC: replay frame with post-gap seq
MSC->>App: deliver catch-up output
end
MSC->>Mac: scheduleWorkspaceListRefreshFromEvent()
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant App as iOS App
participant MSC as MobileShellComposite
participant Sub as refreshTerminalEventSubscription
participant Mac as Mac Host
App->>MSC: submitTerminalRawInput(surfaceID)
MSC->>MSC: "input_seq_wait detected (localSeq < remoteSeq)"
MSC->>Sub: refreshTerminalEventSubscription(reason: input_seq_wait, replaySurfaceIDsIfRepaired: [all mounted])
Sub->>Mac: requestTerminalEventSubscription(topics)
Mac-->>Sub: "ack { already_subscribed: false }"
Note over Sub: Registration was lost - repair path
Sub->>MSC: replayAfterRepairedTerminalEventSubscription(surfaceIDs: [all mounted])
loop For each mounted surface
MSC->>MSC: cancelTerminalReplayInFlight(surfaceID)
MSC->>Mac: requestTerminalReplay(surfaceID, resolvedWorkspaceID)
Mac-->>MSC: replay frame with post-gap seq
MSC->>App: deliver catch-up output
end
MSC->>Mac: scheduleWorkspaceListRefreshFromEvent()
Reviews (29): Last reviewed commit: "Avoid render-grid resync on pending dupl..." | Re-trigger Greptile |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift`:
- Around line 5749-5753: The repaired-subscription flow in
MobileShellComposite’s subscription refresh path is incorrectly gated by
replaySurfaceIDsIfRepaired and an early return on repaired acks, which can block
default callers and lose a later input_seq_wait repair replay. Update the logic
in the subscription handling method so alreadySubscribed == false is handled
first, then replay the current/provided mounted surfaces, and always enqueue the
workspace refresh regardless of whether the replay list is empty. Also ensure
terminalSubscriptionRefreshTask does not suppress the follow-up refresh request
needed after a repaired ack.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 0c060037-cde0-4442-bbc4-8704accc2d05
⛔ Files ignored due to path filters (1)
.github/swift-file-length-budget.tsvis excluded by!**/*.tsv
📒 Files selected for processing (4)
Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swiftPackages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/MobileShellRenderGridLivenessTestSupport.swiftPackages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/MobileShellRenderGridLivenessTests.swiftios/cmuxPackage/Tests/cmuxFeatureTests/cmuxFeatureTests.swift
…dergridterminalinputwaitsforliveeve
…dergridterminalinputwaitsforliveeve # Conflicts: # .github/swift-file-length-budget.tsv
…dergridterminalinputwaitsforliveeve # Conflicts: # .github/swift-file-length-budget.tsv # Packages/iOS/CmuxAgentChatUI/Sources/CmuxAgentChatUI/Screen/ChatScrollEdgeCoordinator.swift # Packages/iOS/CmuxAgentChatUI/Sources/CmuxAgentChatUI/Transcript/ChatTranscriptTableView.swift # Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/MobileShellRenderGridLivenessTestSupport.swift # Packages/iOS/CmuxMobileTerminal/Sources/CmuxMobileTerminal/GhosttySurfaceView.swift
…h replay Regression coverage for the race autoreview flagged in replayAfterRepairedTerminalEventSubscription: when a cold-attach replay for a surface is still in flight, requestTerminalReplay coalesces (no-ops on its in-flight guard), so the repaired-subscription catch-up is silently dropped and the surface stays behind the input response sequence with no follow-up replay. This test parks the mount replay in flight, then drives an input_seq_wait repair for the same surface and asserts a second replay is issued and the post-gap frame is delivered. It fails against the current coalescing behavior (commit 1 of the two-commit red/green pair); the superseding fix follows. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
replayAfterRepairedTerminalEventSubscription issued requestTerminalReplay per mounted surface, but that helper is a no-op while a replay for the surface is already in flight. A repaired subscription is precisely the catch-up after the host reported the registration was absent, so coalescing behind an older in-flight replay (e.g. a cold-attach replay that started before the gap) silently dropped the catch-up: the older replay returns a snapshot from before the missed events, pendingTerminalByteEndSeqBySurfaceID stays behind the input response sequence, and nothing re-triggers a replay. Cancel any in-flight replay for the surface before requesting the catch-up so the fresh request always goes out. The subsequent requestTerminalReplay re-adopts the active replay barrier token (if any), preserving barrier semantics; the cancelled request's completion is dropped by its stale request-id guard, so there is no retry storm. Fixes the race covered by the preceding test (commit 2 of the two-commit red/green pair). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
2 issues found across 18 files
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
The two-commit regression pair grew two tracked files past their recorded budgets: the test file (MobileShellRenderGridLivenessTests.swift +54, the new repairReplaySupersedesInFlightColdAttachReplay case) and the fix (MobileShellComposite.swift +9, the supersede guard). Re-record both so the swift-file-length-budget guard (workflow-guard-tests -> ci-status) passes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
An earlier commit replaced `isolated deinit` with
`deinit { MainActor.assumeIsolated { … } }` for older-compiler
compatibility, but `assumeIsolated` traps if the final
`MobileShellComposite` release happens off the main actor — turning an
ordinary lifecycle edge into a crash.
Gate the safe, actually actor-isolated `isolated deinit` behind
`#if compiler(>=6.2)`. CI's iOS-simulator lane (Xcode 26.5) and the
shipping release/TestFlight lanes all run Xcode 26+ (Swift >= 6.2), so
production and CI always take this path. Older local toolchains keep a
`Thread.isMainThread`-guarded `assumeIsolated` fallback, so a stray
off-main release degrades to a best-effort skip instead of trapping
(outstanding work is `[weak self]` and `remoteClient` tears down via its
own deinit, so skipping never leaks). Shared teardown is extracted into
`tearDownOnDeinit()`.
Addresses autoreview P1 and the cubic-dev-ai thread at
MobileShellComposite.swift:952. Refreshes the Swift file-length budget
for the added lines.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The event-driven `waitForLineCount` awaited a continuation with no timeout, so a stream that never reaches the expected chunk count would hang the test instead of failing (the polling it replaced had a bounded fallthrough). Add a `timeoutNanoseconds` parameter and throw `LineCountWaitError.timedOut` with a diagnostic when the output never arrives. The pending waiter resumes exactly once via a small main-actor-isolated box, so the normal-completion and timeout paths can never double-resume the continuation. Addresses the cubic-dev-ai thread at cmuxFeatureTests.swift:43. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…dergridterminalinputwaitsforliveeve # Conflicts: # .github/swift-file-length-budget.tsv
…re-apply repair-replay fix) origin/main (#7159/#7171) independently reworked the mobile render-grid cold-attach replay subsystem: replay-lifecycle methods moved to MobileShellComposite+TerminalReplayLifecycle.swift (widened to internal), refreshTerminalEventSubscription became fire-and-forget, and new cold-attach machinery was added (requestColdAttachTerminalReplay, upgradePendingColdTerminalReplaysIfNeeded, full-replacement tracking). Resolution: - MobileShellComposite.swift: reset to origin/main's version, then re-applied this PR's exact net delta on top — the compiler-gated isolated deinit + tearDownOnDeinit(), the refreshTerminalEventSubscription repair mechanism (replaySurfaceIDsIfRepaired:), and replayAfterRepairedTerminalEventSubscription (which supersedes an in-flight replay before re-requesting so a post-repair catch-up is not coalesced behind a stale cold-attach snapshot). Verified the resolved file equals origin/main + this PR's net delta exactly, with no duplicate declarations against the relocated extension methods. - MobileShellRenderGridLivenessTestSupport.swift: unioned both sides' mock replay mechanisms (per-surface replayFramesBySurfaceID for the liveness tests; replayRenderGridFrames queue for origin/main's cold-attach/staleness tests). - .github/swift-file-length-budget.tsv: regenerated via scripts/swift_file_length_budget.py --write-budget. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The pre-6.2 fallback deinit skipped teardown entirely on an off-main final release (the `Thread.isMainThread` guard failed), leaking the retained terminal event listener task (which holds `client` strongly and parks in `for await`) and the mobile event subscription/socket. Dropping task handles at dealloc does not cancel unstructured tasks. origin/main already fixed deinit safety with an ungated `isolated deinit` (guaranteed main-actor teardown, no trap, no skip), which the CI/shipping toolchains (Swift 6.2+) all use. Revert to that exact form so the deinit has zero delta from mainline; this PR's contribution is the render-grid replay-repair supersede, which is unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…dergridterminalinputwaitsforliveeve # Conflicts: # .github/swift-file-length-budget.tsv # Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift # Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/MobileShellRenderGridLivenessTestSupport.swift
…dergridterminalinputwaitsforliveeve # Conflicts: # .github/swift-file-length-budget.tsv
Fixes #5911
Summary
input_seq_waitdiscovers the subscription was just reinstalledrenderGridTerminalInputWaitsForLiveEventBeforeReplaywait on collector delivery instead of a sleep polling windowValidation
swift test --filter inputSeqWaitRepairingLostSubscriptionReplaysMountedSurface(failed before fix, passed after)swift test --filter MobileShellRenderGridLivenessTestsswift test --filter renderGridTerminalInputWaitsForLiveEventBeforeReplaycould not run locally because SwiftPM reported local binary targetGhosttyKitat/Users/cmux/manaflow/term/cmux133/GhosttyKit.xcframeworkdoes not contain a binary artifactLocalization
Need help on this PR? Tag
/codesmithwith what you need. Autofix is disabled.Summary by cubic
Fixes #5911. Repaired terminal event re-subscribe now reliably replays all mounted render‑grid surfaces, supersedes any in‑flight cold‑attach replay, and ignores duplicate pending ACKs to avoid unnecessary resyncs.
Bug Fixes
refreshTerminalEventSubscription(reason:, replaySurfaceIDsIfRepaired:)uses the ack; when a lost registration is repaired it callsreplayAfterRepairedTerminalEventSubscription(...)to replay the given surfaces (or all mounted) and refresh workspaces.replayAfterRepairedTerminalEventSubscriptioncancels any in‑flight replay before requesting the repaired catch‑up and passes resolved workspace IDs to avoid re‑scans.input_seq_waitrequests a repaired replay for all mounted surfaces; if a surface is still behind after setting a pending seq, trigger a resync; duplicate pending ACKs no longer restart the event stream.MobileGlassEffectContainer,View+MobileGlass), mark key‑command creation and transcript/table builders@MainActor, addnonisolatedinits, and move push permission reads to anonisolatedhelper.Tests
mobile.terminal.replay, delayed subscribe acks,terminal.inputresponses, andterminal.bytesgaps.TerminalOutputCollector.waitForLineCountnow fails fast with a timeout.Written for commit b59b924. Summary will update on new commits.
Summary by CodeRabbit