Skip to content

Prevent stalled remote PTY starts and bound wedged reattach loops - #9111

Merged
austinywang merged 40 commits into
mainfrom
issue-8750-cmuxd-remote-attach-wedge
Jul 29, 2026
Merged

austinywang merged 40 commits into
mainfrom
issue-8750-cmuxd-remote-attach-wedge

Conversation

@austinywang

@austinywang austinywang commented Jul 29, 2026 •

Copy link
Copy Markdown
Contributor

Closes #8750

Issue: #8750

Root cause

The persistent PTY path had several lifecycle hazards that compose into the reported affected-slot-only wedge:

  • wsPTYHub.prepareAttachment allocated the PTY and started the shell while holding the hub-global mutex. One stalled OS-level start therefore serialized every new attach/session operation in that daemon, while already-running PTY pumps could keep streaming.
  • pty.attach shared the synchronous RPC read loop. A request that stopped making progress could retain per-request state and block unrelated work on that transport; timed-out callers had no protocol operation that canceled the in-flight server request.
  • attachment identity was optional, cleanup did not fully distinguish persistent and anonymous starts, and fast-exit generations could remain visible as live state. Reattach churn could therefore retain attachments, contexts, start ownership, or short-lived generations longer than intended.
  • the client flattened useful daemon subcodes and treated a bridge that closed without confirmed exit as status 254 forever. Replayed scrollback also looked like fresh progress, so the wrapper had no reliable consecutive no-progress budget.

These mechanisms match the forensic split in #8750: existing attachments stay usable, but new attachment establishment on one old persistent server stops progressing.

Fix

Isolated, bounded server lifecycle

  • Reserve session starts under the hub mutex, then allocate/start the PTY after releasing it.
  • Coalesce concurrent callers for the same session onto one start while independent sessions and healthy reattaches continue.
  • Dispatch pty.attach asynchronously and bound each RPC connection to 32 in-flight attaches.
  • Bound each hub to 16 start owners and 64 coalesced start waiters. Capacity pressure returns structured unavailable instead of creating unbounded goroutines.
  • Make attach work connection- and operation-context-aware. Disconnects, shutdown, cancellation, and async response-write failures release reservations without waiting behind a stalled start.
  • Separate persistent-session retention from anonymous-session cleanup; terminate a canceled anonymous start, retain persistent work only under its explicit lifecycle, and keep shutdown/idle reaping bounded.
  • Separate retained fast-exit generations from genuinely live sessions, and bound exit-output draining so teardown cannot wedge indefinitely.

Cancellable attachment protocol

  • Require stable attachment IDs for persistent PTY operations and retain the server-issued attachment token for write/resize/detach identity.
  • Add pty.attach.cancel, keyed by request plus session/attachment identity, so a timed-out attach is canceled without destroying healthy calls on the same transport.
  • Require the cancellation capability for persistent PTYs, causing an old daemon to be upgraded rather than silently using the leaky protocol.
  • Bound the client cancellation write to one second; if a congested writer cannot deliver it, reset that transport so cancellation itself cannot wedge.
  • Release completed-call contexts and linearize cancellation versus late start publication.

Diagnosable, bounded client recovery

  • Preserve structured daemon error codes through the Swift RPC and bridge layers. unavailable maps to status 251 (retry without reauthentication) instead of the generic fatal path.
  • Return status 252 only when a ready bridge closes before 30 seconds with zero live PTY output. The server reports replay length and the client excludes replayed scrollback from progress.
  • Generate both wrapper paths from one retry policy and stop after three consecutive status-252 attempts with a specific localized diagnostic and full surface/lifecycle cleanup.
  • Preserve normal 254/255 transport/authentication behavior and the existing session-not-found respawn path; other outcomes reset or leave the no-progress budget as appropriate.

The bounded fatal path intentionally does not kill the entire remote daemon automatically, because an affected slot can contain unrelated non-tmux work. It prevents the infinite loop while the server changes remove and contain the shared lifecycle failure modes.

