Skip to content

feat(cua-driver-rs): daemon session identity — own + clean up session-scoped recording & config - #1776

Merged
f-trycua merged 4 commits into
mainfrom
feat/cua-driver-rs-daemon-session-identity
May 31, 2026
Merged

feat(cua-driver-rs): daemon session identity — own + clean up session-scoped recording & config#1776
f-trycua merged 4 commits into
mainfrom
feat/cua-driver-rs-daemon-session-identity

Conversation

@f-trycua

@f-trycua f-trycua commented May 31, 2026

Copy link
Copy Markdown
Collaborator

The multi-tenant problem

The cua-driver daemon (serve.rs) is one shared process: every cua-driver mcp proxy connects to it and shares its ToolState. Any "set once, affects later calls" daemon state is therefore multi-tenant and gets clobbered across concurrent MCP sessions:

  • RecordingToolRegistry.recording is a singleton. Session A's disconnect could stop session B's recording.
  • Configset_config does config.write() AND persists to ~/.cua-driver/config.json. A sets vision, B sets som → clobber, and the on-disk default flips under everyone.

The daemon drives one physical machine, so this is a state ownership + cleanup problem, not safe concurrent screen/input control (two sessions clicking at once still contend for one desktop — out of scope, kept honest).

The session_id model

A proxy-minted session identity (one per cua-driver mcp process) carried in the daemon request envelope, used to OWN and CLEAN UP session-scoped state.

  • EnvelopeDaemonRequest gains an optional session_id (skip_serializing_if = Option::is_none, so the absent case is byte-identical on the wire; serde defaults a missing field to None). A new session_end lifecycle method reuses the same envelope. The daemon injects _session_id into the tool args right before invoke (Unix + Windows mirrors); tools read it via the existing ArgsExt, exactly like cursor_idno Tool trait change, no schema whitelist (the daemon never validates args against input_schema).
  • Recording ownershipRecordingSession gains owner: Option<String>; start() stamps it from _session_id; stop_owner(requester) is None = unconditional (manual/CLI/idle-TTL), Some==owner = stop, Some!=owner = no-op. Supersedes the fix(cua-driver-rs)(macos): tear down recording on client disconnect + default record_video=false (#1764) #1775 generation token wholesale — a stable owner identity that doubles as the config key, instead of a monotonic counter.
  • Config-per-session (macOS)SessionConfigRegistry holds in-memory overrides keyed by session_id, layered over the global. A named MCP session's set_config writes only its override (no global write, no disk); the anonymous CLI / one-shot path keeps writing + persisting the global default. get_config / get_window_state / click resolve effective = call-arg > session > global.
  • Cleanup — the proxy mints the id once (dep-free pid+nanos, no uuid crate) and sends one best-effort session_end on stdin EOF; the daemon drops that session's recording (stop_owner) + config overrides (platform session-end hooks). _session_id and any _-prefixed key are stripped before a turn is recorded, so the UUID never lands in action.json.

In scope vs deferred

In: the envelope + session_end, daemon-side _session_id injection (both platforms), proxy minting/teardown, recording ownership (replacing the #1775 token), and config-per-session for macOS.

Deferred (design-noted TODOs):

  • Cursor session-scoping — pure-additive, rides this envelope; CursorRegistry needs a remove/overlay-disable that doesn't exist yet.
  • PiP — process-global by design (one experimental preview for the one machine); no session change ever.
  • Generic per-session idle reaper for SIGKILLed proxies (session_end only fires on graceful EOF). The daemon-global recording idle-TTL is an acceptable interim backstop; there is no merged config TTL yet, so a SIGKILLed session's config overrides linger until daemon restart — a small bounded in-memory leak.
  • Windows/Linux set_config parity — they have their own DriverConfig; this PR scopes the config split to macOS.
  • The clean invoke_with_session trait/context API — YAGNI; inject-via-args avoids touching every Tool impl across 3 platform crates.

Verification (live, isolated socket, two real cua-driver mcp proxies)

  • Config isolation — A set_config capture_mode=vision, B set_config capture_mode=som; A's get_configvision, B's → som, concurrently, no clobber. ~/.cua-driver/config.json capture_mode stayed ax (neither named-session write touched disk). ✅
  • Recording ownership + disconnect — A start_recording, B start_recording (clobbers the singleton); close A → recording still enabled, owner = B's id; close B → stops. A-only start + disconnect → stops. ✅
  • Manual / CLI stop unconditional — a recording owned by a session is torn down by an anonymous direct-socket recording stop (session_id=Nonestop_owner(None)). ✅
  • Backward-compat — one-shot cua-driver call get_config (no session) returns the global; call set_config capture_mode=vision writes the persisted global default (survives a daemon reboot via load_driver_config()). ✅
  • Args non-pollution — a recorded move_cursor turn's action.json carries only {x, y}; _session_id was injected, used, and stripped. ✅
  • cargo build -p cua-driver clean; cargo test -p cua-driver -p cua-driver-core introduces no new failures beyond the 5 pre-existing mcp_protocol_test ones (verified identical set on the base branch).

Notes

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features

    • Video recording is now off by default when starting a recording session.
    • Added session-scoped configuration overrides—settings changes are now isolated to individual proxy sessions instead of affecting global configuration.
    • Implemented automatic recording cleanup: recordings owned by a session are stopped when that session ends.
    • Added idle-timeout backstop to automatically stop recordings after inactivity.
  • Documentation

    • Updated process-model guide to explain daemon-proxy session scoping and state cleanup.
    • Expanded MCP tools reference with details on session isolation, ownership semantics, and platform requirements.

f-trycua and others added 4 commits May 30, 2026 16:46
…roxy-exit + idle-TTL) + default record_video=false (#1764)

The serve daemon's recording is a process-global singleton with no
connection ownership. When an MCP client started a record_video recording
and disconnected without calling stop_recording, the ScreenCaptureKit
SCStream kept capturing the full display at native res / 30fps / H.264
indefinitely — ~6 GB/h to disk until the daemon was killed.

Why the first attempt (per-connection teardown in serve.rs) was wrong:
the shipped daemon-proxy transport forwards EVERY tools/call via
send_request, which opens a brand-new short-lived UnixStream per call and
closes it the instant the call returns. So the daemon per-connection task
handles exactly ONE request then hits EOF. Putting teardown there fired
milliseconds after start_recording (before any turn) and BROKE recording
on the primary path. 'connection close' != 'session end'. That serve.rs
hunk is fully reverted here.

The correct seam is the MCP proxy process, whose lifetime == the MCP
session:

1. Proxy-exit hook (proxy.rs, primary fix). run_proxy tracks whether THIS
   session has an outstanding recording (flag flipped only on a successful
   start/stop_recording forward; plain bool is safe because the read loop
   is strictly sequential). When stdin hits EOF (the real client-disconnect
   seam) the proxy sends one best-effort stop_recording DaemonRequest to
   the daemon socket, then exits. The daemon services it like any other
   per-call connection.

2. Idle-TTL backstop (serve.rs, defense-in-depth). A daemon-global task
   auto-stops recording after RECORDING_IDLE_TTL_SECS (default 300s,
   overridable via CUA_DRIVER_RS_RECORDING_IDLE_TTL_SECS) of zero tool-call
   activity. Covers the case the proxy can't run its hook (SIGKILL/crash,
   or a non-proxy client that dies holding a connection). Keyed on call
   activity, not connection liveness, so an actively-used session is never
   reaped. stop() is idempotent, so racing the proxy-exit hook is benign.
   Added to both unix and Windows run_serve for parity.

3. record_video now defaults to false (recording_tools.rs, kept from the
   prior attempt). A full-display native-res 30fps recording defaulting on
   for every start_recording was a sharp footgun. The legacy CLI recording
   start path keeps video on (asymmetry intentional). Tool/schema text,
   recording.rs doc-comment, and the /docs mcp-tools reference updated.

Verified against a freshly built daemon on an isolated socket
(/tmp/wf1764b), all 4 checks pass: (a) recording survives multiple
separate per-call daemon connections (no premature teardown); (b) closing
the real cua-driver mcp proxy's stdin stops the recording within ~3s;
(c) idle-TTL auto-stops after the TTL with no activity; (d) start_recording
with no record_video arg produces no recording.mp4.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…eo comments (#1764 review nits)

Addresses the three minor/nit review findings on PR #1775 (comment-only, no
behavior change):
- proxy.rs: the proxy-exit teardown block is reached on a clean stdin EOF (the
  normal MCP-client disconnect); reword the comment to stop implying it also
  covers the I/O-error `?` path, and note that path is backstopped by the
  daemon-side recording idle-TTL.
- recording.rs: fix the stale `configure()` doc-comment that said it defaults
  record_video "to match the new surface" — the MCP tool now defaults video
  OFF; the legacy CLI path forces it on.
- serve.rs: note the idle backstop's 30s tick granularity (sub-30s TTL
  overrides still fire no sooner than ~30s).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…stop so it cannot stop another session's recording (#1764)

