Skip to content

Multi-agent exclusive desktop leases + idle VNC reaper - #10

Merged
Aurora Capital (auroracapital) merged 3 commits into
mainfrom
feat/multi-agent-leases
Jul 30, 2026
Merged

Multi-agent exclusive desktop leases + idle VNC reaper#10
Aurora Capital (auroracapital) merged 3 commits into
mainfrom
feat/multi-agent-leases

Conversation

@auroracapital

@auroracapital Aurora Capital (auroracapital) commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Summary

Multi-agent safe desktops for concurrent agents on Linux (pool), with portable leases on macOS/Windows.

  • ensure_desktop(owner_id) reuses the agent lease, claims idle seats, or auto-spawns a new Xvnc/noVNC on the next free port when others are busy
  • Exclusive owner_id + lease_until under the existing file-locked pool
  • Background reaper + process exit: free lock and stop pool VNC when idle past TTL or the MCP host exits (CLI sets RELEASE_ON_EXIT=0 so one-shot ensure keeps seats)
  • Default geometry 2560x1440 for side-by-side work
  • Windows limited hybrid backend so imports/status work cross-OS
  • CI: ruff + unit tests on Ubuntu/macOS/Windows; Linux dual-owner e2e ensure smoke
  • CLI: ensure, heartbeat, reap, acquire --force-new

Test plan

  • Local: ruff check/format, pytest tests/, dual-owner ensure → :50 + :51, reuse owner, release kills VNC
  • CI green on PR (lint matrix + e2e-linux-pool)
  • macOS job imports server (no Xvnc required)
  • Windows job imports server (limited backend)

Note

Medium Risk
Large changes to pool lifecycle (spawn, reap, process kill, cross-process locks) affect concurrent agents and host resource usage; Windows/macOS paths are mostly advisory leases with different isolation than Linux.

Overview
Adds multi-agent exclusive desktop leasing on the existing file-locked Xvnc pool, bumps the plugin to 0.2.0, and wires CI (ruff, pytest, Linux dual-owner ensure smoke).

MCP server: New tools ensure_desktop, heartbeat_desktop, and reap_idle_desktops. Pool sessions now carry owner_id + lease_until; ensure_desktop reuses the caller’s live lease, claims idle/expired seats, or auto-spawns the next free display when others are busy (never steals a live lease). acquire_desktop defaults to ensure behavior; force_new keeps always-spawn. A background reaper and exit/SIGTERM handling free leases and kill pool VNC (Xvnc + websockify); host default display is not reaped. Default geometry is 2560x1440. WinBackend provides lease-only portability; pool lock uses msvcrt on Windows. CLI sets DESKTOP_ACT_RELEASE_ON_EXIT=0 so one-shot invocations don’t tear down desktops immediately.

CLI: Subcommands ensure, heartbeat, reap; run uses ensure instead of acquire; --owner-id / --geometry / --force-new.

Docs & packaging: README, skill, and CLI README updated for the multi-agent pattern. pyproject.toml added; python-xlib gated to Linux only.

Reviewed by Cursor Bugbot for commit 6d373b8. Bugbot is set up for automated code reviews on this repo. Configure here.

Agents get exclusive seats via ensure_desktop(owner_id): reuse own lease,
claim idle seats, or auto-spawn a new Xvnc/noVNC pair on a free port.
Leases expire and a background reaper frees the lock and stops pool VNC
on idle or process exit (long-lived MCP hosts). Default geometry is
2560x1440 for side-by-side work. Windows gets a limited hybrid backend so
the server imports cleanly; CI runs lint/unit on Linux/macOS/Windows plus
a Linux dual-owner e2e smoke.
@cursor

cursor Bot commented Jul 30, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_b35fb5e2-a704-46b0-a8b5-2b4ff2a55378)

@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@auroracapital, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 32 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 84ed44ae-1c9a-4ab8-b703-9efbaa180e8a

📥 Commits