Reproduction steps (verbatim from #8750)

Probabilistic — the internal state accumulates over days:

  1. cmux ssh <host> --ssh-option ClearAllForwardings=yes and keep the slot's persistent-server alive for several days.
  2. Open many tabs on that host over time (each running tmux attach), and put the client through heavy reattach churn (laptop sleep/wake, network changes).
  3. Eventually, opening a new tab on that one slot hangs with the loop above, while existing tabs on the same host still work.

Regression coverage

The commit history keeps the regressions and fixes separate. Behavior coverage includes:

  • stalled PTY allocation versus healthy reattach and independent starts
  • same-session start coalescing; 16-owner/64-waiter capacity bounds; shutdown and disconnect races
  • stdio and WebSocket RPC progress while another attach is stalled
  • attach timeout cancellation, late publication, async write failure, and context release
  • persistent versus anonymous cancellation/cleanup and fast-exit generation visibility
  • structured daemon error propagation and status-251 capacity recovery
  • status-252 consecutive limits, ordinary-status reset behavior, cleanup at exhaustion, and real CLI exit behavior
  • replayed scrollback versus live PTY progress
  • persistent reattach and transport/server replacement lifecycle behavior

Local validation on final HEAD 0a68c39c07:

  • CmuxFoundation: 155/155 tests
  • CmuxRemoteDaemon: 25/25 tests
  • CmuxRemoteSession: 111/111 tests
  • CmuxRemoteWorkspace: 87/87 tests
  • full Go ordinary and race suites
  • focused cancellation/identity and persistent-reattach tests repeated 20×
  • lifecycle stress cases repeated up to 100× ordinary and 50× under the race detector
  • strict determinism, project normalization/test wiring, package policy, localization JSON/key parity, and diff checks
  • no local xcodebuild test or XCUITest, per issue instructions

Exact-HEAD CI: 18/18 jobs passed in https://github.com/manaflow-ai/cmux/actions/runs/30450715809, including remote-daemon, Swift-package, app-host, release-build, and workflow guard lanes. CodeRabbit and Socket checks passed; all review threads are resolved.

Localization audit: the terminal diagnostics use String(localized:); English and Japanese entries are present in Resources/Localizable.xcstrings. There is no web-facing string change. The Swift warning budget was untouched; the repository's current file/package guards passed.

Tagged dev-build verification

Cloud build https://github.com/manaflow-ai/cmux/actions/runs/30454380014 succeeded at the exact final HEAD. In the isolated issue-8750-cmuxd-remote-attach-wedge app, using cmux ssh austinwang@127.0.0.1 --port 2296 --ssh-option ClearAllForwardings=yes with zsh -il:

  • six simultaneous surfaces attached to one persistent PTY and all received shared live output
  • 20 attach/use/detach churn cycles completed; attachment count returned to exactly six and the original surface remained responsive
  • killing the tagged SSH transport produced a replacement in four seconds; all six attachments recovered, the original surface worked immediately, and a new surface attached on its first check
  • killing the tagged persistent server produced a fresh server; all six surfaces restored, the original surface accepted input immediately, and a new surface attached and worked on its first check
  • final baseline: six surfaces, six server attachments, the original surface responsive, and the replacement persistent server healthy

Residual uncertainty

The original multi-day Ubuntu 24.04 state was not available for a goroutine dump, and its probabilistic ~3.5-day trigger cannot be recreated within this run. The verification therefore covers the identified failure mechanisms deterministically and stress-tests reattach/transport/server recovery, but does not claim an end-to-end reproduction of the exact multi-day host state.

@coderabbitai

coderabbitai Bot commented Jul 29, 2026 •

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It 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 reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The change adds SSH PTY bridge exit-code classification and bounded no-progress retries, centralizes retry-loop generation, updates bridge reconciliation, and adds localization and regression coverage. It also coordinates concurrent remote PTY session startup, RPC cancellation, and shutdown handling.

Changes

SSH PTY retry handling

Layer / File(s) Summary
Retry classification and exit contracts
Packages/macOS/CmuxFoundation/Sources/CmuxFoundation/SSHPTYAttachExitCode.swift, CLI/SSHPTYAttachExitCode.swift, CLI/CMUXCLI+SSHPTYAttachBridge.swift, Resources/Localizable.xcstrings
Defines exit code 252, no-progress classification and retry-budget helpers, bridge-failure classifiers, wrapper retry decisions, and localized closure messages.
Retry loop generation and wiring
Packages/macOS/CmuxFoundation/Sources/CmuxFoundation/SSHPTYAttachExitCode.swift, Sources/SSHPTYAttachStartupCommandBuilder.swift, CLI/cmux.swift
Generates shared shell retry-loop lines and uses them for SSH PTY startup and reconnect flows.
Bridge EOF reconciliation and validation
CLI/cmux.swift, CLI/CMUXCLI+SSHPTYAttachBridge.swift, cmuxTests/*, cmux.xcodeproj/project.pbxproj
Tracks bridge readiness and output, classifies EOF closures, updates surface cleanup and errors, and tests no-progress exhaustion and retry-streak reset behavior.

Remote PTY hub coordination

Layer / File(s) Summary
Session startup coordination
daemon/remote/cmd/cmuxd-remote/ws_pty.go
Tracks in-progress starts per session, coordinates concurrent attachments, separates session preparation from goroutine execution, and rejects starts after hub closure.
Context-aware PTY RPC dispatch
daemon/remote/cmd/cmuxd-remote/main.go, daemon/remote/cmd/cmuxd-remote/ws_pty.go
Dispatches PTY attachments asynchronously with bounded concurrency, propagates connection cancellation, and prevents attachment tracking after server shutdown.
Concurrency and shutdown regression coverage
daemon/remote/cmd/cmuxd-remote/*_test.go
Tests stalled starts, duplicate allocation, bounded ownership, prompt shutdown, disconnect cancellation, and asynchronous response failures.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant PTYBridge
  participant CMUXCLI
  participant RetryLoop
  participant RemotePTY
  PTYBridge->>CMUXCLI: report EOF with output and uptime
  CMUXCLI->>RetryLoop: classify closure and retry budget
  RetryLoop-->>CMUXCLI: return exit code
  CMUXCLI->>RemotePTY: reconcile or retry attach
Loading
sequenceDiagram
  participant AttachRequest
  participant rpcRequestDispatcher
  participant rpcServer
  participant wsPTYHub
  AttachRequest->>rpcRequestDispatcher: dispatch pty.attach
  rpcRequestDispatcher->>rpcServer: handle attachment with context
  rpcServer->>wsPTYHub: prepareAttachment
  wsPTYHub-->>rpcServer: publish or reuse session
  rpcServer-->>rpcRequestDispatcher: write attach response
Loading

Important

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

❌ Failed checks (2 errors, 1 warning)

Check name Status Explanation Resolution
Cmux Swift Blocking Runtime ❌ Error Production SSHPTYAttachExitCode adds runtime retry/polling shell loops with sleep; the rule forbids timing-based sync outside tests. Move retry/backoff to a cancellable async/timer/state-transition path; keep sleeps/polling confined to test-only scaffolding.
Cmux Full Internationalization ❌ Error FAIL: the new cli.sshPtyAttach.* keys in Resources/Localizable.xcstrings are only localized for en/ja, but that catalog already supports 20 locales. Add translated stringUnit entries for ar, bs, da, de, es, fr, it, km, ko, nb, pl, pt-BR, ru, th, tr, uk, zh-Hans, zh-Hant for each new key.
Docstring Coverage ⚠️ Warning Docstring coverage is 19.15% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (22 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The code changes address #8750 by moving PTY startup off the hub mutex and bounding zero-output retries with a fatal no-progress path.
Out of Scope Changes check ✅ Passed No clear out-of-scope changes stand out; the added tests, localization, and RPC coordination all support the PTY start/retry fix.
Cmux Swift Actor Isolation ✅ Passed Touched Swift changes are pure helpers/enums/tests; no new @MainActor, nonisolated, Sendable reference types, or UI-store/background access were introduced.
Cmux Browser Automation Off-Main ✅ Passed Diff only changes SSH PTY attach/server files; no browser.* socket commands, WebKit/AppKit waits, or control-policy tests were touched.
Cmux Expensive Synchronous Load ✅ Passed Diff only adds SSH PTY retry/exit-code helpers and shell generation; no agent-history loaders or heavy synchronous file parses were introduced.
Cmux Cache Substitution Correctness ✅ Passed The diff only adds PTY retry/concurrency logic; it doesn’t replace any fresh authoritative persistence/snapshot read with a cached or opportunistic value.
Cmux No Hacky Sleeps ✅ Passed Diff only touched Go/Swift/project/resource files; no covered TS/JS/shell/build/runtime scripts or fixed sleeps were introduced.
Cmux Algorithmic Complexity ✅ Passed Edited paths do only O(1) bookkeeping; the only collection pass is a single sessions.contains in bridge reconciliation, with no nested rescans or repeated sort/filter work.
Cmux Swift Concurrency ✅ Passed PASS: the changed runtime Swift code is synchronous; the only new DispatchQueue.global usage is in XCTest helpers, which the rule permits.
Cmux Swift @Concurrent ✅ Passed No touched Swift file adds async/nonisolated work or @concurrent misuse; the only DispatchQueue use is a test helper, not UI-isolated code.
Cmux Swift Package Boundaries ✅ Passed PASS: the reusable SSH PTY retry policy moved into CmuxFoundation; remaining app-target edits are thin CLI glue and tests, not new reusable domain logic.
Cmux Swiftpm Lockfiles ✅ Passed The PR only edits cmux.xcodeproj sources; no SwiftPM package refs, Package.resolved, or .gitignore lockfile rules changed, so the policy is not violated.
Cmux Swift Logging ✅ Passed HEAD only changes Go files; the Swift files in scope are identical to the parent, so no new Swift logging was introduced.
Cmux User-Facing Error Privacy ✅ Passed Changed user-facing strings stay generic; no vendor names, raw upstream errors, or env var names appear in visible copy.
Cmux Swiftui State Layout ✅ Passed Touched files are CLI/foundation/tests only; scans found no new SwiftUI state/layout patterns (@Published, ObservableObject, GeometryReader, lazy rows, or render-time mutation).
Cmux Architecture Rethink ✅ Passed Swift changes centralize retry policy in one shared enum/helper, add no new Swift timers/locks/observers, and keep clear ownership/invariants.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PR only changes CLI PTY retry logic and tests; no NSWindow/NSPanel/WindowGroup code or cmuxAuxiliaryWindowIdentifiers edits appear in the diff.
Cmux Source Artifacts ✅ Passed All 14 changed paths are intentional source, test, config, or localization files; no artifact, scratch, build, or log outputs were added.
Cmux No Test Or Debug Seam In Production Source ✅ Passed No Sources-path production file adds a DEBUG/test-only seam; the new retry helpers are production policy used by the startup builder.
Cmux No Ambient Global State ✅ Passed No new ambient global state: added behavior stays on CMUXCLI methods and existing enum namespace types; no new file-scope API or singleton.
Title check ✅ Passed The title clearly matches the core change: preventing stalled PTY starts and bounding retry loops.
Description check ✅ Passed The description is thorough and covers summary, root cause, fixes, and extensive testing, though it doesn't follow the template sections exactly.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch issue-8750-cmuxd-remote-attach-wedge

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.

@austinywang

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 29, 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.

@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: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
Sources/SSHPTYAttachStartupCommandBuilder.swift (1)

206-212: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Third divergent shellQuote copy introduced by this extraction.

This file's shellQuote (fast-path regex + single-quote escape) is duplicated in CLI/cmux.swift, and the new SSHPTYAttachRetryLoop.swift (created as part of this refactor) adds a third, slightly different implementation that always wraps in quotes without the safe-pattern fast path. Consider extracting one shared shell-quoting utility that all three call sites use, so quoting behavior can't drift.

🤖 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 `@Sources/SSHPTYAttachStartupCommandBuilder.swift` around lines 206 - 212,
Extract the shell-quoting logic from
SSHPTYAttachStartupCommandBuilder.shellQuote, CLI/cmux.swift, and
SSHPTYAttachRetryLoop.swift into one shared utility, then update all three call
sites to use it. Preserve the existing safe-pattern fast path and single-quote
escaping behavior consistently across every caller, removing the duplicated
implementations.
🤖 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/SSHPTYAttachRetryLoop.swift`:
- Around line 59-64: Update the case branches generated by SSHPTYAttachRetryLoop
to replace the hardcoded 254 and 255 literals with the raw values of
SSHPTYAttachExitCode.bridgeClosedSessionRunning and
SSHPTYAttachExitCode.retryableTransient, matching the existing noProgressStatus
enum-derived branch.

In `@cmuxTests/SSHPTYAttachNoProgressRetryTests.swift`:
- Around line 47-169: Extract the duplicated temporary-directory, fake CLI/fake
sleep, executable-permission, and base environment setup from
repeatedZeroProgressClosuresStop and
normalRetryableClosureResetsNoProgressStreak into a private helper. Parameterize
the helper with the fake CLI’s per-attempt decision behavior and return the
shared test result/artifacts needed for assertions, while keeping each test’s
distinct expected attempts, policy log, status, and stderr assertions in the
tests.

In `@daemon/remote/cmd/cmuxd-remote/ws_pty_test.go`:
- Around line 256-273: Update the concurrent attach test around attach and
releaseStart so it waits until the second caller has entered the in-flight-start
waiting branch before closing releaseStart. Add or use a hub-owned waiter
counter or test hook channel, then apply a deadline-bounded wait and preserve
the existing assertions for both attach results and a single PTY start.

---

Outside diff comments:
In `@Sources/SSHPTYAttachStartupCommandBuilder.swift`:
- Around line 206-212: Extract the shell-quoting logic from
SSHPTYAttachStartupCommandBuilder.shellQuote, CLI/cmux.swift, and
SSHPTYAttachRetryLoop.swift into one shared utility, then update all three call
sites to use it. Preserve the existing safe-pattern fast path and single-quote
escaping behavior consistently across every caller, removing the duplicated
implementations.
🪄 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 Plus

Run ID: 7d20e041-1a7f-496b-aec2-7ca485123958

📥 Commits

Reviewing files that changed from the base of the PR and between c3e1664 and 7cc29bb.

📒 Files selected for processing (11)
  • CLI/CMUXCLI+SSHPTYAttachBridge.swift
  • CLI/SSHPTYAttachExitCode.swift
  • CLI/SSHPTYAttachRetryLoop.swift
  • CLI/cmux.swift
  • Resources/Localizable.xcstrings
  • Sources/SSHPTYAttachStartupCommandBuilder.swift
  • cmux.xcodeproj/project.pbxproj
  • cmuxTests/CLINotifyProcessIntegrationRegressionTests.swift
  • cmuxTests/SSHPTYAttachNoProgressRetryTests.swift
  • daemon/remote/cmd/cmuxd-remote/ws_pty.go
  • daemon/remote/cmd/cmuxd-remote/ws_pty_test.go

Comment thread CLI/SSHPTYAttachRetryLoop.swift Outdated
Comment thread cmuxTests/SSHPTYAttachNoProgressRetryTests.swift
Comment thread daemon/remote/cmd/cmuxd-remote/ws_pty_test.go Outdated
@austinywang austinywang changed the title Prevent stalled remote PTY starts from wedging a slot Prevent stalled remote PTY starts and bound wedged reattach loops Jul 29, 2026

@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: 2

🤖 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/macOS/CmuxFoundation/Sources/CmuxFoundation/SSHPTYAttachExitCode.swift`:
- Line 112: Remove the elapsed-time sleep from the reconnect command assembled
in SSHPTYAttachExitCode. Replace delay-based retry coordination with an explicit
lifecycle/readiness result that determines when the next attach attempt may
proceed, preserving reconnect behavior without timing-based backoff.
- Around line 70-73: Update the noProgressFormat construction in
SSHPTYAttachExitCode to use ICU pluralization, selecting explicit .one and
.other localized catalog entries based on the retry limit so singular output
says “1 attempt” and other counts say “N attempts.” Add matching .one and .other
entries to the localization catalog while preserving remoteCommandShellQuoted
handling.
🪄 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 Plus

Run ID: 4c2f5e34-ada1-4a8a-8ec4-1be1684adb1b

📥 Commits

Reviewing files that changed from the base of the PR and between 3343a17 and 9b15833.

📒 Files selected for processing (8)
  • CLI/CMUXCLI+SSHPTYAttachBridge.swift
  • CLI/SSHPTYAttachExitCode.swift
  • CLI/cmux.swift
  • Packages/macOS/CmuxFoundation/Sources/CmuxFoundation/SSHPTYAttachExitCode.swift
  • Sources/SSHPTYAttachStartupCommandBuilder.swift
  • cmux.xcodeproj/project.pbxproj
  • cmuxTests/SSHPTYAttachExitCodeClassifierTests.swift
  • cmuxTests/SSHPTYAttachNoProgressRetryTests.swift
💤 Files with no reviewable changes (2)
  • CLI/SSHPTYAttachExitCode.swift
  • cmux.xcodeproj/project.pbxproj

Comment thread Packages/macOS/CmuxFoundation/Sources/CmuxFoundation/SSHPTYAttachExitCode.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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
Packages/macOS/CmuxFoundation/Sources/CmuxFoundation/SSHPTYAttachExitCode.swift (1)

48-56: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Fix the no-progress retry boundary.

With limit == 3, currentRetry == 2 represents two prior retries and should allow the third attempt, but currentRetry + 1 < limit returns false. It can also overflow for Int.max.

Proposed fix
-        currentRetry >= 0 && limit > 0 && currentRetry + 1 < limit
+        currentRetry >= 0 && limit > 0 && currentRetry < limit
🤖 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
`@Packages/macOS/CmuxFoundation/Sources/CmuxFoundation/SSHPTYAttachExitCode.swift`
around lines 48 - 56, Update SSHPTYAttachExitCode.hasNoProgressRetryRemaining so
a valid currentRetry allows attempts while it is below limit, including
currentRetry == limit - 1; avoid adding 1 to currentRetry to prevent Int.max
overflow, while preserving the existing nonnegative retry and positive limit
validation.
🤖 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.

Outside diff comments:
In
`@Packages/macOS/CmuxFoundation/Sources/CmuxFoundation/SSHPTYAttachExitCode.swift`:
- Around line 48-56: Update SSHPTYAttachExitCode.hasNoProgressRetryRemaining so
a valid currentRetry allows attempts while it is below limit, including
currentRetry == limit - 1; avoid adding 1 to currentRetry to prevent Int.max
overflow, while preserving the existing nonnegative retry and positive limit
validation.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: b5130ebe-1666-4de9-b410-84ba8886b3e3

📥 Commits

Reviewing files that changed from the base of the PR and between 9b15833 and 4f7fca6.

📒 Files selected for processing (3)
  • CLI/cmux.swift
  • Packages/macOS/CmuxFoundation/Sources/CmuxFoundation/SSHPTYAttachExitCode.swift
  • cmuxTests/SSHPTYAttachNoProgressRetryTests.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.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
daemon/remote/cmd/cmuxd-remote/ws_pty.go (1)

872-886: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Owner-scoped cancellation is propagated to unrelated waiters, and the started session is discarded even when live waiters want it.

At Line 913-914 the start is failed with the owner's ctx.Err() and the freshly started session is discarded. Waiters that joined this start (Line 876-879) then receive context.Canceled verbatim, even though their own connections are healthy — so one client disconnecting during allocation turns into a spurious failure for every other client attaching the same session, and the just-allocated PTY is torn down and must be re-allocated.

Given this PR also bounds client-side retries after consecutive no-progress attaches, these owner-scoped failures can count against unrelated clients' retry budgets.

Suggest distinguishing owner-scoped errors (owner ctx cancellation) from session-scoped errors: on owner cancellation, either hand ownership to a waiter (keep the session and let the loop republish it) or signal waiters to retry the loop instead of returning the owner's error.

♻️ Sketch: let waiters retry instead of inheriting owner cancellation
 		if start := h.startingSessions[sessionKey]; start != nil {
 			closedCh := h.closedCh
 			h.mu.Unlock()
 			select {
 			case <-start.done:
-				if start.err != nil {
+				// Owner-scoped cancellation is not a failure for this caller;
+				// retry the loop and start the session ourselves.
+				if start.err != nil && !start.ownerCanceled {
 					return nil, nil, nil, start.err
 				}

with ownerCanceled set alongside start.err in the case ctx.Err() != nil: branch.

Also applies to: 908-921

🤖 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 `@daemon/remote/cmd/cmuxd-remote/ws_pty.go` around lines 872 - 886, Update the
starting-session wait path around startingSessions and the owner-cancellation
handling near the session allocation failure branch to distinguish owner ctx
cancellation from session-scoped errors. When the allocating owner cancels, do
not return that owner’s context error to unrelated waiters; signal them to retry
the session-start loop, while preserving and republishing a successfully
allocated PTY when live waiters remain. Continue propagating genuine session
errors and each waiter’s own ctx cancellation normally.
🤖 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 `@daemon/remote/cmd/cmuxd-remote/main.go`:
- Around line 388-398: Update rpcRequestDispatcher to track dispatched
goroutines with a sync.WaitGroup, incrementing before each dispatch goroutine
and calling Done on completion, then expose dispatcher.wait(). In
daemon/remote/cmd/cmuxd-remote/main.go lines 388-398, and apply the same
ordering at lines 1318-1321 in runRPCServerWithReader, cancel the connection,
wait for in-flight handlers, and only then call writer.flush(); ensure deferred
teardown does not flush before cancellation and waiting.
- Around line 2264-2274: Update the context-error response in the attachment
flow around ctx.Err() to use the same product-level message as the sibling
connection-loss branch near the subsequent error handling, rather than exposing
err.Error(). Keep the existing cleanup, error code, and response structure
unchanged.
- Around line 143-180: Update rpcRequestDispatcher.dispatch and the async PTY
attach handling to use the existing request-response path for id-less requests,
preserving notification semantics: id-less pty.attach must route through
handleNotificationResponse and emit a pty.error event rather than writing an
unconditional response frame, while requests with IDs retain normal responses
and existing concurrency/error behavior.

---

Outside diff comments:
In `@daemon/remote/cmd/cmuxd-remote/ws_pty.go`:
- Around line 872-886: Update the starting-session wait path around
startingSessions and the owner-cancellation handling near the session allocation
failure branch to distinguish owner ctx cancellation from session-scoped errors.
When the allocating owner cancels, do not return that owner’s context error to
unrelated waiters; signal them to retry the session-start loop, while preserving
and republishing a successfully allocated PTY when live waiters remain. Continue
propagating genuine session errors and each waiter’s own ctx cancellation
normally.
🪄 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 Plus

Run ID: a9ab740c-412a-4d10-b7fd-e01906eff32e

📥 Commits

Reviewing files that changed from the base of the PR and between 4f7fca6 and a541bf5.

📒 Files selected for processing (4)
  • daemon/remote/cmd/cmuxd-remote/main.go
  • daemon/remote/cmd/cmuxd-remote/main_test.go
  • daemon/remote/cmd/cmuxd-remote/ws_pty.go
  • daemon/remote/cmd/cmuxd-remote/ws_pty_test.go

Comment thread daemon/remote/cmd/cmuxd-remote/main.go
Comment thread daemon/remote/cmd/cmuxd-remote/main.go Outdated
Comment thread daemon/remote/cmd/cmuxd-remote/main.go Outdated
@cursor

cursor Bot commented Jul 29, 2026

Copy link
Copy Markdown

Bugbot is paused — on-demand spend limit reached

Bugbot uses usage-based billing for this team and has hit its on-demand spend limit.

A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue.

@cursor

cursor Bot commented Jul 29, 2026

Copy link
Copy Markdown

Bugbot is paused — on-demand spend limit reached

Bugbot uses usage-based billing for this team and has hit its on-demand spend limit.

A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue.

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.

cmuxd-remote persistent-server wedges new-PTY attach after long uptime; client loops "reattaching (attempt 1/∞)" instead of respawning

1 participant