fix(cua-driver): retry daemon socket writes on EAGAIN instead of failing fatally (write-side mirror of #1997) - #2036
Conversation
…ing fatally send_request's write side had a 5s SO_SNDTIMEO but no retry, so when the daemon was momentarily too busy to read a request (backpressure under concurrent slow system tools — list_apps / get_accessibility_tree / launch_app), the write timed out and surfaced as a fatal "daemon transport error forwarding '<tool>': Resource temporarily unavailable (os error 35)" — even for a tiny request. The read side already tolerates the mirror case (trycua#1997 for trycua#1864): a daemon still working is not a transport failure. Add write_all_with_retry (cua-driver-core::socket_io): write with offset bookkeeping, treat WouldBlock/TimedOut as "keep waiting" until an overall deadline, mirroring the read loop. send_request uses it with the same 120s budget. Extracted to core so the retry logic is unit-tested without linking the platform crates (and their Swift/Metal interop) the cua-driver binary needs. cargo test -p cua-driver-core green (3 socket_io tests: transient-EAGAIN retry, deadline timeout, partial-write accumulation); cargo check -p cua-driver green.
|
@LaZzyMan is attempting to deploy a commit to the Cua Team on Vercel. A member of the Team first needs to authorize it. |
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
📝 WalkthroughWalkthroughThe crate now exports a socket write helper that retries partial, interrupted, and backpressured writes until a deadline. Unix ChangesSocket Write Retries
Sequence Diagram(s)sequenceDiagram
participant unix_send_request as "Unix send_request"
participant write_all_with_retry as "write_all_with_retry"
participant daemon_socket as "daemon socket"
unix_send_request->>write_all_with_retry: bytes + 120s deadline
loop until all bytes are written or deadline expires
write_all_with_retry->>daemon_socket: Write::write
daemon_socket-->>write_all_with_retry: partial write / Interrupted / WouldBlock / TimedOut
end
write_all_with_retry-->>unix_send_request: Ok(()) or TimedOut(bytes_written)
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 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 |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
libs/cua-driver/rust/crates/cua-driver-core/src/socket_io.rs (1)
21-62: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low valueDocument the blocking-write precondition for this public helper.
The retry loop has no sleep/backoff: on
WouldBlock/TimedOutit immediately re-issuesw.write(...). This is safe only because the sole caller setsSO_SNDTIMEO(5s) so eachwritecall blocks before returningTimedOut. A future caller passing a genuinely non-blocking writer (no send timeout) would turn this into a CPU-bound hot loop until the deadline. Since this is now exported fromcore, consider documenting that the writer must be blocking-with-timeout (the doc comment mentionsSO_SNDTIMEObut doesn't state it as a requirement).🤖 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 `@libs/cua-driver/rust/crates/cua-driver-core/src/socket_io.rs` around lines 21 - 62, Document write_all_with_retry as requiring a blocking writer with a send timeout, since the retry path immediately loops on WouldBlock/TimedOut with no backoff. Update the public doc comment on write_all_with_retry in socket_io.rs to state the precondition clearly, referencing the existing SO_SNDTIMEO behavior and warning that non-blocking writers are unsupported and may spin until the deadline.
🤖 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.
Nitpick comments:
In `@libs/cua-driver/rust/crates/cua-driver-core/src/socket_io.rs`:
- Around line 21-62: Document write_all_with_retry as requiring a blocking
writer with a send timeout, since the retry path immediately loops on
WouldBlock/TimedOut with no backoff. Update the public doc comment on
write_all_with_retry in socket_io.rs to state the precondition clearly,
referencing the existing SO_SNDTIMEO behavior and warning that non-blocking
writers are unsupported and may spin until the deadline.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 2e3822f7-afba-4b14-9116-2dbb32ecc995
📒 Files selected for processing (3)
libs/cua-driver/rust/crates/cua-driver-core/src/lib.rslibs/cua-driver/rust/crates/cua-driver-core/src/socket_io.rslibs/cua-driver/rust/crates/cua-driver/src/serve.rs
Ports the fix from upstream trycua/cua#2036 into the vendored driver. A non-blocking daemon socket can return EAGAIN/EWOULDBLOCK mid-write when the peer's receive buffer is momentarily full. The driver treated that as fatal and dropped the connection. Add a bounded retry/poll loop (mirror of the read-side socket_io helper) so transient back-pressure no longer kills the session; only a real timeout or hard error fails the write.
…coordinates (QwenLM#5896) * feat(cua-driver): vendor trycua/cua driver with 1000-normalized coordinate support Vendor libs/cua-driver from trycua/cua into packages/cua-driver as the basis for qwen-code's computer-use backend, adding an opt-in relative (1000x1000 normalized) coordinate mode for Qwen-VL clients. - coord_norm.rs: 0-1000 <-> pixel conversion, per-(pid,window_id) size cache, tools/list description rewrite (TDD, 27 tests) - ToolRegistry: normalized field + invoke input/output hooks - protocol.rs: system-instruction coordinate wording switched by mode - serve.rs: daemon list path description rewrite (input_schema aware) - main.rs: CUA_DRIVER_RS_COORDINATE_SPACE env seed Default coordinate_space=pixels => zero behavior change for existing pixel clients. Set CUA_DRIVER_RS_COORDINATE_SPACE=normalized_1000 to enable. Excludes rust/target build output. * feat(cua-driver): make normalized coordinate scale configurable Add CUA_DRIVER_RS_COORDINATE_SCALE (default 1000) so the normalization full-scale can absorb the Qwen 999-vs-1000 cookbook ambiguity without a recompile. norm_to_px/px_to_norm now take an explicit scale; denormalize_args reads the process-wide COORDINATE_SCALE seeded once at startup from env. * ci(cua-driver): add cross-platform release workflow for vendored driver Standalone GitHub Action that builds, signs, and releases the vendored cua-driver under packages/cua-driver. Adapted from upstream trycua/cua cd-rust-cua-driver.yml: - macOS: universal binary (lipo arm64+x86_64), codesigned + notarized into CuaDriver.app using qwen-code's existing secrets (MAC_CSC_LINK cert + App Store Connect API key notarization); Developer ID identity is auto-discovered from the imported cert. - Linux: x86_64 + arm64, built in debian:11 for a glibc 2.31 floor. - Windows: x86_64 + arm64, unsigned (no EV cert, matches upstream). - Release: softprops/action-gh-release on cua-driver-rs-v* tags or manual dispatch, prerelease. Triggered by tag push (cua-driver-rs-v*) or workflow_dispatch. * chore(cua-driver): rebrand vendored driver as qwen-cua-driver Rename the vendored trycua/cua driver so the fork installs and runs independently of any upstream trycua install: - binary cua-driver -> qwen-cua-driver - bundle CuaDriver.app -> QwenCuaDriver.app - bundle id com.trycua.driver -> com.qwencode.cua-driver Updates the cargo/uia manifests, Info.plist, bundle/proxy launch paths, permission/health-report wording, the install/build scripts, and the cross-platform release workflow. * feat(cua-driver): finish relative-coordinate mode — toggle, scale, zoom/move_cursor - CUA_DRIVER_RS_COORDINATE_SPACE is now a 1/0 toggle (via is_env_truthy); default off keeps pixel mode byte-identical to upstream. - Thread CUA_DRIVER_RS_COORDINATE_SCALE through every coordinate surface (was hardcoded 1000): input denormalization already used it; now the rewritten screenshot dims, the tool/param descriptions, and the agent instructions track the configured scale too. - Normalize zoom (window basis) and move_cursor (screen basis) inputs and rewrite their descriptions, alongside click/double_click/right_click/drag. - Fix zoom on downscaled (Retina) windows: apply the get_window_state resize ratio so the crop lands on the region the agent saw. Normalized mode only; pixel-mode zoom unchanged. All coordinate behavior stays gated on the normalized flag, so the default (pixels) path is unchanged from upstream. * chore(cua-driver): add upstream-sync script (git subtree unusable here) `git subtree split --prefix=libs/cua-driver` hangs on a commit deep in trycua/cua's history, so the subtree add/pull workflow isn't usable for the vendored driver (and a pull would re-split + re-hang every time). Add scripts/sync-from-upstream.sh instead: it git-diffs two upstream refs (never walks the full history, so it dodges the hang), reprefixes the libs/cua-driver delta to packages/cua-driver, and `git apply --reject`s it on top of our local changes — conflicts land as *.rej for manual fixup. Record the vendored version in .vendored-from and document the migration + sync method in the design doc. * chore(cua-driver): exclude vendored driver from qwen-code ESLint The vendored packages/cua-driver tree carries upstream JS (e.g. the test-harness Electron app) that doesn't follow qwen-code's lint rules and fails CI. It is not a workspace package (no package.json) and is not qwen-code TypeScript, so add it to eslint.config.js global ignores — alongside packages/desktop/** — the standard treatment for vendored code. * fix(cua-driver): let start_session revive an idle-reaped session Ports the fix from upstream trycua/cua#2035 into the vendored driver. When a session is reaped for idleness, a subsequent start_session with the same id failed instead of resuming it. Revive the ended session in place so the agent can continue rather than getting a hard error. * fix(cua-driver): retry daemon socket writes on EAGAIN Ports the fix from upstream trycua/cua#2036 into the vendored driver. A non-blocking daemon socket can return EAGAIN/EWOULDBLOCK mid-write when the peer's receive buffer is momentarily full. The driver treated that as fatal and dropped the connection. Add a bounded retry/poll loop (mirror of the read-side socket_io helper) so transient back-pressure no longer kills the session; only a real timeout or hard error fails the write. * fix(cua-driver/linux): stop reporting bare "Clicked" for X11 synthetic clicks Ports the fix from upstream trycua/cua#2025 into the vendored driver. On X11, clicks are delivered via XSendEvent synthetic events, which many toolkits (GTK/SDL/Allegro) ignore because send_event is set. The driver still reported a flat success ("Clicked"), masking that nothing happened. Report the synthetic-delivery caveat honestly so the agent can fall back instead of assuming the click landed. (platform-linux crate is not built on macOS; verified by clean upstream apply and covered by upstream + release-workflow Linux CI.) * fix(cua-driver/windows): list empty-/null-title top-level windows Ports the fix from upstream trycua/cua#2021 into the vendored driver. list_windows filtered out any top-level window whose title was empty or null, so legitimate targets (splash screens, some Electron/game windows, tool windows) were invisible to the agent and unclickable. Include empty-title windows, using class name / process as a fallback label. (platform-windows crate is not built on macOS; verified by clean upstream apply and covered by upstream + release-workflow Windows CI.) * chore(cua-driver): track cherry-picked upstream PRs; fix vendored-from The vendored copy is actually at cua-driver-rs-v0.6.7 (workspace version and all 0.6.7->0.6.8 delta files confirm it), but .vendored-from had drifted to 0.6.8 during an earlier sync-script trial whose code delta was not kept. Left as-is it would make a future sync diff 0.6.8->newer and silently skip the real 0.6.7->0.6.8 fixes. Correct it back to 0.6.7. Also record the four not-yet-merged upstream PRs we carry as cherry-picks (trycua/cua#2021/QwenLM#2025/QwenLM#2035/QwenLM#2036) in .vendored-patches.md, and have sync-from-upstream.sh point at it so the next sync reconciles them. * ci(cua-driver): satisfy repo yamllint on the release workflow The vendored-driver release workflow tripped 114 quoted-strings violations under the repo's .yamllint (quote-type: single, required). Single-quote all string scalars to match every other workflow in .github/workflows. While reformatting, the release-notes body also got its paragraph blank lines collapsed and still referenced the old CUA_DRIVER_RS_COORDINATE_SPACE= normalized_1000 value — restore the blank lines and update it to the current 0/1 toggle (default 0 = off; optional CUA_DRIVER_RS_COORDINATE_SCALE=1000). * chore(cua-driver): sync vendored driver to cua-driver-rs-v0.6.8 First real run of scripts/sync-from-upstream.sh: it 3-way-applied the upstream 0.6.7->0.6.8 delta onto our local fork. 10/12 files applied cleanly; the 2 rejects (install.ps1, _install-rust.sh) were already-applied baked-version bumps (0.6.6->0.6.7, our copies were already at 0.6.7), i.e. no real conflict. 0.6.8 brings: Wayland input path (platform-linux), linux health_report + overlay tweaks, a platform-macos build.rs step, and dependency bumps. Version moved to 0.6.8 across the workspace. Verified our work survived the sync untouched: the relative-coordinate shim (coord_norm/protocol) and all four cherry-picked PRs (socket_io/session + linux/windows) are intact — in particular the 0.6.8 edit to platform-linux tools/impl_.rs landed alongside our QwenLM#2025 change with no collision. macOS cargo check + 132 core tests green. (platform-linux/windows + the binary integration test build only on their own runners; upstream CI covers those.) * ci(cua-driver): add a dry_run gate to the release workflow Mirror the desktop-release / release dry-run pattern: a workflow_dispatch dry_run boolean input (default true). The cross-platform build + package jobs always run and upload their artifacts; the GitHub Release job now publishes only on a tag push or an explicit dry_run=false dispatch. Lets us rehearse the whole build/package pipeline (dry_run=true, notarize=false) and inspect the produced artifacts without cutting a release. A branch push (no tag, not a dispatch) likewise builds without releasing.
Problem
list_apps(and other tools) intermittently fail with:even though the request is tiny. Seen across a large fraction of trajectories; the heavy hitters are slow system queries (
list_apps,get_accessibility_tree,launch_app).Root cause
serve.rs::send_requesthandles EAGAIN asymmetrically:WouldBlock/TimedOutas "daemon still working, keep waiting" up to a 120 s deadline — the fix(cua-driver): don't treat socket read-timeout (EAGAIN) as fatal in daemon proxy (#1864) #1997 fix for cua-driver MCP daemon proxy returns os error 35 forwarding get_window_state #1864.SO_SNDTIMEO(set_write_timeout) but no retry:w.write_all(...)?propagates the timeout as a bareos error 35.When the daemon (a machine-wide singleton serving every proxy) is momentarily too busy to read — backpressure from concurrent slow system tools — the proxy's write times out and the bare EAGAIN becomes a fatal transport error, wrapped by
proxy.rsinto the message above.That
os error 35has noconnect to …:prefix (so not the connect path) and isn't thetimed out after 120sread message — by elimination it'swrite_all, the only bare-EAGAIN site left after #1997 fixed the read side. A daemon momentarily not reading is the exact write-side mirror of a daemon still computing a response: not a transport failure.Fix
cua-driver-core::socket_io::write_all_with_retry: write with offset bookkeeping, treatWouldBlock/TimedOutas "keep waiting" until an overall deadline (the same 120 s budget as the read loop).send_requestuses it instead ofwrite_all. Extracted tocoreso the retry logic is unit-testable without linking the platform crates (and their Swift/Metal interop) thecua-driverbinary pulls in.Tests
New
socket_iotests:retries_through_transient_eagain— N transientWouldBlocks then success.times_out_when_daemon_never_drains— a never-draining writer surfacesTimedOut(not a bare EAGAIN) after the deadline.accumulates_partial_writes— offset bookkeeping for short writes.Verification status
cargo test -p cua-driver-coregreen (3 socket_io tests + all core).cargo check -p cua-drivergreen.cua-driverbinary doesn't link on my machine (unrelated platform-macos Swift/Metal interop vs the local SDK) and I can't re-run the eval that surfaced this. The retry logic is unit-tested and thesend_requestchange is a drop-in for the existingwrite_all.Summary by CodeRabbit