How Ego-Browser Manages Task Space Ownership and Control Handoff Between Agents and Users

Ego-browser isolates every AI-agent interaction in a dedicated task space with strict ownership flags—"agent", "user", or "agentDelegatedToUser"—and provides atomic helper functions in src/helpers.ts to guarantee that only one party can issue browser commands at any moment, preventing race conditions during handoff.

Ego-browser is the Chromium-based automation engine in the citrolabs/ego-lite repository that solves the fundamental multi-actor problem: how can an AI agent and a human user share a browser without stepping on each other’s inputs? The answer lies in a rigorous task space ownership and control handoff model that partitions every session into isolated browsing contexts governed by explicit state flags.

Understanding Task Space Ownership States

Every task space in ego-browser carries an ownership property that acts as a mutex for browser control. The three possible states enforce a single-controller policy at all times.

Agent Ownership

When ownership === "agent", the AI has exclusive rights to create, select, close, and run Chrome DevTools Protocol (CDP) commands within the browsing context. Any user-initiated action is rejected with a "user is controlling" error, ensuring the automation script cannot be interrupted by accidental input.

User Ownership

When ownership === "user", the human operator retains full control. The agent cannot perform browser actions until it explicitly takes ownership via claimTaskSpace. The page remains visible to the user, but the agent overlay is hidden to prevent visual clutter.

Delegated Ownership

The transitional state ownership === "agentDelegatedToUser" allows the agent to temporarily hand the space back to the user (for example, to solve a CAPTCHA or complete 2FA) while reserving the right to reclaim control later. During this phase, the user operates under the same rules as standard user ownership.

The Control Handoff API

The public contract for managing these transitions lives in package/ego-browser/src/helpers.ts. These helpers wrap the underlying ego.* RPC calls with ownership checks and automatic space selection.

useOrCreateTaskSpace

The entry point for any automation workflow is useOrCreateTaskSpace(nameOrId). This helper attempts to re-attach to an existing space or spin up a new Chromium context that inherits the logged-in user session. According to the source at lines 193–211, the logic branches on the current ownership:

  • If the space is agent-owned, it is selected and returned for immediate use.
  • If the space is user-owned, it is only selected; the agent must call claimTaskSpace before issuing clicks or navigation.
const task = await useOrCreateTaskSpace('search-github');
// If task was user-owned, we cannot click yet...

claimTaskSpace

To transition from user control to agent control, the runtime calls claimTaskSpace(nameOrId). As implemented at lines 24–42, this function invokes ego.claimTaskSpace(id, space.name) to flip the ownership flag, then immediately calls selectTaskSpace to focus the agent overlay. This is the mandatory gateway for an agent to take over a space previously owned by the user.

const existing = await findTaskSpace('user-search');
if (existing.ownership === 'user') {
  await claimTaskSpace(existing.id); // Now the agent can act
}
await click('a[href="/search"]');

handOffTaskSpace

When the agent encounters a gate that requires human judgment—such as a login form with 2FA—it calls handOffTaskSpace([nameOrId]). The implementation at lines 26–40 first checks match.ownership === "user"; if true, it returns { done: false, skipped: "user-owned" } as a no-op. Otherwise, it executes ego.handOffTaskSpace(), which flips the ownership to "user" and hides the agent overlay.

await handOffTaskSpace(task.id);
console.log('Please complete the login, then reply "done".');

takeOverTaskSpace

After the user finishes the manual step, the agent resumes control via takeOverTaskSpace([nameOrId]). Located at lines 47–53, this helper optionally selects the named space, then calls ego.takeOverTaskSpace() to restore agent ownership and reveal the automation overlay.

await takeOverTaskSpace(task.id);
await waitForAgentControl(task.id); // Optional safety check

completeTaskSpace

To tear down a session, use completeTaskSpace(nameOrId, { keep }). The logic at lines 63–78 handles edge cases: if keep: true and the space is user-owned, the function skips closure to prevent yanking control away from the user. Otherwise, it selects or claims the space, then calls ego.completeTaskSpace() or ego.closeTaskSpace() to destroy the browsing context.

// Close immediately
await completeTaskSpace(task.id, { keep: false });

// Or leave open for the user
await completeTaskSpace(task.id, { keep: true });

waitForAgentControl

