Skip to content

fix: Abort connection retries on permanent socket errors and report the syscall error verbatim - #2019

Merged
hatayama merged 4 commits into
feature/cli-discoverability-integrationfrom
fix/permanent-connect-error-diagnostics
Jul 27, 2026
Merged

hatayama merged 4 commits into
feature/cli-discoverability-integrationfrom
fix/permanent-connect-error-diagnostics

Conversation

@hatayama

@hatayama hatayama commented Jul 27, 2026 •

Copy link
Copy Markdown
Owner

Summary

  • A uloop command whose connection the operating system refuses now fails immediately and names the real reason, instead of retrying for 60 seconds and reporting i/o timeout.
  • The failure is reported as permanent (Retryable: false, SafeToRetry: false) with next actions that point at sandboxing and socket permissions rather than at waiting.

User Impact

Before: when connect() was refused with EPERM/EACCES — an agent's sandboxed shell denying the Unix socket, or wrong permissions on the socket path — the project runner kept retrying for its full 60-second window and then reported the window's own deadline expiry:

Unity is running but the Unity CLI Loop server is not responding:
the Unity CLI Loop server is not reachable for this project:
dial unix /tmp/uloop-501/UnityCliLoop-....sock: i/o timeout
Retryable: true / SafeToRetry: true
NextActions: "Wait and retry; Unity may be starting, importing assets, compiling, ..."

The per-attempt error (connect: operation not permitted) existed only in the debug VibeLog. Following the advice cost 60 seconds per attempt and sent one investigation after a Unity Editor that was healthy the whole time.

After: the first refused attempt ends the command and reports itself:

ErrorCode: UNITY_NOT_REACHABLE
Message: The operating system refused the connection to the Unity CLI Loop server for this project.
Retryable: false / SafeToRetry: false
Details.Cause: dial unix /tmp/uloop-501/UnityCliLoop-....sock: connect: operation not permitted
NextActions:
  - Retrying will not help: the connection was refused before it reached Unity, and the Editor never saw it.
  - If this command ran inside a sandbox ... run it with sandboxing disabled for this command.
  - Otherwise check the ownership and permissions of the endpoint path in Details.

The same abort applies to the readiness wait used by uloop launch, which previously polled out its own timeout under the same condition.

Dial failures the retry window exists for — the socket not created yet, nobody listening yet, a deadline expiry — keep being retried unchanged.

Changes

  • New shared classifier for a connect the operating system refused, used by every dial-retry path and by the error envelope, so the rule lives in one place. It tests os.ErrPermission so a Windows named pipe's ERROR_ACCESS_DENIED counts as well as POSIX EPERM/EACCES, scoped to a connection attempt so file permission failures reaching the same callers are not mistaken for a refused dial.
  • The project runner's undispatched-connection retry no longer accepts a permanently refused connect, so the send returns that error verbatim instead of being replaced by the retry window's deadline error.
  • The shared tool-readiness wait returns a permanently refused connect at the first probe instead of polling until its timeout.
  • The pause-point wait dials on every poll too, so it now aborts at the first refused connect instead of spending the whole --timeout on it.
  • The connection-attempt envelope gained a permanent branch: syscall text verbatim, no retry flags, sandbox/permission guidance.
  • docs/claude-code-sandbox.md: the Symptom section now describes the new output (and marks i/o timeout as evidence of an older CLI); the Cause section records that the block is not transport-specific — with the default allowedHosts policy localhost TCP is stopped too, and a plain uloop ... command works only because excludedCommands takes it out of the sandbox.
  • scripts/stamp-release-inputs.sh run in this PR, since non-test cli/common sources changed.

Verification

  • scripts/check-go-cli.sh — exit 0, 0 issues. for every module, no FAIL lines.
  • New end-to-end test over the real send path (TestSendWithTransientConnectionRetryAbortsOnRefusedConnect): a real listening Unix socket chmod'ed to 000, so the errno comes from the kernel. Reverting the retry-predicate change reproduces the incident exactly and fails the test: --- FAIL (60.01s) ... not responding: ... i/o timeout. With the fix it passes in 0.00s.
  • Mutation-checked each production change by temporarily disabling it: retry predicate → TestShouldNotRetryPermanentlyRefusedConnection red; readiness abort → TestWaitForToolReadinessReturnsPermanentlyRefusedConnectImmediately red in 0.10s with the wrapped timeout error; pause-point abort → TestWaitForPausePointAbortsWhenTheConnectIsRefused red after 7432 polls over the full 10s timeout; envelope branch → TestClassifyConnectionAttemptErrorForRefusedConnect red with Retryable:true, SafeToRetry:true; classification narrowed back to the two POSIX errnos → the named-pipe test red, and dropped the connection-attempt scope → the file-permission test red.
  • GOOS=windows go vet passes for the touched packages in both modules.
  • Not verified: an end-to-end run of the released binary against a real sandbox denial. In this session the sandbox no longer refuses the dev binary's socket connect (dist/darwin-arm64/uloop compile succeeded), so the live EPERM environment that produced the incident is not reproducible here; the socket-permission test above exercises the same errno path through the same code.
  • Repository CI does not run for pull requests targeting the integration branch; the local scripts/check-go-cli.sh run above is the equivalent evidence, and CI runs on the final integration pull request.