The daemon recorder is a global singleton. With the proxy-exit auto-stop
added in #1775, this sequence could stop a recording its owner no longer
held: client A start_recording (owns it), client B start_recording
(clobbers A, now owns the recorder), then A disconnects and its proxy-exit
hook sent an unconditional stop_recording -> B's recording stopped.

Add a monotonically-increasing `generation` token to RecordingSession,
bumped on every successful start(). start_recording surfaces it in
structuredContent; the proxy captures the generation it started with and
passes it back on the proxy-exit stop_recording. RecordingSession::stop()
gains an optional expected-generation guard (race-free inside the lock):
a stale token is a silent no-op, leaving the newer owner's recording
running. Manual stop_recording (no generation) and the idle-TTL backstop
stay unconditional.

Verified live against a real daemon+proxy on an isolated socket:
A(gen1)/B(gen2)/A-exit leaves B's gen2 recording enabled; B-exit stops it;
single-session exit stops; manual stop with no generation stops; default
record_video stays off; idle-TTL still reaps after inactivity.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…-scoped recording & config by a proxy-minted session_id

The cua-driver daemon (serve.rs) is one shared process: every `cua-driver mcp`
proxy connects to it and shares its ToolState. Any "set once, affects later
calls" daemon state is therefore multi-tenant and clobbered across concurrent
sessions — the trajectory recorder is a singleton, and set_config mutates a
process-global + disk-persisted DriverConfig. Two MCP sessions racing set_config
clobber each other AND flip the on-disk default under everyone; one session's
disconnect could stop another's recording.

