From d4154282b17e208a520190d3c4ef05667bf15e9c Mon Sep 17 00:00:00 2001 From: Francesco Bonacci Date: Wed, 17 Jun 2026 19:44:36 -0700 Subject: [PATCH 1/2] fix(cua-driver-rs)(windows): stop idle overlay CPU + orphan mcp child (#1808) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Windows agent-cursor overlay render timer ran at ~125 Hz unconditionally: every WM_TIMER tick allocated a full virtual-screen tiny-skia pixmap, swizzled it RGBA->BGRA pixel-by-pixel, and blitted it via UpdateLayeredWindow — even when no cursor was animating and the pointer was static. An idle `cua-driver mcp` therefore pinned 60-85% of a CPU core (issue #1808), and long-lived instances accumulated CPU-hours. Part A (idle CPU): mirror the macOS fix (#1865). Add a `needs_frame_tick` predicate (in-flight path / spring / click pulse / unfinished idle-fade) and gate the composite+blit+z-order behind it. The render timer is now re-armed between an ACTIVE cadence (~125 Hz, smooth animation) and a slow IDLE heartbeat (250 ms) once every cursor goes quiescent. `send_command` / `remove_cursor` call `wake_overlay()` to flip back to ACTIVE within ~8 ms via a cross-thread SetTimer, so the first move after idle is not delayed. A final settle frame is still emitted as animations finish, so the layered window is left in its resting/cleared state before the loop parks. No full-screen pixmap allocation, no RGBA->BGRA copy, no UpdateLayeredWindow while idle. Part B (orphan on disconnect): the overlay runs on a detached STA thread with its own Win32 message loop, so returning from `async_main` after the stdio MCP server loop ended (stdin EOF) was not guaranteed to tear it down promptly. The in-process Windows/Linux `mcp` path now `std::process::exit`es once `server::run` returns, mirroring the macOS arm, so the overlay thread dies with the process the moment the client disconnects. Adds headless unit tests for the quiescent-sentinel state, the active-animation state, and the click-pulse-then-quiescent transition. Verified `platform-windows` cross-compiles cleanly for x86_64-pc-windows-msvc and all platform-windows lib tests pass. Co-Authored-By: Claude Opus 4.8 --- .../docs/cua-driver/reference/changelog.mdx | 14 + libs/cua-driver/rust/Cargo.lock | 18 +- .../rust/crates/cua-driver/src/main.rs | 17 +- .../crates/platform-windows/src/overlay.rs | 319 ++++++++++++++---- 4 files changed, 296 insertions(+), 72 deletions(-) diff --git a/docs/content/docs/cua-driver/reference/changelog.mdx b/docs/content/docs/cua-driver/reference/changelog.mdx index dd5fa3b328..8320025cf2 100644 --- a/docs/content/docs/cua-driver/reference/changelog.mdx +++ b/docs/content/docs/cua-driver/reference/changelog.mdx @@ -18,6 +18,20 @@ The canonical, always-current source is the release body is auto-generated per release from the commits touching `libs/cua-driver/rust`, including SHA256 checksums and install instructions. +## 0.5.6 (unreleased) + +- **Windows: fix idle CPU burn and orphaned `mcp` processes from the cursor + overlay** (#1808). The agent-cursor overlay render timer ran at ~125 Hz + unconditionally and re-composited a full virtual-screen pixmap (plus an + RGBA→BGRA copy and `UpdateLayeredWindow` blit) on every tick — so an idle + `cua-driver mcp` with no automation pinned 60–85% of a CPU core. The render + loop is now event-driven: it only composites and blits while a cursor is + actually animating or fading, and drops to a slow heartbeat (waking instantly + on the next command) once every cursor is static. The in-process `mcp` (stdio) + child now force-exits when the client disconnects (stdin EOF / pipe closed) + instead of leaving the overlay loop running as an orphan that accumulates CPU. + Use `--no-overlay` to disable the agent cursor entirely for headless runs. + ## 0.5.4 (2026-06-15) - **macOS: fix 0x0 screenshots on macOS 15+ — the release bundle now ships the diff --git a/libs/cua-driver/rust/Cargo.lock b/libs/cua-driver/rust/Cargo.lock index 5133676757..e9aed8b7cb 100644 --- a/libs/cua-driver/rust/Cargo.lock +++ b/libs/cua-driver/rust/Cargo.lock @@ -486,7 +486,7 @@ dependencies = [ [[package]] name = "cua-driver" -version = "0.5.3" +version = "0.5.6" dependencies = [ "anyhow", "async-trait", @@ -517,7 +517,7 @@ dependencies = [ [[package]] name = "cua-driver-core" -version = "0.5.3" +version = "0.5.6" dependencies = [ "anyhow", "async-trait", @@ -532,7 +532,7 @@ dependencies = [ [[package]] name = "cua-driver-uia" -version = "0.5.3" +version = "0.5.6" dependencies = [ "anyhow", "cua-driver-core", @@ -547,7 +547,7 @@ dependencies = [ [[package]] name = "cursor-overlay" -version = "0.5.3" +version = "0.5.6" dependencies = [ "anyhow", "image", @@ -787,7 +787,7 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "focus-monitor-win" -version = "0.5.3" +version = "0.5.6" dependencies = [ "windows 0.58.0", ] @@ -1591,7 +1591,7 @@ checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "pip-preview" -version = "0.5.3" +version = "0.5.6" dependencies = [ "anyhow", "serde_json", @@ -1617,7 +1617,7 @@ checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" [[package]] name = "platform-linux" -version = "0.5.3" +version = "0.5.6" dependencies = [ "anyhow", "async-trait", @@ -1644,7 +1644,7 @@ dependencies = [ [[package]] name = "platform-macos" -version = "0.5.3" +version = "0.5.6" dependencies = [ "anyhow", "async-trait", @@ -1677,7 +1677,7 @@ dependencies = [ [[package]] name = "platform-windows" -version = "0.5.3" +version = "0.5.6" dependencies = [ "anyhow", "async-trait", diff --git a/libs/cua-driver/rust/crates/cua-driver/src/main.rs b/libs/cua-driver/rust/crates/cua-driver/src/main.rs index 207ce9fe77..bbc0b77375 100644 --- a/libs/cua-driver/rust/crates/cua-driver/src/main.rs +++ b/libs/cua-driver/rust/crates/cua-driver/src/main.rs @@ -714,8 +714,21 @@ async fn async_main() -> anyhow::Result<()> { let registry = Arc::new(build_registry(cursor_cfg)); registry.init_self_weak(); maybe_init_pip(); - cua_driver_core::server::run(registry).await?; - Ok(()) + let result = cua_driver_core::server::run(registry).await; + if let Err(e) = &result { + tracing::error!("MCP server error: {e}"); + } + + // The stdio MCP server loop has ended — the client disconnected (stdin + // EOF) or a fatal I/O error occurred. The cursor overlay runs on its own + // detached thread with an independent Win32 message loop (and we raised the + // multimedia timer resolution via `timeBeginPeriod`), so simply returning + // is not guaranteed to tear it down promptly: that thread is never joined + // and would otherwise keep its render loop alive as an orphan, accumulating + // CPU after the client is gone (issue #1808). Force a clean process exit so + // the overlay thread dies with us the moment the transport closes — mirrors + // the macOS `std::process::exit(0)` after `server::run`. + std::process::exit(if result.is_ok() { 0 } else { 1 }); } // ── Registry builder (non-macOS) ────────────────────────────────────────── diff --git a/libs/cua-driver/rust/crates/platform-windows/src/overlay.rs b/libs/cua-driver/rust/crates/platform-windows/src/overlay.rs index 3c057fc3d1..c7b8c2cdf5 100644 --- a/libs/cua-driver/rust/crates/platform-windows/src/overlay.rs +++ b/libs/cua-driver/rust/crates/platform-windows/src/overlay.rs @@ -197,9 +197,41 @@ pub fn send_command(key: CursorKey, cmd: OverlayCommand) { } if let Some(tx) = CMD_TX.get() { let _ = tx.try_send(OverlayMsg::Cmd(KeyedOverlayCommand { key, cmd })); + wake_overlay(); } } +/// Kick the overlay render timer back to the ACTIVE cadence immediately so a +/// command enqueued while the loop is parked in the slow IDLE heartbeat is +/// picked up within ~8ms instead of waiting out the full idle period (issue +/// #1808). `SetTimer` may be called cross-thread for a window owned by another +/// thread, so this is safe to invoke from the MCP tool threads. No-op until the +/// overlay window exists and a no-op when already ACTIVE. +#[cfg(target_os = "windows")] +fn wake_overlay() { + use std::sync::atomic::Ordering::Relaxed; + if TIMER_PERIOD_MS.load(Relaxed) == TIMER_MS_ACTIVE { + return; // already ticking at frame cadence + } + let hwnd_isize = OVERLAY_HWND.load(Relaxed); + if hwnd_isize == 0 { + return; // overlay window not created yet + } + // Flip the cadence flag first so a racing WM_TIMER doesn't re-park us, then + // arm the ACTIVE-period timer. The WM_TIMER handler re-confirms the cadence + // from render state, so an over-eager wake just costs one cheap idle tick. + TIMER_PERIOD_MS.store(TIMER_MS_ACTIVE, Relaxed); + unsafe { + use windows::Win32::Foundation::HWND; + use windows::Win32::UI::WindowsAndMessaging::SetTimer; + let hwnd = HWND(hwnd_isize as *mut _); + SetTimer(hwnd, TIMER_ID, TIMER_MS_ACTIVE, None); + } +} + +#[cfg(not(target_os = "windows"))] +fn wake_overlay() {} + /// Convenience for callsites not yet threaded with a session key: drives the /// seeded `"default"` cursor (the anonymous / one-shot identity). pub fn send_command_default(cmd: OverlayCommand) { @@ -216,6 +248,7 @@ pub fn remove_cursor(key: CursorKey) { } if let Some(tx) = CMD_TX.get() { let _ = tx.try_send(OverlayMsg::Remove(key)); + wake_overlay(); } } @@ -423,6 +456,35 @@ impl RenderState { // returns `false` for it and we silently drop it here. let _ = self.core.apply_command_base(cmd, false, false); } + + /// True while the render loop must keep ticking at frame cadence because + /// the next tick can still change pixels: an in-flight glide path, a + /// spring-settle, a click pulse, or an idle-fade that has not yet fully + /// faded the cursor out. A brand-new sentinel cursor (off-screen at + /// `(-200, -200)`) and a cursor that has already faded to `idle_alpha ≈ 0` + /// are both quiescent, so `mcp`/`serve` with no agent activity can let the + /// timer go cheap instead of compositing a full virtual-screen pixmap at + /// ~125 Hz. Mirrors `platform_macos::cursor::overlay`'s `needs_frame_tick`. + #[cfg_attr(not(target_os = "windows"), allow(dead_code))] + fn needs_frame_tick(&self) -> bool { + self.core.path.is_some() + || self.core.spring.is_some() + || self.core.click_t.is_some() + || (self.core.motion.idle_hide_ms > 0.0 + && self.core.visible + && self.core.pos.0 >= -100.0 + && self.core.idle_alpha >= 0.004) + } +} + +/// True if ANY owned cursor still needs frame ticks (animation / fade in +/// progress). When this is false the render loop is fully quiescent: the last +/// emitted frame already left the layered window in its resting / cleared +/// state, so the timer can drop to a slow idle cadence and skip the expensive +/// composite + `UpdateLayeredWindow` until the next command wakes it. +#[cfg_attr(not(target_os = "windows"), allow(dead_code))] +fn render_map_needs_frame_tick(map: &RenderMap) -> bool { + map.cursors.values().any(RenderState::needs_frame_tick) } // ── Win32 message-loop thread ───────────────────────────────────────────── @@ -510,10 +572,14 @@ fn run_overlay_thread(cfg: CursorConfig, rx: std::sync::mpsc::Receiver>> = Mutex: static LAST_ZTICK: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0); static Z_ORDER: OnceLock = OnceLock::new(); +// ── Idle render gate (issue #1808) ──────────────────────────────────────── +// +// The overlay window timer is re-armed between two cadences: +// * ACTIVE (`TIMER_MS_ACTIVE`, ~125 Hz) while any cursor is animating / +// fading — this is what produces a smooth glide + click pulse. +// * IDLE (`TIMER_MS_IDLE`, a slow heartbeat) when every cursor is +// quiescent — the handler then only drains the command channel cheaply +// and re-arms ACTIVE the instant a command arrives. No full-screen pixmap +// allocation, no RGBA→BGRA copy, no UpdateLayeredWindow while idle. +// +// Before this gate the timer ran at ~125 Hz unconditionally and every tick +// allocated a virtual-screen pixmap, swizzled it pixel-by-pixel, and blitted +// it with UpdateLayeredWindow — burning 60–85% of a core with the cursor +// static (issue #1808). `TIMER_PERIOD_MS` is the cadence the timer is currently +// armed at; the WM_TIMER handler flips it based on `render_map_needs_frame_tick`. +const TIMER_ID: usize = 1; +const TIMER_MS_ACTIVE: u32 = 8; // ~125 Hz, matches the C# reference render rate +const TIMER_MS_IDLE: u32 = 250; // slow heartbeat: drain channel, stay responsive +/// Current armed timer cadence in ms. Compared against the desired cadence each +/// WM_TIMER so we only call `SetTimer` (re-arm) on an actual active↔idle flip. +static TIMER_PERIOD_MS: std::sync::atomic::AtomicU32 = + std::sync::atomic::AtomicU32::new(TIMER_MS_ACTIVE); + // ── Window procedure ────────────────────────────────────────────────────── #[cfg(target_os = "windows")] @@ -593,19 +682,30 @@ unsafe extern "system" fn wnd_proc( } } - // ── Drain commands, tick all cursors, composite one pixmap ─────── + // ── Drain commands, tick all cursors, maybe composite one pixmap ─ // Measure real dt from last tick — Windows timer resolution defaults // to 15ms so the hardcoded 8ms ran the animation at half speed. - let (pixmap, arrived, pinned_wid) = { + // + // Idle gate (issue #1808): the full-screen composite + RGBA→BGRA + // swizzle + UpdateLayeredWindow only runs when a command arrived + // this tick, when a previous tick left an animation in flight + // (`was_active`), or when a cursor is still animating/fading after + // this tick (`needs_tick`). When all three are false every cursor is + // quiescent and the layered window already holds its resting frame, + // so we skip the expensive work entirely and let the timer drop to + // the slow IDLE cadence below. + let was_active = + TIMER_PERIOD_MS.load(std::sync::atomic::Ordering::Relaxed) == TIMER_MS_ACTIVE; + let (pixmap, arrived, pinned_wid, needs_tick) = { let mut guard = RENDER.lock().unwrap(); if let Some(map) = guard.as_mut() { // Drain the channel via get-or-create; track the last-touched // key so the z-order pin follows the most-recent cursor. - let mut drained = 0u32; + let mut had_msg = false; if let Ok(rx_guard) = CMD_RX_WIN.try_lock() { if let Some(ref rx) = *rx_guard { while let Ok(m) = rx.try_recv() { - drained += 1; + had_msg = true; if let Some(k) = apply_msg(map, m) { map.last_active = Some(k); } @@ -628,61 +728,90 @@ unsafe extern "system" fn wnd_proc( } } - // Decide where to pin the single overlay window in z. - // - // The overlay is ONE full-virtual-screen layered window, so it - // can occupy only one z-slot. It must sit ABOVE every window a - // live cursor is actuating, but NOT above whatever sits above - // those (the user's foreground). The right slot is therefore - // "just above the HIGHEST-z actuating window": the overlay is - // full-screen, so being above the topmost driven window puts it - // above all of them (they're all at-or-below it), while still - // below anything stacked above them. Pinning above one fixed - // window (the old last-active behaviour) instead let any other - // driven window stacked above it occlude its cursors — the - // blink-out. NB: this is a RELATIVE z move (insert above a - // specific window), which works from this non-foreground - // thread; an absolute HWND_TOP can be refused by the - // foreground lock and sink the overlay behind everything. - let mut driven: Vec = Vec::new(); - for rs in map.cursors.values() { - if !rs.core.visible || rs.core.idle_alpha < 0.004 { continue; } - if let Some(w) = rs.core.pinned_wid { - if !driven.contains(&w) { driven.push(w); } + // After ticking: does any cursor still need frame ticks? + let needs_tick = render_map_needs_frame_tick(map); + + // Render only when something can have changed pixels this + // frame: a fresh command, a still-running animation, or the + // final settle frame as the previous animation winds down + // (`was_active && !needs_tick`). A fully-quiescent idle tick + // returns `None` here and does no compositing at all. + let should_render = had_msg || needs_tick || was_active; + + if !should_render { + (None, arrived, None, needs_tick) + } else { + // Decide where to pin the single overlay window in z. + // + // The overlay is ONE full-virtual-screen layered window, + // so it can occupy only one z-slot. It must sit ABOVE + // every window a live cursor is actuating, but NOT above + // whatever sits above those (the user's foreground). The + // right slot is therefore "just above the HIGHEST-z + // actuating window": the overlay is full-screen, so being + // above the topmost driven window puts it above all of + // them while still below anything stacked above them. + // Pinning above one fixed window (the old last-active + // behaviour) instead let any other driven window stacked + // above it occlude its cursors — the blink-out. NB: this + // is a RELATIVE z move (insert above a specific window), + // which works from this non-foreground thread; an + // absolute HWND_TOP can be refused by the foreground lock + // and sink the overlay behind everything. + let mut driven: Vec = Vec::new(); + for rs in map.cursors.values() { + if !rs.core.visible || rs.core.idle_alpha < 0.004 { + continue; + } + if let Some(w) = rs.core.pinned_wid { + if !driven.contains(&w) { + driven.push(w); + } + } + } + let pinned = unsafe { topmost_of(&driven) }; + + // Composite every cursor into ONE virtual-screen pixmap. + // While ACTIVE this runs every ~8ms, so a steady + // per-frame blit keeps resting cursors flicker-free as + // others animate; once the loop goes idle the whole block + // is skipped (the `should_render` gate above). + let w = map.virt_w.max(1) as u32; + let h = map.virt_h.max(1) as u32; + let mut pm = tiny_skia::Pixmap::new(w.max(1), h.max(1)) + .unwrap_or_else(|| tiny_skia::Pixmap::new(1, 1).unwrap()); + for (_k, rs) in &map.cursors { + cursor_overlay::paint_cursor( + &mut pm, + &rs.core, + map.virt_x as f64, + map.virt_y as f64, + None, // focus-rect is macOS-only + ); } - } - let pinned = unsafe { topmost_of(&driven) }; - - // Composite every cursor into ONE virtual-screen pixmap and - // blit it every frame. (An earlier "skip when idle" gate was - // removed: starting/stopping the full-screen UpdateLayeredWindow - // as activity comes and goes made all the resting cursors - // flicker when any one of them clicked — very visible with many - // cursors. A steady per-frame blit is flicker-free, and during - // playback at least one cursor is almost always active anyway.) - let _ = drained; - let w = map.virt_w.max(1) as u32; - let h = map.virt_h.max(1) as u32; - let mut pm = tiny_skia::Pixmap::new(w.max(1), h.max(1)) - .unwrap_or_else(|| tiny_skia::Pixmap::new(1, 1).unwrap()); - for (_k, rs) in &map.cursors { - cursor_overlay::paint_cursor( - &mut pm, - &rs.core, - map.virt_x as f64, - map.virt_y as f64, - None, // focus-rect is macOS-only - ); - } - (Some(pm), arrived, pinned) + (Some(pm), arrived, pinned, needs_tick) + } } else { - (None, Vec::new(), None) + (None, Vec::new(), None, false) } }; if let Some(pm) = pixmap { update_layered_window(hwnd, &pm); + + // Z-order maintenance every 80ms — delegate to the cross-platform + // ZOrderEnforcer so the contract for "z+1 of the application under + // test" is documented once in `cursor_overlay::z_order`. Only run + // while we actually rendered: a quiescent overlay leaves its z-slot + // untouched until the next command wakes the loop. + let last = LAST_ZTICK.load(std::sync::atomic::Ordering::Relaxed); + if now_ms.wrapping_sub(last) >= 80 { + LAST_ZTICK.store(now_ms, std::sync::atomic::Ordering::Relaxed); + if let Some(enforcer) = Z_ORDER.get() { + enforcer.reassert(pinned_wid); + } + } } // Fire arrival oneshots for cursors whose path just ended — unblocks @@ -692,14 +821,19 @@ unsafe extern "system" fn wnd_proc( arrival_fire(k); } - // Z-order maintenance every 80ms — delegate to the cross-platform - // ZOrderEnforcer so the contract for "z+1 of the application under - // test" is documented once in `cursor_overlay::z_order`. - let last = LAST_ZTICK.load(std::sync::atomic::Ordering::Relaxed); - if now_ms.wrapping_sub(last) >= 80 { - LAST_ZTICK.store(now_ms, std::sync::atomic::Ordering::Relaxed); - if let Some(enforcer) = Z_ORDER.get() { - enforcer.reassert(pinned_wid); + // ── Re-arm the render timer at the cadence the current state needs ─ + // ACTIVE (~125 Hz) while animating/fading; IDLE (slow heartbeat) once + // quiescent so a static cursor stops burning CPU (issue #1808). We + // only call SetTimer on an actual cadence flip — re-arming with the + // same period every tick would itself be needless work. + let desired_ms = if needs_tick { + TIMER_MS_ACTIVE + } else { + TIMER_MS_IDLE + }; + if TIMER_PERIOD_MS.swap(desired_ms, std::sync::atomic::Ordering::Relaxed) != desired_ms { + unsafe { + SetTimer(hwnd, TIMER_ID, desired_ms, None); } } @@ -1072,6 +1206,69 @@ mod tests { assert!(!map.cursors.contains_key("sessA"), "ended session must not be resurrected"); } + #[test] + fn sentinel_cursor_is_quiescent_no_frame_tick() { + // A brand-new `mcp`/`serve` with no agent activity holds only the + // "default" cursor at the off-screen sentinel (-200, -200). It must NOT + // request frame ticks, so the render timer can drop to the slow idle + // cadence instead of compositing a full-screen pixmap at ~125 Hz + // (issue #1808 idle-CPU burn). + let map = empty_map(); + assert!( + !render_map_needs_frame_tick(&map), + "an untouched sentinel-only overlay must be quiescent" + ); + } + + #[test] + fn animating_cursor_requests_frame_ticks() { + // After a MoveTo, the cursor has an in-flight path → the loop must keep + // ticking at frame cadence so the glide actually animates. + let mut map = empty_map(); + apply_msg(&mut map, move_msg("sessA", 60.0, 60.0)); + assert!( + render_map_needs_frame_tick(&map), + "a cursor with an in-flight glide must request frame ticks" + ); + assert!( + map.cursors["sessA"].needs_frame_tick(), + "the animating cursor itself must report needs_frame_tick" + ); + } + + #[test] + fn click_pulse_requests_frame_ticks_then_goes_quiescent() { + let mut map = empty_map(); + // ClickPulse seeds click_t = Some(0.0) → active. + apply_msg( + &mut map, + OverlayMsg::Cmd(KeyedOverlayCommand { + key: "sessA".to_owned(), + cmd: OverlayCommand::ClickPulse { x: 10.0, y: 10.0 }, + }), + ); + assert!( + render_map_needs_frame_tick(&map), + "click pulse must keep ticking" + ); + + // Disable idle-hide so the only activity source is the click pulse, then + // advance time past the pulse: the cursor must fall quiescent so the + // loop can park. + for rs in map.cursors.values_mut() { + rs.core.motion.idle_hide_ms = 0.0; + } + for _ in 0..120 { + for rs in map.cursors.values_mut() { + rs.tick(0.016); + } + } + assert!( + !render_map_needs_frame_tick(&map), + "after the click pulse finishes the overlay must go quiescent" + ); + } + #[test] fn remove_clears_last_active_for_that_key() { let mut map = empty_map(); From da80a8d2ed7bb10994c19aad1a513d2e7fa8b6b7 Mon Sep 17 00:00:00 2001 From: Francesco Bonacci Date: Wed, 17 Jun 2026 20:12:12 -0700 Subject: [PATCH 2/2] chore(cua-driver-rs): keep Cargo.lock matching main (avoid nix cargoHash churn) The overlay fix needs no new dependencies; an incidental cargo build had re-synced the workspace member versions (0.5.3 -> 0.5.6) in Cargo.lock, which fetchCargoVendor hashes, breaking the Nix cargoHash and turning every Linux nix job red. Restore Cargo.lock to main's committed state so the hash stays valid. (The Cargo.toml/Cargo.lock version drift on main is a separate pre-existing issue, not this PR's concern.) Co-Authored-By: Claude Opus 4.8 --- libs/cua-driver/rust/Cargo.lock | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/libs/cua-driver/rust/Cargo.lock b/libs/cua-driver/rust/Cargo.lock index e9aed8b7cb..5133676757 100644 --- a/libs/cua-driver/rust/Cargo.lock +++ b/libs/cua-driver/rust/Cargo.lock @@ -486,7 +486,7 @@ dependencies = [ [[package]] name = "cua-driver" -version = "0.5.6" +version = "0.5.3" dependencies = [ "anyhow", "async-trait", @@ -517,7 +517,7 @@ dependencies = [ [[package]] name = "cua-driver-core" -version = "0.5.6" +version = "0.5.3" dependencies = [ "anyhow", "async-trait", @@ -532,7 +532,7 @@ dependencies = [ [[package]] name = "cua-driver-uia" -version = "0.5.6" +version = "0.5.3" dependencies = [ "anyhow", "cua-driver-core", @@ -547,7 +547,7 @@ dependencies = [ [[package]] name = "cursor-overlay" -version = "0.5.6" +version = "0.5.3" dependencies = [ "anyhow", "image", @@ -787,7 +787,7 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "focus-monitor-win" -version = "0.5.6" +version = "0.5.3" dependencies = [ "windows 0.58.0", ] @@ -1591,7 +1591,7 @@ checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "pip-preview" -version = "0.5.6" +version = "0.5.3" dependencies = [ "anyhow", "serde_json", @@ -1617,7 +1617,7 @@ checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" [[package]] name = "platform-linux" -version = "0.5.6" +version = "0.5.3" dependencies = [ "anyhow", "async-trait", @@ -1644,7 +1644,7 @@ dependencies = [ [[package]] name = "platform-macos" -version = "0.5.6" +version = "0.5.3" dependencies = [ "anyhow", "async-trait", @@ -1677,7 +1677,7 @@ dependencies = [ [[package]] name = "platform-windows" -version = "0.5.6" +version = "0.5.3" dependencies = [ "anyhow", "async-trait",