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

> Understand the ego-lite control handoff mechanism for agents and users to seamlessly yield and reclaim browser task spaces. Learn how to manage control flow.

- Repository: [CitroLabs/ego-lite](https://github.com/citrolabs/ego-lite)
- Tags: internals
- Published: 2026-08-15

---

**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`](https://github.com/citrolabs/ego-lite/blob/main/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`](https://github.com/citrolabs/ego-lite/blob/main/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`

```typescript
// 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`](https://github.com/citrolabs/ego-lite/blob/main/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.

```typescript
// 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`](https://github.com/citrolabs/ego-lite/blob/main/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.

```typescript
// 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`](https://github.com/citrolabs/ego-lite/blob/main/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`](https://github.com/citrolabs/ego-lite/blob/main/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`](https://github.com/citrolabs/ego-lite/blob/main/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`](https://github.com/citrolabs/ego-lite/blob/main/helpers.ts), [`state.ts`](https://github.com/citrolabs/ego-lite/blob/main/state.ts), and [`SKILL.md`](https://github.com/citrolabs/ego-lite/blob/main/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.

### Can an agent force control away from a user without their consent?

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`](https://github.com/citrolabs/ego-lite/blob/main/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.