hatayama added 2 commits July 27, 2026 10:10
A connect refused with EPERM or EACCES cannot become reachable while the
caller waits, but both dial-retry paths treated every connection attempt
error as transient. The project runner spent its full 60-second unity-alive
window on it and then reported the window's own deadline as `i/o timeout`,
so the syscall error the first attempt already had never reached the caller;
the readiness wait behaved the same way over its own timeout. The reported
envelope then advised waiting and retrying a condition that never clears,
which is what sent a 2026-07-26 investigation after a healthy Editor.

Both loops now stop at the first refused connect and report that error, and
its envelope states the syscall text verbatim with Retryable and SafeToRetry
false plus guidance pointing at sandboxing and socket permissions.
…ansport scope

The Symptom section described the old misdiagnosis as current behavior, which
would now read as a bug against the fixed CLI. It also implied the block was a
Unix socket property, and that reading produced the wrong inference that a TCP
transport would get through: with the default allowedHosts policy the sandbox
stops localhost TCP too, and a plain `uloop` command succeeds only because
excludedCommands removes it from the sandbox.

The stamp files move because this change touches non-test cli/common sources,
which CI requires to reach every affected release.
@coderabbitai

coderabbitai Bot commented Jul 27, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: b14fa2a8-867d-4bc0-bd36-458ef991711b

📥 Commits

Reviewing files that changed from the base of the PR and between 8fff41c and b8becdb.

📒 Files selected for processing (1)
  • cli/common/errors/transport_errors_windows_test.go

📝 Walkthrough

Walkthrough

Adds permanent connect-error classification for permission-denied socket failures, propagates those errors through readiness and pause polling, prevents retries, expands coverage, updates generated input hashes, and revises sandbox failure documentation.

Changes

Permanent connection error handling

Layer / File(s) Summary
Permanent connection classification and envelopes
cli/common/errors/*
Permission-based connection failures are identified as permanent, classified as non-retryable, and reported with refusal-specific details and next actions.
Connection retry gating
cli/project-runner/internal/projectrunner/connection_retry.*
Undispatched sends stop retrying permanent connection failures while transient dial failures remain retryable.
Readiness and pause polling termination
cli/common/clicore/tool_readiness.*, cli/project-runner/internal/projectrunner/pause_point_wait_poll.*
Readiness and pause-point polling return permanent connection errors immediately instead of waiting for timeout.
Sandbox guidance and generated input updates
docs/claude-code-sandbox.md, cli/*/shared-inputs-stamp.json
Sandbox documentation reflects immediate permission-denied failures and both shared-input hashes are updated.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant CLI
  participant ReadinessPolling
  participant ConnectionClassifier
  participant RetryLoop
  CLI->>ReadinessPolling: probe or query status
  ReadinessPolling->>ConnectionClassifier: classify connection error
  ConnectionClassifier-->>ReadinessPolling: permanent refusal
  ReadinessPolling-->>CLI: return error immediately
  CLI->>RetryLoop: send connection request
  RetryLoop->>ConnectionClassifier: classify connection error
  ConnectionClassifier-->>RetryLoop: permanent refusal
  RetryLoop-->>CLI: abort without retry wait
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: aborting retries on permanent socket errors and surfacing the syscall error.
Description check ✅ Passed The description is directly related to the changeset and accurately explains the permanent-connect handling, reporting, and tests.
Docstring Coverage ✅ Passed Docstring coverage is 82.35% which is sufficient. The required threshold is 80.00%.
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.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/permanent-connect-error-diagnostics

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.

…-connect abort

Matching EPERM and EACCES left Windows out: a named pipe reports access
denial as ERROR_ACCESS_DENIED, which maps to os.ErrPermission but to neither
POSIX errno, so the refusal stayed retryable there and the 60-second window
still replaced it with its own deadline error. The classification now tests
os.ErrPermission, scoped to a connection attempt so file permission failures
reaching the same callers are not mistaken for a refused dial.

The pause-point wait dials on every poll as well, so a refused connect spent
the whole --timeout before reporting itself; it now aborts at the first poll.

The readiness test's own timeout is shorter than one poll interval, so a
regression fails on its assertions instead of hanging until the package
deadline.
The cross-platform test substitutes os.ErrPermission for the status
go-winio actually returns, so it proves the classifier's branch but not the
assumption underneath it: that Go maps ERROR_ACCESS_DENIED to that target.
This test uses the status code itself, so the assumption fails loudly on the
Windows CI job instead of leaving Windows silently retrying a refusal that
never clears.
@hatayama
hatayama merged commit 3f15d79 into feature/cli-discoverability-integration Jul 27, 2026
1 of 2 checks passed
@hatayama
hatayama deleted the fix/permanent-connect-error-diagnostics branch July 27, 2026 01:49
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