This adds a proxy-minted session identity carried in the daemon request
envelope, used to OWN and CLEAN UP session-scoped state:

- Envelope: optional `session_id` on DaemonRequest (skip_serializing_if =
  Option::is_none, so absent == byte-identical legacy wire; serde defaults a
  missing field to None). New `session_end` lifecycle method reuses the same
  envelope. The daemon injects `_session_id` into tool args before invoke (both
  Unix + Windows branches); tools read it via the existing ArgsExt, exactly like
  cursor_id. No Tool trait change.
- Recording ownership: RecordingSession gains an `owner: Option<String>`;
  start() stamps it from `_session_id`; stop_owner(requester) — None =
  unconditional (manual/CLI/idle-TTL), Some==owner = stop, Some!=owner = no-op.
  Supersedes the #1775 generation token wholesale (a stable owner identity that
  doubles as the config key, not a monotonic counter).
- Config-per-session (macOS): SessionConfigRegistry holds in-memory overrides
  keyed by session_id, layered over the global. A named MCP session's set_config
  writes only its override (no global write, no disk); the anonymous CLI /
  one-shot path keeps writing + persisting the global default. get_config /
  get_window_state / click resolve effective = call-arg > session > global.
- Cleanup: proxy mints the id once (dep-free pid+nanos, no uuid crate) and sends
  one best-effort `session_end` on stdin EOF; the daemon drops that session's
  recording (stop_owner) + config overrides (platform session-end hooks). Old
  daemons return "Unknown method" and the proxy swallows it (graceful degrade).
