The control lease
How agent and human share one browser without racing each other.
A session broker holds the only connection to the browser. The agent and every viewer are clients of the broker, never of Chrome. Control is a lease:
{
sessionId: string,
owner: 'agent' | 'human' | 'none' | 'transferring',
epoch: number, // increments on every transfer
holderId: string | null, // the user when owner is 'human'
expiresAt: number // renewed by the viewer's heartbeat
}Every input command carries the epoch it was planned under. The broker rejects any command whose epoch is not current with a typed STALE_EPOCH error, and the caller must re-observe. This makes the agent-versus-human race impossible rather than unlikely.
Transfer sequence
- Owner becomes
transferring; agent input is refused from this point. - In-flight agent commands are awaited, bounded.
- Quiescence: no pending navigation, no in-flight main-frame requests.
- Hardware-level input ignore is switched on as a backstop.
- The page is snapshotted: latest frame, full accessibility tree, frame tree, cookies.
- Epoch increments; owner becomes
human; input ignore is switched off.
Hand-back reverses the sequence and always re-snapshots.
Where the lease lives
In the broker's store, keyed by session, never in the streaming connection. A dropped WebSocket does not lose your control; reconnecting resumes it. Lease changes reach the run's workflow as a signal, and while a run is parked the workflow re-reads the lease periodically so a lost signal cannot strand it.
Expiry
The viewer heartbeats the lease. If the heartbeat stops, the policy is per environment: destructive environments park the run for a human, others return control to the agent.