Skip to content

fix(cua-driver): retry daemon socket writes on EAGAIN instead of failing fatally (write-side mirror of #1997) - #2036

Merged
f-trycua merged 1 commit into
trycua:mainfrom
LaZzyMan:fix/daemon-write-eagain-retry
Jul 1, 2026
Merged

fix(cua-driver): retry daemon socket writes on EAGAIN instead of failing fatally (write-side mirror of #1997)#2036
f-trycua merged 1 commit into
trycua:mainfrom
LaZzyMan:fix/daemon-write-eagain-retry

Conversation

@LaZzyMan

@LaZzyMan LaZzyMan commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Problem

list_apps (and other tools) intermittently fail with:

MCP error -32603: daemon transport error forwarding `list_apps`: Resource temporarily unavailable (os error 35)

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_request handles EAGAIN asymmetrically:

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.rs into the message above.

That os error 35 has no connect to …: prefix (so not the connect path) and isn't the timed out after 120s read message — by elimination it's write_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, treat WouldBlock/TimedOut as "keep waiting" until an overall deadline (the same 120 s budget as the read loop). send_request uses it instead of write_all. Extracted to core so the retry logic is unit-testable without linking the platform crates (and their Swift/Metal interop) the cua-driver binary pulls in.

Tests

New socket_io tests:

  • retries_through_transient_eagain — N transient WouldBlocks then success.
  • times_out_when_daemon_never_drains — a never-draining writer surfaces TimedOut (not a bare EAGAIN) after the deadline.
  • accumulates_partial_writes — offset bookkeeping for short writes.

Verification status

  • cargo test -p cua-driver-core green (3 socket_io tests + all core).
  • cargo check -p cua-driver green.
  • ⚠️ Not reproduced against a live backpressured daemon: the cua-driver binary 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 the send_request change is a drop-in for the existing write_all.

Summary by CodeRabbit

  • Bug Fixes
    • Improved socket writes to better handle temporary backpressure and partial writes.
    • Reduced failures caused by transient “resource temporarily unavailable” errors when communicating with the daemon.
    • Added a longer overall timeout so requests can keep retrying before failing.
    • Included validation to ensure writes complete reliably across retries and short writes.

…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.
@vercel

vercel Bot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

@LaZzyMan is attempting to deploy a commit to the Cua Team on Vercel.

A member of the Team first needs to authorize it.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@coderabbitai

coderabbitai Bot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The crate now exports a socket write helper that retries partial, interrupted, and backpressured writes until a deadline. Unix send_request now uses that helper with a 120-second deadline instead of direct write_all/flush.

Changes

Socket Write Retries

Layer / File(s) Summary
Core helper and export
libs/cua-driver/rust/crates/cua-driver-core/src/lib.rs, libs/cua-driver/rust/crates/cua-driver-core/src/socket_io.rs
cua-driver-core exports socket_io, which defines write_all_with_retry and unit tests for transient WouldBlock, partial writes, and deadline timeout.
Unix send_request integration
libs/cua-driver/rust/crates/cua-driver/src/serve.rs
Unix send_request uses write_all_with_retry with a 120-second deadline and drops the unused Write import.

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)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

I hopped through bytes with twitchy nose,
Past EAGAIN winds and sleepy throes.
A deadline moon kept me on track,
I wrote it all and flushed on back,
Hop-hop—*thump*—the socket answered back. 🐇

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: daemon socket writes now retry on EAGAIN instead of failing immediately.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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 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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
libs/cua-driver/rust/crates/cua-driver-core/src/socket_io.rs (1)

21-62: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Document the blocking-write precondition for this public helper.

The retry loop has no sleep/backoff: on WouldBlock/TimedOut it immediately re-issues w.write(...). This is safe only because the sole caller sets SO_SNDTIMEO (5s) so each write call blocks before returning TimedOut. 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 from core, consider documenting that the writer must be blocking-with-timeout (the doc comment mentions SO_SNDTIMEO but 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

📥 Commits

Reviewing files that changed from the base of the PR and between e08574c and df22db8.

📒 Files selected for processing (3)
  • libs/cua-driver/rust/crates/cua-driver-core/src/lib.rs
  • libs/cua-driver/rust/crates/cua-driver-core/src/socket_io.rs
  • libs/cua-driver/rust/crates/cua-driver/src/serve.rs

LaZzyMan added a commit to QwenLM/qwen-code that referenced this pull request Jun 26, 2026
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.
shenyankm pushed a commit to shenyankm/qwen-code that referenced this pull request Jun 26, 2026
…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.
@f-trycua
f-trycua merged commit d524d97 into trycua:main Jul 1, 2026
1 of 2 checks passed
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.

2 participants