- `_session_id` and any `_`-prefixed key are stripped before a turn is recorded,
  so the UUID never lands in action.json.

Backward-compatible: absent session_id == anonymous/global session (today's
behavior). One-shot `cua-driver call` is its own ephemeral anonymous session.
The daemon drives ONE physical machine, so this solves state ownership + cleanup,
not safe concurrent screen/input control.

Deferred to follow-ups (design-noted): cursor session-scoping, PiP (process-
global by design), a generic per-session idle reaper for SIGKILLed proxies,
Windows/Linux set_config parity, and the clean invoke_with_session trait API.

Verified live on an isolated socket with two real `cua-driver mcp` proxies:
config isolation (A=vision / B=som, persisted default unchanged), recording
ownership (A+B start, A disconnect → B survives; B disconnect → stops; A-only
disconnect → stops; manual stop unconditional), backward-compat one-shot call,
and `_session_id` stripped from recorded turns.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@vercel

vercel Bot commented May 31, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
docs Ready Ready Preview, Comment May 31, 2026 1:55am

Request Review

@coderabbitai

coderabbitai Bot commented May 31, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 634caf01-744c-4d37-99b2-934b1349e825

📥 Commits

Reviewing files that changed from the base of the PR and between f0b990d and c38f6e7.

⛔ Files ignored due to path filters (1)
  • libs/cua-driver/rust/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (14)
  • docs/content/docs/cua-driver/guide/getting-started/process-model.mdx
  • docs/content/docs/cua-driver/reference/mcp-tools.mdx
  • libs/cua-driver/rust/crates/cua-driver-core/src/lib.rs
  • libs/cua-driver/rust/crates/cua-driver-core/src/recording.rs
  • libs/cua-driver/rust/crates/cua-driver-core/src/recording_tools.rs
  • libs/cua-driver/rust/crates/cua-driver-core/src/session.rs
  • libs/cua-driver/rust/crates/cua-driver/src/cli.rs
  • libs/cua-driver/rust/crates/cua-driver/src/proxy.rs
  • libs/cua-driver/rust/crates/cua-driver/src/serve.rs
  • libs/cua-driver/rust/crates/platform-macos/src/tools/click.rs
  • libs/cua-driver/rust/crates/platform-macos/src/tools/get_config.rs
  • libs/cua-driver/rust/crates/platform-macos/src/tools/get_window_state.rs
  • libs/cua-driver/rust/crates/platform-macos/src/tools/mod.rs
  • libs/cua-driver/rust/crates/platform-macos/src/tools/set_config.rs

📝 Walkthrough

Walkthrough

This PR implements session-scoped recording ownership and per-session configuration isolation for daemon-proxy mode. It adds session lifecycle hooks, owns recordings with optional session identifiers, extends the daemon protocol with session_id forwarding, implements session_end lifecycle handlers, and introduces in-memory per-session config overrides layered over persisted global config.

Changes

Session-scoped daemon state and lifecycle

