reticle_run { tool: "reticle_lease", args: { action: "acquire" } } hands each one an isolated context (its own cookies, storage and DOM) from a single shared headless Chromium. One daemon for each project, and identity is the app (a stable projectId stamped by the build plugin), not the port, so an app that boots on :3001 today is still the same app.
Reticle is built for the messy real world: several apps running at once, ports that shift between runs, and many agents driving different flows of the same app in parallel, without each one spinning up its own Chromium. This page explains how that works and how to use it.
The mental model
- One daemon for each project.
reticle mcpfinds this project’s daemon through~/.reticle, or starts one. When another project’s daemon holds the port, it starts one on a free port. A crashed daemon’s stale pidfile is reclaimed automatically, so you never chase “port already in use” or a zombie server. - Identity is the app, not the port. The build plugin stamps a stable
projectIdthat travels in every connection. If your Next app usually runs on:3000but boots on:3001today, Reticle still knows which app it is, and an agent scoped to project A will never accidentally drive project B’s tab. Origin is only a fallback hint. - One browser, many contexts. When agents need their own headless tabs, the daemon’s browser pool launches a single Chromium and hands out isolated contexts (one per flow). They are cheap, and capped so a big fan-out can’t exhaust the machine. An acquire over the cap is refused at once and names the cap.
- Attach-only daemon. The daemon never starts your dev server. It connects to an app that is running (or opens a headless tab pointed at it). When nothing is running, the agent starts your project’s own dev script in the background and tells you. A build process spawned by a background daemon would be invisible, unconsented, and orphaned when the daemon exits.
Manual testing: ~5 minutes
- Add the plugin (Vite/Next) or one
reticle.connect()call. (See getting-started.) - Start your app as you normally do.
- Open it in a browser; the in-page panel shows Reticle is connected.
- Click around. To report something that looks wrong, open Notes in the panel, select Add notes, and click the element. The agent drains those with
reticle_session { action: "review" }.
Agent testing: ~2 minutes
With the app running and instrumented, an agent drives a flow end to end:reticle_run { tool: "reticle_lease", args: { action: "acquire" } } opens a fresh isolated headless context against your already-running app, stamps the lease identity into the URL so the app’s own SDK registers under a sessionId you can target, and waits until that tab has connected (ready: true) before returning, so the sessionId is usable immediately. Release when the flow finishes.
Lease the exact host that the dev server binds. http://localhost:3000 and http://127.0.0.1:3000 are two origins, and they are not the same address. A dev server that binds only ::1 (Vite can do this for localhost) refuses a lease on 127.0.0.1, and a server that binds only 127.0.0.1 does not answer on ::1. The two origins also have their own cookies and storage, and a lease is reused for each origin. Copy the url that the dev server prints, host and port included.
Permissions
A lease starts with every permission undecided. Passpermissions to grant some on the app’s origin before the first navigation, so the first render already sees them:
geolocation, clipboard-read, clipboard-write and notifications; one the browser does not know is refused with its reason.
The headless shell has no notification support, so a notifications grant never reaches
Notification.permission there. When you ask for notifications, the lease reads the value back from the page and says so in hint whenever the page cannot see the grant.
10 agents, 10 flows, one dashboard
This is the design target, and it needs no special setup:- Each agent calls
reticle_run { tool: "reticle_lease", args: { action: "acquire" } }for the same dashboard URL → its own isolated context (own cookies/storage) in the one shared Chromium. - The pool caps simultaneous contexts (
RETICLE_MAX_CONTEXTS, default scales with CPU under a ceiling). An acquire, or areticle drive, that finds every slot taken is refused at once withall N lease slots are in use, naming the release and the override, so an agent knows to release a lease or wait rather than retry into the same full pool. The parallel flow runner queues instead, since it holds its own slots and frees them as each flow ends. - Flows can’t bleed into each other: contexts are isolated and every session is scoped by
projectId. - A single crashed page is reclaimed on its own, and if an agent crashes or hangs its lease stops being touched and the lease reaper reclaims the context after a TTL, freeing the slot. One dead agent never starves the others.
reticle_session { action: "list" } lists everything with projectId (group by app) and leased (pool context vs a human tab), so an orchestrator can see the whole fleet at a glance.
Knobs
Why not just open many browsers?
Ten Chromiums is hundreds of MB each and will thrash a laptop. Ten contexts in one browser is a few MB apiece and fully isolated: same correctness, a fraction of the cost. That’s the whole point of the pool. Measured (bench/harness/multi-agent-throughput.mjs): 16 verification flows that take 35.4s one-at-a-time finish in 5.2s across 8 leased contexts on a single Chromium (6.78× faster, ~30s saved per batch), with all 8 contexts live at peak. The speed-up scales with agent count up to the cap; the win over launching a browser per agent grows with how much per-agent browser startup you avoid.