Reviewing files that changed from the base of the PR and between 23569d8 and 6d373b8.

📒 Files selected for processing (11)
  • .claude-plugin/plugin.json
  • .github/workflows/ci.yml
  • .gitignore
  • README.md
  • cli/README.md
  • cli/desktop_act_cli.py
  • mcp-server/server.py
  • pyproject.toml
  • requirements.txt
  • skills/desktop-act/SKILL.md
  • tests/test_leases.py

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.

Comment thread mcp-server/server.py
Comment on lines 1396 to +1400
return _pid_alive(ws)
return False

async def acquire_desktop(self, geometry) -> dict:
async def ensure_desktop(self, geometry: str = DEFAULT_GEOMETRY, owner_id: str = "") -> dict:
# Single real desktop — lease is advisory; second agent is warned but can share.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Bug: In X11Backend.list_desktops, the result of a session reap is discarded and the session pool is re-read outside the lock, creating a race condition.
Severity: MEDIUM

Suggested Fix

Do not discard the mutated pool dictionary after _reap_pool_locked is called. Use this in-memory pool to build the session list, which avoids the race condition and the unnecessary file read.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.

Location: mcp-server/server.py#L1396-L1400

Potential issue: In `X11Backend.list_desktops`, an opportunistic reap of dead sessions
is performed inside a lock to ensure the returned list is 'honest'. However, the result
of this operation is discarded. The code then releases the lock and re-reads the session
pool from disk. This creates a time-of-check-to-time-of-use (TOCTOU) race condition
where concurrent processes can add new sessions between the lock release and the
re-read, defeating the purpose of the reap and making the returned list inconsistent
with the state immediately after the cleaning operation.

Did we get this right? 👍 / 👎 to inform future reviews.

Comment thread mcp-server/server.py
Comment on lines 1511 to 1519
os.kill(pid, signal.SIGKILL)
except OSError:
pass
_clear_lease(sess)
pool[session_id] = sess
_write_pool(pool)
self._bridge_pid = None
_LAST_SHOT.pop(sess["display"], None)
pool.pop(session_id, None)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Bug: MacBackend.release_desktop incorrectly deletes the session record due to a double-write and holds a file lock across an await, causing lock contention.
Severity: MEDIUM

Suggested Fix

Remove the second pool.pop(session_id, None) and _write_pool(pool) call to prevent the session from being deleted. To fix the lock contention, move the entire lock-protected synchronous code block into a separate function and run it in a thread pool executor using run_in_executor, similar to the pattern used in X11Backend.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.

Location: mcp-server/server.py#L1511-L1519

Potential issue: The `MacBackend.release_desktop` method contains two distinct issues.
First, it performs a double write: it clears a session's lease and writes the pool to
disk, but then immediately removes the session from the pool and writes to disk again.
This incorrectly deletes the session record, causing `list_desktops` to report zero
sessions. Second, it holds a file lock across an `await asyncio.sleep(0.3)` call. This
blocks any other concurrent operations that require the same lock for the duration of
the sleep, leading to performance degradation and potential timeouts under load.

Also affects:

  • mcp-server/server.py:1458

Did we get this right? 👍 / 👎 to inform future reviews.

fcntl is Unix-only; CI on windows-latest failed at import. Use msvcrt
byte locks on Windows and fcntl.flock elsewhere, put pool files under
the OS temp dir on Windows, and only install python-xlib on Linux.
WinBackend ensure no longer overwrites another owner's unexpired lease.
@cursor

cursor Bot commented Jul 30, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_2a29a359-9001-4a3e-9bc5-c79d1a1b51a6)

argparse -h failed on windows-latest under cp1252 because help text
used U+2192. Keep the run help string plain ASCII.
@cursor

cursor Bot commented Jul 30, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_152e5513-e0bd-48f1-8b2b-71c5fe812ec7)

@auroracapital
Aurora Capital (auroracapital) merged commit 23ce141 into main Jul 30, 2026
9 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.

1 participant