Skip to content

Fix cmux ssh resize freezing: reconnect dead cached control-socket connections in the CLI - #5808

Closed
kays0x wants to merge 4 commits into
manaflow-ai:mainfrom
kays0x:fix-cli-socket-stale-reconnect
Closed

kays0x wants to merge 4 commits into
manaflow-ai:mainfrom
kays0x:fix-cli-socket-stale-reconnect

Conversation

@kays0x

@kays0x kays0x commented Jun 10, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • What changed? SocketClient.send() now detects a peer-closed cached connection before writing (connectionAppearsOpen()), transparently re-establishes it, and replays password auth when configured. The ssh-pty-attach SIGWINCH resize handler logs real failures instead of discarding them with try?.
  • Why? The control socket server applies a 30s receive timeout to every accepted connection (SocketTransport.clientReadTimeout, default 30) and closes it once idle — handleClient's read loop treats the timeout as EOF. Any long-lived cached SocketClient is therefore usually talking to a dead socket by the time it next sends. The resize handler swallowed that failure, so every cmux ssh window resize issued more than ~30s after attach silently never reached the remote PTY, freezing the remote at its previous size (local PTY reflows, remote doesn't — the TUI then renders garbled at the stale width).
  • This is the same user-visible bug as Fix cmux ssh window resize not reaching the remote PTY #4960. That PR was closed as "fixed by Add detachable SSH PTY daemon persistence #4807", but Add detachable SSH PTY daemon persistence #4807 only made the server serve multiple requests per connection — resize works for ~30s after attach and then breaks, which is why the earlier verification passed. Reproduced on v0.64.14 against a live session: the attached CLI's control-socket fd shows peer (none) in lsof while the data bridge stays healthy, and the daemon never receives the resize RPC.
  • Fixing it inside SocketClient.send() (rather than at the resize call site as Fix cmux ssh window resize not reaching the remote PTY #4960 did) also heals the other long-lived users of a cached client — e.g. handleSSHPTYBridgeEOF, which could misreport "bridge closed before remote PTY exit could be confirmed" (exit 255) on a stale connection. The relay-backed path already treats reconnect-per-send as steady state; this extends the same honesty to the unix-socket path. Per-connection password auth is replayed on reconnect (registered by authenticateSocketClientIfNeeded), which a call-site-only fix would miss.

Testing

  • Two commits per the regression policy: commit 1 = failing test only (CI red), commit 2 = fix (CI green).
  • Regression test testSSHPTYAttachReconnectsResizeForEachSIGWINCH (ported from Fix cmux ssh window resize not reaching the remote PTY #4960, kays0x authorship): drives several SIGWINCH signals against a one-request-per-connection control-socket mock and asserts each is delivered as its own workspace.remote.pty_resize RPC.
    • Without the fix: fails with delivered=0 (10.7s, exhausts the poll window).
    • With the fix: passes in 0.8s.
  • Adjacent SSH PTY tests pass unchanged: testSSHPTYAttachSerializesResizeBeforeEOFLocalCleanup, testSSHPTYAttachBridgeEOFWhileSessionRunsExitsWithoutSSHRetryStatus, testSSHPTYAttachBridgeEOFWhenSessionGoneClearsLocalState, testSSHPTYAttachWaitUsesCurrentTerminalSizeForBridgeHandshake.
  • xcodebuild test -scheme cmux-unit ... -only-testing:cmuxTests/CLINotifyProcessIntegrationRegressionTests/... → TEST SUCCEEDED (5 tests, 0 failures).
  • Manual root-cause verification on a live v0.64.14 session against a real remote (ovh VPS): local PTY resized 88x55 → 60x37 while remote stayed 88x55; manual SIGWINCH no-op; lsof showed the CLI's cached unix socket with peer gone while the bridge TCP connection stayed ESTABLISHED; daemon-side pty_sessions still reported the attachment at 88x55 — resize RPCs were being silently dropped.

Demo Video

  • Terminal-only change; before/after evidence above (live-session TIOCGWINSZ reads + lsof fd state + daemon pty_sessions output).

Checklist

  • I tested the change locally
  • I added or updated tests for behavior changes
  • I updated docs/changelog if needed (not needed — behavior fix)
  • Bot reviews run automatically on every push
  • All code review bot comments are resolved
  • All human review comments are resolved

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


Summary by cubic

Fixes cmux ssh resize freezes by auto-reconnecting stale control-socket connections and replaying auth safely. Resizes now reach the remote PTY after idle timeouts; real errors are logged and EOF cleanup exits cleanly.

  • Bug Fixes
    • Detect peer-closed cached connections in SocketClient.send() and reconnect before sending; replay per-connection password auth via a remembered auth command, routed through performSend to avoid re-entering send().
    • ssh-pty-attach resize handler now logs failures (with size and error) instead of swallowing them.
    • Regression test simulates a one-request-per-connection control socket: asserts each SIGWINCH delivers a workspace.remote.pty_resize with full attach context and that the process exits 0 after the bridge closes; exit wait is guarded to fail instead of crash if hung.

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

Review in cubic

Summary by CodeRabbit

  • New Features
    • Socket connections now transparently reconnect after peer-closed idle sessions and automatically replay the last successful authentication.
  • Bug Fixes
    • SSH PTY resize propagation now reports failures instead of silently ignoring them, improving troubleshooting when resize forwarding fails.
  • Tests
    • Added a regression test verifying ssh-pty-attach reconnects and continues sending PTY resize requests across repeated SIGWINCH signals.

@vercel

vercel Bot commented Jun 10, 2026

Copy link
Copy Markdown

@kays0x is attempting to deploy a commit to the Manaflow Team on Vercel.

A member of the Team first needs to authorize it.

@chatgpt-codex-connector

Copy link
Copy Markdown

To use Codex here, create an environment for this repo.

@coderabbitai

coderabbitai Bot commented Jun 10, 2026 •

Copy link
Copy Markdown

Review Change Stack

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 67e4f3d6-c6ee-4c0f-879c-ddb0a6ece403

📥 Commits

Reviewing files that changed from the base of the PR and between ac45606 and b6cc12a.

📒 Files selected for processing (1)
  • CLI/cmux.swift

📝 Walkthrough

Walkthrough

SocketClient now caches a successful auth command and transparently replays it after detecting a peer-closed connection; send() guards reestablishment per request. PTY resize propagation errors are now caught and logged. A regression test verifies reconnect-and-resend behavior across multiple SIGWINCH signals.

Changes

Socket Reconnection with Auth Replay and PTY Resize Error Logging

Layer / File(s) Summary
Reconnection state and mechanism
CLI/cmux.swift
Adds private var reconnectAuthCommand: String?, func rememberReconnectAuth(command:), and private func reestablishConnectionIfPeerClosed(responseTimeout:) throws; send(command:responseTimeout:) calls the reconnection routine and replays stored auth via performSend when needed; successful auth responses now call client.rememberReconnectAuth(command: "auth \(socketPassword)").
PTY resize error visibility
CLI/cmux.swift
Replaces silent try? workspace.remote.pty_resize with do/catch and logs errors thrown by workspace.remote.pty_resize.
Regression test for reconnect on SIGWINCH
cmuxTests/CLINotifyProcessIntegrationRegressionTests.swift
Adds testSSHPTYAttachReconnectsResizeForEachSIGWINCH which runs ssh-pty-attach against a one-request-per-connection mock control socket and loopback bridge, emits multiple SIGWINCH signals, asserts at least three full-context workspace.remote.pty_resize RPCs are delivered, and verifies clean exit and empty stdout/stderr.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I stored the key, I hop, I try,
When sockets nap I stitch the tie.
SIGWINCH rings out, I bounce and send,
Reconnect, resize — the signals mend.
Logs now tell when things unbend.

🚥 Pre-merge checks | ✅ 20 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 36.36% 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 accurately describes the main fix: reconnecting dead cached control-socket connections to resolve resize freezing in cmux ssh.
Description check ✅ Passed The PR description follows the template with complete Summary and Testing sections; it thoroughly explains what changed, why it matters, how it was tested, and includes a detailed regression test explanation.
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 SocketClient's new private property and methods don't introduce Swift 6 actor isolation issues; they follow existing synchronization patterns and remain private to a non-Sendable reference type.
Cmux Swift Blocking Runtime ✅ Passed PR introduces no new blocking/timing-based synchronization in production Swift code. New methods rememberReconnectAuth() and reestablishConnectionIfPeerClosed() only store strings and call existing...
Cmux Expensive Synchronous Load ✅ Passed No expensive synchronous loaders added. New SocketClient reconnection methods only use lightweight socket syscalls (poll, socket, stat, connect) and network I/O, not index loads or sysctl.
Cmux Cache Substitution Correctness ✅ Passed PR stores transient in-memory auth command for socket reconnection, not persisted. Replays post-reconnection within same session; no fresh authoritative read is replaced, and server authenticates p...
Cmux No Hacky Sleeps ✅ Passed PR only modifies Swift code (out of scope) and test-only deterministic sleeps (allowed). No production non-Swift code changes that violate runtime-no-hacky-sleeps.md.
Cmux Algorithmic Complexity ✅ Passed New socket reconnection code (connectionAppearsOpen, reestablishConnectionIfPeerClosed, rememberReconnectAuth) is O(1) per operation with constant-time poll/connect, no collection scans, not called...
Cmux Swift Concurrency ✅ Passed PR introduces no problematic legacy async patterns. Socket reconnection is synchronous; signal/filesystem monitoring use required OS API boundaries (DispatchSource); test uses allowed DispatchQueue...
Cmux Swift @Concurrent ✅ Passed All new/modified methods in the PR are synchronous; no async/await or @concurrent annotations are present or required. Blocking socket I/O in performSend/send/reestablishConnectionIfPeerClosed is a...
Cmux Swift File And Package Boundaries ✅ Passed PR adds 46 lines to fix socket reconnection within existing SocketClient class—a focused bug fix well below the 250-line threshold for large file additions, with no new responsibilities or oversize...
Cmux Swift Logging ✅ Passed The PR uses only the established cliDebugLog mechanism (guarded by #if DEBUG) for logging resize failures; no print/NSLog/debugPrint violations in new code.
Cmux User-Facing Error Privacy ✅ Passed Changes use existing error-handling patterns (same as sendV1Command); debug logging is debug-only via cliDebugLog; test code is exempt; no new sensitive information exposed.
Cmux Full Internationalization ✅ Passed Socket reconnection logic adds no user-facing text. Error logging uses debug-only cliDebugLog (exempt per rules). Test file contains no localization requirements. No new .xcstrings or web/messages...
Cmux Swiftui State Layout ✅ Passed PR adds no SwiftUI state patterns; changes are purely backend: SocketClient (non-UI networking class) adds plain private var reconnectAuthCommand: String? and private methods for socket reconnect...
Cmux Architecture Rethink ✅ Passed This PR makes correctness fixes with clear ownership and invariants; no new timing patches, polling, or split lifecycle ownership introduced. The fix properly reconnects stale sockets in SocketClie...
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PR adds no window-related code: only socket client reconnection logic and regression tests with no NSWindow/NSPanel/NSWindowController/SwiftUI Window/WindowGroup changes.
Cmux Source Artifacts ✅ Passed PR #5808 changes only 2 legitimate files: CLI/cmux.swift (socket reconnection logic) and a test file. No artifact directories (.claude, .vercel, .greptile, DerivedData, etc.) are added.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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 10, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a real, reproducible bug where cmux ssh window resizes silently stop working roughly 30 seconds after attaching — because the control-socket server applies a 30 s receive timeout and closes idle connections, leaving the CLI's cached SocketClient talking to a dead socket. The SIGWINCH handler swallowed write failures via try?, so resizes were quietly dropped.

  • SocketClient.send() now calls reestablishConnectionIfPeerClosed() before writing: it poll()s the fd with zero timeout, and if POLLHUP/POLLERR/POLLNVAL is detected it tears down and rebuilds the connection, replaying saved password auth via the new performSend core (so no re-entrancy guard is needed — the isReplayingReconnectAuth sentinel flagged in the previous review is gone).
  • The SIGWINCH handler replaces try? with a do/catch that routes real failures to cliDebugLog (#if DEBUG only), making unexpected failures visible in debug builds instead of silently freezing the remote PTY.
  • A regression test (testSSHPTYAttachReconnectsResizeForEachSIGWINCH) drives a one-request-per-connection mock socket server, fires several SIGWINCH signals, and asserts each one produces its own workspace.remote.pty_resize RPC — validating both the reconnect path and clean process exit.

Confidence Score: 5/5

Safe to merge — the reconnect logic is contained within SocketClient.send(), the isReplayingReconnectAuth sentinel removed by prior-review feedback is gone, and the regression test exercises the exact failure scenario end-to-end.

The change is narrow and well-tested: it adds a poll-based dead-connection check and a clean reconnect+auth-replay path, validated by a purpose-built regression test that was red before the fix and green after. The only open gap (sendOneWay not calling reestablishConnectionIfPeerClosed) was flagged in a prior review and is not new. No new defects are introduced on the changed path.

CLI/cmux.swift — specifically sendOneWay, which still bypasses the reconnect guard (pre-existing open comment).

Important Files Changed

Filename Overview
CLI/cmux.swift Adds reestablishConnectionIfPeerClosed() + performSend() to SocketClient and wires the resize handler's try? into a proper do/catch. The isReplayingReconnectAuth sentinel from the previous review is correctly absent; the sendOneWay gap (flagged in the prior review) remains open.
cmuxTests/CLINotifyProcessIntegrationRegressionTests.swift Adds a well-structured regression test that drives a one-request-per-connection Unix socket mock, asserts each SIGWINCH produces a resize RPC with all required params, and guards the exit wait with a semaphore timeout to avoid crashing on hang — solid test scaffolding.

Sequence Diagram

sequenceDiagram
    participant SH as SIGWINCH Handler
    participant SC as SocketClient.send()
    participant RC as reestablishConnectionIfPeerClosed()
    participant PS as performSend()
    participant SS as Control Socket Server

    SH->>SC: sendV2("workspace.remote.pty_resize")
    SC->>RC: reestablishConnectionIfPeerClosed(responseTimeout)
    RC->>RC: "connectionAppearsOpen() poll → POLLHUP set → false"
    RC->>SC: close() then connect()
    RC->>PS: performSend("auth password") via direct call
    PS->>SS: "auth password newline on new connection"
    SS-->>PS: "OK"
    PS-->>RC: "OK"
    RC-->>SC: return success
    SC->>PS: performSend("workspace.remote.pty_resize params")
    PS->>SS: "write RPC"
    SS-->>PS: response
    PS-->>SC: response string
    SC-->>SH: success
Loading

Reviews (4): Last reviewed commit: "test: fail instead of crash when ssh-pty..." | Re-trigger Greptile

Comment thread CLI/cmux.swift Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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 `@cmuxTests/CLINotifyProcessIntegrationRegressionTests.swift`:
- Around line 3702-3760: The test currently stops after counting resize RPCs but
doesn't wait for the child Process to exit; after signalling closeBridge, call
process.waitUntilExit() and then assert the process exited cleanly by checking
process.terminationStatus == 0 (and optionally process.terminationReason ==
.exit) so the test verifies a successful end-to-end run; use the existing
process, closeBridge.signal(), resizeCount(), targetResizes and delivered
symbols to locate where to add the wait and assertion.
🪄 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: 2de1dbba-a11d-4bae-aea9-e8f519d05ffd

📥 Commits

Reviewing files that changed from the base of the PR and between d535a02 and 26bb6bc.

📒 Files selected for processing (2)
  • CLI/cmux.swift
  • cmuxTests/CLINotifyProcessIntegrationRegressionTests.swift

Comment thread cmuxTests/CLINotifyProcessIntegrationRegressionTests.swift
hhsw2015 pushed a commit to hhsw2015/cmux that referenced this pull request Jun 11, 2026
kays0x added 4 commits June 13, 2026 21:03
The cmux control socket serves a single request per connection (the server
closes it after replying). `cmux ssh` forwards local SIGWINCH to the remote
PTY via workspace.remote.pty_resize over one long-lived SocketClient, so
after the first resize the cached connection is dead and every later resize
is swallowed by `_ = try? client.sendV2(...)`: the remote PTY freezes at its
attach-time width while the local PTY reflows.

Add testSSHPTYAttachReconnectsResizeForEachSIGWINCH, which drives several
SIGWINCH signals against a one-request-per-connection mock and asserts each
resize is delivered as its own RPC. The count requires every resize to carry
the full attach context (workspace/session/attachment ids + token), so a
resize that drops it does not satisfy the assertion. Against the unfixed
handler it fails with delivered=0, so this commit is intentionally red; the
fix follows.

Regressed by manaflow-ai#4323 (persistent SSH PTY sessions); reproduces on v0.64.10.
…n the CLI

The control socket server applies a 30s receive timeout to each accepted
connection and closes it once idle, so any long-lived cached SocketClient
(the ssh-pty-attach SIGWINCH resize source, bridge EOF handling) is
usually talking to a dead socket by the time it next sends. The resize
handler swallowed the failure with `try?`, freezing the remote PTY at
its previous size for every resize issued more than 30s after attach.

Detect the peer-closed connection in SocketClient.send() before writing
(a closed connection is the expected steady state for cached clients, not
an error), re-establish it, and replay password auth when configured --
the server authenticates each connection independently. The resize
handler now logs real failures instead of discarding them.

Turns testSSHPTYAttachReconnectsResizeForEachSIGWINCH green.
Address review: route the auth replay through a performSend core that
send() also uses, so reestablishConnectionIfPeerClosed never re-enters
itself and the isReplayingReconnectAuth sentinel (a race window on a
shared-mutable class) is gone entirely. Also tighten the regression
test per review: after the bridge closes, wait for ssh-pty-attach to
exit and assert a clean zero-status run with no stray output, so the
EOF-cleanup path is covered end-to-end.
Reading terminationStatus on a still-running Process raises an ObjC
exception, so a hung CLI would crash the test run rather than fail the
assertion. Guard the exit wait and return on timeout.
@kays0x
kays0x force-pushed the fix-cli-socket-stale-reconnect branch from ac45606 to b6cc12a Compare June 14, 2026 01:07

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 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 `@CLI/cmux.swift`:
- Around line 23217-23249: The resolveSurfaceAllowingFallbackDetailed method
calls claudeHookSurfaceIsListed multiple times, each performing a surface.list
RPC and linear scan, which violates algorithmic complexity guidelines for hot
paths. Refactor to fetch the surface list once at the start of
resolveSurfaceAllowingFallbackDetailed, build a Set containing both the id and
ref values from the returned surfaces for constant-time lookups, then replace
the two claudeHookSurfaceIsListed calls with direct Set membership checks
against the pre-built collection. This eliminates redundant RPC calls and linear
scans while maintaining the same verification logic.
- Around line 22201-22205: The code correctly computes
resolvedSurface.isAuthoritative and uses it to conditionally set surfaceId in
one location (lines 22201-22205), but the upsert/persist operations still write
surfaceId unconditionally into the session record. This allows a
non-authoritative fallback surface to be persisted as if it were authoritative,
causing incorrect reuse across panes. Apply the same isAuthoritative guard to
all upsert paths that persist surfaceId into the session: ensure that in lines
22201-22205 and the related upsert operations around lines 22450-22460, the
surfaceId is only written to the session when resolvedSurface.isAuthoritative is
true (otherwise pass nil or omit the field entirely to avoid persisting
borrowed/fallback surface identities).
- Around line 7283-7425: The runWindowDefaultDisplayCommand, runWindowNamespace,
runWindowDisplaysCommand, and runWindowDisplayCommand functions represent
substantial new window management functionality being added directly to the
monolithic cmux.swift file. Extract this newly added window command logic into a
dedicated CLI unit or package target rather than extending the already large
production Swift file. Create a separate module or component to encapsulate
window namespace handling, and have the main CLI file delegate to it, keeping
the primary CLI file focused and within size guidelines.
- Around line 7316-7334: The window default-display command currently silently
ignores unsupported flags (e.g., typos like --cleer), allowing them to fall
through to read mode and hiding user mistakes. Add validation to reject
unrecognized flags before processing the command. After filtering out positional
arguments, check if any remaining items in commandArgs start with a hyphen but
aren't the supported --clear flag, and throw a CLIError with an appropriate
message listing the supported options if such unsupported flags are found. This
validation should occur early in the command handling logic to catch user errors
immediately.
🪄 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: 67e4f3d6-c6ee-4c0f-879c-ddb0a6ece403

📥 Commits

Reviewing files that changed from the base of the PR and between ac45606 and b6cc12a.

📒 Files selected for processing (1)
  • CLI/cmux.swift

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 4

🤖 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 `@CLI/cmux.swift`:
- Around line 23217-23249: The resolveSurfaceAllowingFallbackDetailed method
calls claudeHookSurfaceIsListed multiple times, each performing a surface.list
RPC and linear scan, which violates algorithmic complexity guidelines for hot
paths. Refactor to fetch the surface list once at the start of
resolveSurfaceAllowingFallbackDetailed, build a Set containing both the id and
ref values from the returned surfaces for constant-time lookups, then replace
the two claudeHookSurfaceIsListed calls with direct Set membership checks
against the pre-built collection. This eliminates redundant RPC calls and linear
scans while maintaining the same verification logic.
- Around line 22201-22205: The code correctly computes
resolvedSurface.isAuthoritative and uses it to conditionally set surfaceId in
one location (lines 22201-22205), but the upsert/persist operations still write
surfaceId unconditionally into the session record. This allows a
non-authoritative fallback surface to be persisted as if it were authoritative,
causing incorrect reuse across panes. Apply the same isAuthoritative guard to
all upsert paths that persist surfaceId into the session: ensure that in lines
22201-22205 and the related upsert operations around lines 22450-22460, the
surfaceId is only written to the session when resolvedSurface.isAuthoritative is
true (otherwise pass nil or omit the field entirely to avoid persisting
borrowed/fallback surface identities).
- Around line 7283-7425: The runWindowDefaultDisplayCommand, runWindowNamespace,
runWindowDisplaysCommand, and runWindowDisplayCommand functions represent
substantial new window management functionality being added directly to the
monolithic cmux.swift file. Extract this newly added window command logic into a
dedicated CLI unit or package target rather than extending the already large
production Swift file. Create a separate module or component to encapsulate
window namespace handling, and have the main CLI file delegate to it, keeping
the primary CLI file focused and within size guidelines.
- Around line 7316-7334: The window default-display command currently silently
ignores unsupported flags (e.g., typos like --cleer), allowing them to fall
through to read mode and hiding user mistakes. Add validation to reject
unrecognized flags before processing the command. After filtering out positional
arguments, check if any remaining items in commandArgs start with a hyphen but
aren't the supported --clear flag, and throw a CLIError with an appropriate
message listing the supported options if such unsupported flags are found. This
validation should occur early in the command handling logic to catch user errors
immediately.
🪄 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: 67e4f3d6-c6ee-4c0f-879c-ddb0a6ece403

📥 Commits

Reviewing files that changed from the base of the PR and between ac45606 and b6cc12a.

📒 Files selected for processing (1)
  • CLI/cmux.swift
🛑 Comments failed to post (4)
CLI/cmux.swift (4)

7283-7425: 🛠️ Refactor suggestion | 🟠 Major | 🏗️ Heavy lift

Extract newly added command/hook logic from CLI/cmux.swift instead of extending the monolith.

This PR adds substantial new responsibilities (window/workspace namespace handling, browser routing, Claude-hook surface/state logic) to an already very large production Swift file. Please split these additions into dedicated CLI units/package targets before further growth.

As per coding guidelines, production Swift files over 800 lines and large additions to them should be flagged and decomposed.

Also applies to: 11938-12077, 22185-22620

🤖 Prompt for 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.

In `@CLI/cmux.swift` around lines 7283 - 7425, The runWindowDefaultDisplayCommand,
runWindowNamespace, runWindowDisplaysCommand, and runWindowDisplayCommand
functions represent substantial new window management functionality being added
directly to the monolithic cmux.swift file. Extract this newly added window
command logic into a dedicated CLI unit or package target rather than extending
the already large production Swift file. Create a separate module or component
to encapsulate window namespace handling, and have the main CLI file delegate to
it, keeping the primary CLI file focused and within size guidelines.

Source: Coding guidelines


7316-7334: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Reject unsupported flags in window default-display.

Unsupported flags are currently ignored and can silently fall through to read mode (e.g., typoed --cleer), which hides user mistakes.

Suggested change
+        let supportedFlags: Set<String> = ["--clear"]
+        if let stray = commandArgs.first(where: { $0.hasPrefix("-") && !supportedFlags.contains($0) }) {
+            throw CLIError(message: "window default-display does not support \(stray)")
+        }
+
         if commandArgs.contains("--clear") {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

        let supportedFlags: Set<String> = ["--clear"]
        if let stray = commandArgs.first(where: { $0.hasPrefix("-") && !supportedFlags.contains($0) }) {
            throw CLIError(message: "window default-display does not support \(stray)")
        }

        if commandArgs.contains("--clear") {
            try runBlocking { try await store.reset(key) }
            if jsonOutput { print(jsonString(["default_display": NSNull()])) }
            else { print("Cleared dev window display default.") }
            return
        }

        let positional = commandArgs.filter { !$0.hasPrefix("-") }
        if let raw = positional.first {
            let name = raw.trimmingCharacters(in: .whitespacesAndNewlines)
            guard !name.isEmpty else {
                throw CLIError(message: "window default-display requires a display name, or --clear")
            }
            try runBlocking { try await store.set(name, for: key) }
            if jsonOutput { print(jsonString(["default_display": name])) }
            else { print("Dev builds will open on \"\(name)\" (DEBUG builds, applied at window creation).") }
            return
        }
🤖 Prompt for 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.

In `@CLI/cmux.swift` around lines 7316 - 7334, The window default-display command
currently silently ignores unsupported flags (e.g., typos like --cleer),
allowing them to fall through to read mode and hiding user mistakes. Add
validation to reject unrecognized flags before processing the command. After
filtering out positional arguments, check if any remaining items in commandArgs
start with a hyphen but aren't the supported --clear flag, and throw a CLIError
with an appropriate message listing the supported options if such unsupported
flags are found. This validation should occur early in the command handling
logic to catch user errors immediately.

22201-22205: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Do not persist non-authoritative fallback surfaces into session ownership.

You correctly compute resolvedSurface.isAuthoritative, but these upsert paths still persist surfaceId unconditionally. That lets a borrowed fallback surface be written into the session record and later reused as if authoritative, which can misclassify stale hooks across panes.

Suggested change
-                    surfaceId: surfaceId,
+                    surfaceId: resolvedSurface.isAuthoritative ? surfaceId : nil,

As per coding guidelines, cache-substitution correctness requires cached/borrowed identity values to not silently become authoritative without explicit freshness/authority guarantees.

Also applies to: 22450-22460

🤖 Prompt for 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.

In `@CLI/cmux.swift` around lines 22201 - 22205, The code correctly computes
resolvedSurface.isAuthoritative and uses it to conditionally set surfaceId in
one location (lines 22201-22205), but the upsert/persist operations still write
surfaceId unconditionally into the session record. This allows a
non-authoritative fallback surface to be persisted as if it were authoritative,
causing incorrect reuse across panes. Apply the same isAuthoritative guard to
all upsert paths that persist surfaceId into the session: ensure that in lines
22201-22205 and the related upsert operations around lines 22450-22460, the
surfaceId is only written to the session when resolvedSurface.isAuthoritative is
true (otherwise pass nil or omit the field entirely to avoid persisting
borrowed/fallback surface identities).

Source: Coding guidelines


23217-23249: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Avoid per-hook surface.list rescans in surface resolution.

claudeHookSurfaceIsListed does a surface.list RPC + linear scan on each check, and resolveSurfaceAllowingFallbackDetailed can invoke it multiple times per hook event. This introduces repeated scalable scans and extra round-trips in a hot path.

Please fetch/list once per resolution (or once per hook handling path), build a Set of ids/refs, and do constant-time membership checks.

As per coding guidelines, .github/review-bot-rules/algorithmic-complexity.md fails per-event scalable-collection scans in hot paths.

🤖 Prompt for 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.

In `@CLI/cmux.swift` around lines 23217 - 23249, The
resolveSurfaceAllowingFallbackDetailed method calls claudeHookSurfaceIsListed
multiple times, each performing a surface.list RPC and linear scan, which
violates algorithmic complexity guidelines for hot paths. Refactor to fetch the
surface list once at the start of resolveSurfaceAllowingFallbackDetailed, build
a Set containing both the id and ref values from the returned surfaces for
constant-time lookups, then replace the two claudeHookSurfaceIsListed calls with
direct Set membership checks against the pre-built collection. This eliminates
redundant RPC calls and linear scans while maintaining the same verification
logic.

Source: Coding guidelines

@kays0x

kays0x commented Jun 14, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@kays0x

kays0x commented Jun 14, 2026

Copy link
Copy Markdown
Contributor Author

@cubic-dev-ai review

@cubic-dev-ai

cubic-dev-ai Bot commented Jun 14, 2026

Copy link
Copy Markdown

@cubic-dev-ai review

@kays0x I can't start this review because your workspace has reached its free monthly review limit. cubic has reviewed 241,260 of the 240,000 allowed lines of code this month. Reviews resume on 1 July 2026 (in 17 days). Paid plans include much higher monthly review limits. Upgrade now to resume reviews.

To help optimise your usage, you can tune cubic to get the most out of your usage limits:

Learn more →

@coderabbitai

coderabbitai Bot commented Jun 14, 2026 •

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@kays0x

kays0x commented Jun 17, 2026

Copy link
Copy Markdown
Contributor Author

@lawrencecchen would appreciate a look at this one. It still reproes on v0.64.16: SocketClient.send() only reconnects in relay mode, so the cached control-socket path never does, and connectionAppearsOpen() (CLI/cmux.swift ~L2083) is still uncalled from send(). After the server's 30s SO_RCVTIMEO idle-close, resize writes hit a dead fd and silently stop ~30s after attach. This is the real fix for #4960 (#4807 only made the server multi-request, not this path).

Two-commit red/green; bots green, the red checks are just the fork Vercel deploys. Happy to rebase if the CLI refactor moved things around.

Related: #5809 is a separate PR for the keepalive override bug on the same ssh path. Thanks!

@kays0x

kays0x commented Jun 23, 2026

Copy link
Copy Markdown
Contributor Author

Closing as superseded. As of v0.64.17 the user-visible cmux ssh resize freeze is resolved by #5989 (reconcile remote PTY size after arming SIGWINCH — fixes the attach-time lost-resize race) and #6432 (resize under SSH ControlMaster). Verified on a stock v0.64.17 build: remote-pane resize propagates correctly with no manual nudge.

This PR targeted a distinct root cause — the long-lived cached SocketClient in the CLI sending resize into a control socket the server already closed after its 30s idle timeout (send() never reconnects). That hardening is still technically absent from send() on main, but it's no longer producing an observable resize regression, so I'm closing rather than rebasing onto the post-#5989/#6432 refactor. Happy to reopen with just the reconnect hardening if maintainers want the latent correctness fix.

@kays0x kays0x closed this Jun 23, 2026
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.

1 participant