For synchronization after a handoff, waitForAgentControl(nameOrId, options) polls until the agent regains control. Implemented around lines 64–79, it uses a snapshot probe that throws only when the user is controlling the space, allowing the agent to safely wait without busy-waiting on the main thread.

Ownership Enforcement in Practice

The lifecycle of a typical task space follows a strict handshake protocol that prevents conflicting commands:

  1. Create or ReuseuseOrCreateTaskSpace establishes the context. If the space exists and is user-owned, the agent must claim it before acting.
  2. Agent Execution – While ownership is "agent", the script runs CDP commands freely.
  3. User InterventionhandOffTaskSpace transfers control for manual steps.
  4. Agent ResumptiontakeOverTaskSpace followed by waitForAgentControl ensures the user has finished before automation resumes.
  5. TeardowncompleteTaskSpace cleans up, respecting the keep flag to avoid destroying user-owned sessions.

This task space ownership and control handoff policy guarantees that at any moment exactly one party can issue browser commands, eliminating race conditions and "user-in-control" runtime errors.

Complete Workflow Example

The following runnable example demonstrates the full handoff cycle, from creation through user delegation to final closure:

// 1️⃣ Start a new agent-owned task space
const task = await useOrCreateTaskSpace('fetch-latest-issues');

// 2️⃣ Perform normal agent actions
await openOrReuseTab('https://github.com/citrolabs/ego-lite/issues');
await click('button[data-test-id="new-issue"]');

// 3️⃣ Hand off for manual login (e.g., 2FA)
await handOffTaskSpace(task.id);
console.log('Please complete the login, then reply "done".');

// 4️⃣ After user confirms, regain control
await takeOverTaskSpace(task.id);
await waitForAgentControl(task.id);

// 5️⃣ Continue automation
await type('input[name="title"]', 'Automated issue title');
await click('button[type="submit"]');

// 6️⃣ Close the space when finished
await completeTaskSpace(task.id, { keep: false });

Key Source Files

The ownership model and handoff API are defined across the following locations in the citrolabs/ego-lite repository:

  • package/ego-browser/src/helpers.ts – Public helper implementations that enforce ownership rules, hand-off, claim, and completion logic.
  • skills/ego-browser/SKILL.md – Agent-facing documentation explaining task-space flow and ownership semantics.
  • package/ego-browser/src/taskspace-e2e.test.mjs – End-to-end test suite exercising task-space creation, hand-off, takeover, and completion.
  • package/ego-browser/src/index.ts – Runtime entry point exporting the helper functions (useOrCreateTaskSpace, claimTaskSpace, etc.).
  • package/ego-browser/src/env.ts – Workspace directory resolver where task-space state is persisted.

Summary

  • Three ownership states (agent, user, agentDelegatedToUser) ensure only one actor controls the browser context at any time.
  • Atomic handoff functions in src/helpers.ts (claimTaskSpace, handOffTaskSpace, takeOverTaskSpace) manage transitions without race conditions.
  • Automatic enforcement prevents the agent from issuing CDP commands while the user owns the space, and vice versa.
  • Lifecycle completion via completeTaskSpace respects the keep option to avoid destroying user-owned sessions unexpectedly.

Frequently Asked Questions

What happens if an agent tries to click while the user owns the task space?

The action is rejected. When ownership === "user", the agent overlay is hidden and any attempt to execute browser commands throws a "user is controlling" error. The agent must first call claimTaskSpace to transition ownership back to "agent".

Can multiple agents share the same task space simultaneously?

No. The ownership model is designed as a mutex. While multiple agents could theoretically connect to the same space ID, the first to claim it sets ownership === "agent", and subsequent claim attempts would fail or block depending on the implementation in src/helpers.ts.

How does waitForAgentControl know when the user has finished?

The helper polls the underlying ego.snapshot RPC (wrapped in a try/catch probe) until it receives a valid response indicating ownership === "agent" or "agentDelegatedToUser". If the user is still controlling the space, the snapshot throws, and the function retries according to the provided timeout options.

Is the task space ownership state persisted across browser restarts?

Yes. According to src/env.ts, task space state is stored in a resolved workspace directory on disk. The ownership flag and browsing context metadata survive process restarts, allowing an agent to reconnect to a space that was handed off to a user in a previous session.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →