Ego-Lite Control Handoff Mechanism in Task Spaces: How Agents Yield and Reclaim Browser Control

Ego-Lite implements a bidirectional control handoff system where agents can surrender browser task spaces to users via handOffTaskSpace(), reclaim them with takeOverTaskSpace(), and block until ready using waitForAgentControl().

The control handoff mechanism in ego-lite task spaces centers on explicit ownership transitions between agent and user. Each browser tab runs in an isolated task space that carries an ownership flag, and the runtime provides three core helpers that enforce these transitions without race conditions or native bridge errors.

How Task Space Ownership Works

Ego-Lite tracks ownership in a strict three-state model defined in package/ego-browser/src/helpers.ts (lines 18–33). A task space can be:

  • "agent" — the agent has full control and renders its overlay UI
  • "agentDelegatedToUser" — the agent created the space but temporarily handed it to the user
  • "user" — the user created or fully owns the space; the agent cannot command it directly

This distinction prevents accidental interference when multiple parties share a browser instance. When an agent needs to operate on a user-owned space, it must first call claimTaskSpace to request a transfer.

The Three Core Handoff Operations

handOffTaskSpace([nameOrId])

The primary method for yielding control to the user. According to the source code in package/ego-browser/src/helpers.ts (lines 326–340), this helper:

  1. Checks if the target space is user-owned
  2. If user-owned, returns immediately with { done: false, skipped: "user-owned" } — a no-op that avoids native bridge errors
  3. If agent-owned, switches to that space (when nameOrId is provided) and invokes ego.handOffTaskSpace
// Hand off the current task space to the user.
// If the space is already user-owned the call is a harmless no-op.
await taskSpaces.handOff();   // → { done: true }

// Hand off a specific space by name or numeric id.
await taskSpaces.handOff('my-shopping-space');   // → { done: true }

// Attempt to hand off a user-owned space – the helper skips the native call.
await taskSpaces.handOff('user-space');   // → { done: false, skipped: "user-owned" }

The skip-on-user-owned behavior ensures idempotency — agents can call handOffTaskSpace defensively without checking ownership first.

takeOverTaskSpace([nameOrId])

Restores the agent overlay and resumes automated interaction. Implemented in package/ego-browser/src/helpers.ts (lines 442–454), this helper works on any space regardless of current ownership because the native bridge performs the final enforcement check.

// Later the agent wants to resume work on the same space.
await taskSpaces.takeOver('my-shopping-space');   // restores the agent overlay

Unlike handOffTaskSpace, there is no early-exit optimization here — the call always reaches the native layer, which validates whether the takeover is permissible under the current security policy.

waitForAgentControl(nameOrId, options)

A polling utility for blocking until control returns to the agent. Located in package/ego-browser/src/helpers.ts (lines 784–809), this function repeatedly queries ego.snapshot, which throws EGO_TASK_SPACE_USER_IN_CONTROL while the user retains interaction rights.

// If the agent needs to wait until the user has finished interacting:
await taskSpaces.waitForAgentControl('my-shopping-space', {
  interval: 5,   // check every 5 seconds
  timeout: 300,  // fail after 5 minutes
});
// Proceed once the agent has regained control.

This is a read-only wait — it changes no ownership state, only probes it. The polling interval and timeout accept custom values; defaults are drawn from the singleton runtime state defined in package/ego-browser/src/state.ts.

Error Handling and Edge Cases

The helper layer shields agents from low-level failures in two key ways:

Scenario Helper Behavior
Double handoff on already-user-owned space Returns structured skip result, no native call
Takeover denied by native security policy Native bridge rejects with standard error; propagates to caller
Snapshot timeout during waitForAgentControl Throws after options.timeout seconds if control never returns

The EGO_TASK_SPACE_USER_IN_CONTROL error code is treated as a retry signal rather than a failure, allowing graceful degredation while waiting for human input.

Integration with Agent Skills

The public façade documented in SKILL.md (under skills/ego-browser/) mirrors these three signatures exactly. Agent authors never interact with the native ego.* bridge directly — all calls route through the typed helper layer in package/ego-browser/src/helpers.ts, which normalizes return shapes and adds ownership-aware short-circuiting.

Summary

  • Task spaces carry explicit ownership flags ("agent", "agentDelegatedToUser", "user") checked by every handoff operation
  • handOffTaskSpace yields control and skips native calls for already-user-owned spaces (lines 326–340)
  • takeOverTaskSpace reclaims control, delegating final permission checks to the native bridge (lines 442–454)
  • waitForAgentControl polls ego.snapshot until EGO_TASK_SPACE_USER_IN_CONTROL clears, with configurable timeouts (lines 784–809)
  • Source files helpers.ts, state.ts, and SKILL.md collectively implement this mechanism as found in the citrolabs/ego-lite repository

Frequently Asked Questions

What happens if handOffTaskSpace is called on a user-owned task space?

The helper detects the ownership mismatch and returns { done: false, skipped: "user-owned" } immediately without invoking the native bridge. This design prevents unnecessary errors and makes the API safe to call speculatively.

No. The takeOverTaskSpace helper submits requests to the native bridge, which enforces security policy. While the helper itself has no early-exit for user-owned spaces, the underlying ego.takeOverTaskSpace implementation decides whether to grant the request based on session state and trust configuration.

How does waitForAgentControl differ from a simple sleep loop?

It is more efficient and semantically precise. Rather than blind sleeping, waitForAgentControl queries ego.snapshot on each interval and specifically interprets EGO_TASK_SPACE_USER_IN_CONTROL as a continuation signal. This aligns agent state with actual browser ownership without wasting cycles or missing rapid handoff events.

Where are default timeout values configured?

Default timeouts and polling intervals live in package/ego-browser/src/state.ts as part of the singleton runtime state. The waitForAgentControl helper merges caller-supplied options over these defaults, allowing per-call customization without global mutations.

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 →