Layer / File(s) Summary
Session lifecycle hook infrastructure
libs/cua-driver/rust/crates/cua-driver-core/src/session.rs, libs/cua-driver/rust/crates/cua-driver-core/src/lib.rs
Process-global hook registry allows platforms to register cleanup callbacks fired when a session ends, providing the session_id to enable session-scoped teardown.
Recording ownership tracking and guarded teardown
libs/cua-driver/rust/crates/cua-driver-core/src/recording.rs
RecordingSession tracks optional owner session id, start() accepts owner parameter and stamps it on successful start, stop_owner() conditionally stops only when requester matches owner (or unconditionally if requester is None), and tool args are sanitized to remove internal _-prefixed keys before persisting.
Recording tools with ownership integration
libs/cua-driver/rust/crates/cua-driver-core/src/recording_tools.rs
start_recording defaults record_video to false, extracts _session_id from args as owner, and passes it to session.start; stop_recording calls unconditional stop_owner(None); both tools document session-scoped cleanup via daemon lifecycle signals.
Daemon protocol extensions for session identity
libs/cua-driver/rust/crates/cua-driver/src/serve.rs
DaemonRequest gains optional session_id field; daemon injects session_id into tool args under _session_id; idle-TTL backstop periodically checks inactivity and auto-stops stale recordings via stop_owner(None); last-activity tracking initialized on Unix and Windows daemon startup and updated on each call handler invocation.
Daemon session_end handler on Unix and Windows
libs/cua-driver/rust/crates/cua-driver/src/serve.rs
New session_end method handler stops recording owned by the session, fires session-end hooks, and returns OK; implemented on both Unix and Windows paths to support graceful cleanup when MCP session closes.
CLI marks requests with anonymous session identity
libs/cua-driver/rust/crates/cua-driver/src/cli.rs
CLI one-shot operations (proxied call, recording control, permission checks, config access) explicitly set session_id: None to distinguish them from MCP proxy sessions carrying minted session identity.
Proxy session identity minting and lifecycle
libs/cua-driver/rust/crates/cua-driver/src/proxy.rs
Proxy mints unique per-proxy session_id at startup (combining pid and nanosecond timestamp), includes it in all daemon interactions (tool list fetch, tool call forwarding), and on clean stdin EOF sends best-effort session_end request to trigger session-owned state cleanup; session identity plumbed through JSON-RPC dispatch and tool forwarding.
Per-session in-memory configuration overrides
libs/cua-driver/rust/crates/platform-macos/src/tools/mod.rs, libs/cua-driver/rust/crates/platform-macos/src/tools/set_config.rs
ConfigOverrides and SessionConfigRegistry hold per-session config deltas with effective() resolver that layers overrides over global config; integrated into ToolState and cleared on session end via hook; set_config branches on _session_id: session-scoped writes to registry (in-memory only), anonymous writes to global config (persisted to disk).
Config and capture tools consume session-effective configuration
libs/cua-driver/rust/crates/platform-macos/src/tools/get_config.rs, libs/cua-driver/rust/crates/platform-macos/src/tools/get_window_state.rs, libs/cua-driver/rust/crates/platform-macos/src/tools/click.rs
get_config, get_window_state, and click tools extract _session_id from args and use session_config.effective() to resolve capture_mode and max_image_dimension, applying per-session overrides when available without persisting to disk.
Documentation updates
docs/content/docs/cua-driver/guide/getting-started/process-model.mdx, docs/content/docs/cua-driver/reference/mcp-tools.mdx
Process-model guide explains session identity stamping and daemon state scoping, clarifies proxy lifecycle with session_end cleanup and idle-TTL backstop; MCP tools reference documents recording ownership, session-scoped config isolation, and unconditional manual stop behavior.

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • trycua/cua#1580: Enables routing non-macOS MCP traffic through daemon-proxy, which relies on this PR's session_id and session_end protocol additions for proper state cleanup.
  • trycua/cua#1718: Precursor refactor to the recording subsystem; this PR builds on it by adding session_id/owner-aware recording start/teardown and session_end lifecycle handling.

🐰 A session springs to life, with minted identity in flight,
State scoped and owned, no global clobbering blight,
Idle TTL stands guard, recording won't leak,
Per-session config whispers what each caller shall seek,
Daemon cleanup dances when the client bids goodbye! 🎉

✨ 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 feat/cua-driver-rs-daemon-session-identity

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint skipped: no ESLint configuration detected in root package.json. To enable, add eslint to devDependencies.


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 and usage tips.

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