# How Agent-User Control Handoff Works in Ego-Lite Task Spaces

> Discover how agent-user control handoff works in ego-lite task spaces. Learn about the ownership flag and the handOffTaskSpace function for seamless control transitions.

- Repository: [CitroLabs/ego-lite](https://github.com/citrolabs/ego-lite)
- Tags: how-to-guide
- Published: 2026-08-07

---

**Ego-Lite manages agent-user control transfers through an ownership flag on task spaces, where the `handOffTaskSpace` function in [`src/helpers.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/helpers.ts) skips handoff if the space is already user-owned or delegates control via the native `ego.handOffTaskSpace` API when the agent currently owns the space.**

Ego-Lite isolates browser interactions inside dedicated *task spaces* that track whether the **agent** or **user** currently controls the session. Understanding the control handoff flow is essential for building automation scripts that safely transition between automated actions and manual user intervention. This article examines the ownership model and helper functions defined in the `citrolabs/ego-lite` repository that coordinate these transitions.

## Understanding Task Space Ownership

Each task space in Ego-Lite maintains an **ownership** flag that dictates control permissions. The three possible states are:

- **`"agent"`** – The automation script has full control
- **`"agentDelegatedToUser"`** – The agent initiated handoff but retains metadata ownership
- **`"user"`** – The user has taken manual control

According to the ownership policy table in [`src/helpers.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/helpers.ts) (lines 18-31), helpers behave differently depending on the current ownership state. The `handOffTaskSpace` helper specifically checks for `"user"` ownership and returns early if the user already controls the space, preventing redundant handoff attempts.

## The Handoff Implementation in helpers.ts

The core handoff logic resides in `handOffTaskSpace` ([`src/helpers.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/helpers.ts), lines 26-40). This async function orchestrates the transfer through a strict validation sequence:

1. **Verify native API availability** – Confirms `globalThis.ego.handOffTaskSpace` exists
2. **Resolve target space** – Uses `findTaskSpace` to locate the space by name or ID
3. **Ownership validation** – Returns `{ done: false, skipped: "user-owned" }` if the space is user-controlled
4. **Space selection** – Calls `selectTaskSpace` to ensure the correct context is active
5. **Native handoff** – Invokes `ego.handOffTaskSpace()` to hide the agent overlay
6. **Completion signal** – Returns `{ done: true }` to confirm successful transfer

```typescript
// handOffTaskSpace implementation – see helpers.ts lines 26-40
export async function handOffTaskSpace(nameOrId?: string | number) {
  const ego = globalThis.ego;
  if (!ego || typeof ego.handOffTaskSpace !== "function") {
    throw new Error("handOffTaskSpace requires ego.handOffTaskSpace");
  }
  if (nameOrId !== undefined) {
    const match = await findTaskSpace(nameOrId);
    if (match.ownership === "user") {
      return { done: false, skipped: "user-owned" as const };
    }
    await selectTaskSpace(ego, match, "handOffTaskSpace");
  }
  assertNoEgoError(await ego.handOffTaskSpace(), "handOffTaskSpace");
  return { done: true };
}

```

The function leverages `assertNoEgoError` from [`src/ego-errors.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/ego-errors.ts) to handle runtime exceptions, ensuring that Ego-specific errors (such as `isEgoUserControlError`) are properly caught and reported.

## Task Space Management API

Ego-Lite exposes handoff functionality through a façade pattern defined in `createTaskSpacesFacade` ([`src/helpers.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/helpers.ts), lines 85-96). This abstraction provides a clean interface for scripts while hiding internal resolution logic:

```typescript
// taskSpaces façade – see helpers.ts lines 85-96
function createTaskSpacesFacade() {
  return {
    list: listTaskSpaces,
    switch: switchTaskSpace,
    new: newTaskSpace,
    useOrCreate: useOrCreateTaskSpace,
    claim: claimTaskSpace,
    complete: completeTaskSpace,
    handOff: handOffTaskSpace,
    takeOver: takeOverTaskSpace,
    waitForAgentControl,
  };
}

```

Scripts access handoff capabilities via `taskSpaces.handOff()`, which maps directly to the `handOffTaskSpace` implementation. The façade also exposes `claim` for transferring user-owned spaces to the agent, and `takeOver` for forceful control reclamation.

## Regaining Agent Control

After handing off to the user, the agent must detect when control returns. Ego-Lite provides two mechanisms in [`src/helpers.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/helpers.ts):

**`waitForAgentControl`** (lines 77-89) polls harmlessly via `probeAgentControl()` until a snapshot succeeds, indicating the agent has regained control. Unlike `takeOverTaskSpace`, this function does not invoke the native takeover API; it merely waits for the user to relinquish control or for the ownership state to change.

**`takeOverTaskSpace`** performs no ownership validation and directly calls `ego.takeOverTaskSpace` after optionally switching to the named space. This bypasses the safety checks present in `handOffTaskSpace` and should be used when the agent must forcefully resume automation.

## Practical Implementation Examples

### Hand Off the Current Task Space

To delegate control of the active space without specifying a name:

```typescript
// Example: hand off the current task space
await taskSpaces.handOff();               // no argument → current space

```

### Handle User-Owned Spaces Gracefully

When targeting a specific space, check the result to determine if handoff occurred:

```typescript
const result = await taskSpaces.handOff('my-space');
if (result.skipped === 'user-owned') {
  console.log('Space already under user control – nothing to do.');
} else {
  console.log('Agent has handed control to the user.');
}

```

### Wait for User Completion

Poll for up to 5 minutes to detect when manual interaction finishes:

```typescript
// Wait up to 5 minutes for the user to give control back
await taskSpaces.waitForAgentControl('my-space', { timeout: 300 });
console.log('Agent control restored – continue automation.');

```

### Claim a User-Owned Space

When the agent must resume work on a user-controlled space, claim ownership first:

```typescript
await taskSpaces.claim('my-space');   // transfers ownership to the agent
await taskSpaces.switch('my-space');  // now safe to run agent-only helpers

```

## Key Source Files

The control handoff flow spans several modules in the `citrolabs/ego-lite` package:

- **[`src/helpers.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/helpers.ts)** – Central hub for all public helpers, including task-space management and the handoff implementation
- **[`src/ego-errors.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/ego-errors.ts)** – Defines error-handling utilities (`assertNoEgoError`, `isEgoUserControlError`) used by the handoff flow
- **[`src/state.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/state.ts)** – Holds runtime state (e.g., `defaultTimeout`) and provides `agentWorkspace()` used by task-space helpers
- **[`src/index.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/index.ts)** – Exposes the public API (`handOffTaskSpace`, `claimTaskSpace`, etc.) to the CLI and external modules

## Summary

- Task spaces use an ownership flag to track whether the agent or user currently controls the browser session
- `handOffTaskSpace` validates ownership before invoking the native API, returning early with `skipped: "user-owned"` when the user already has control
- The native `ego.handOffTaskSpace` API hides the agent overlay and transfers full browser control to the user
- `waitForAgentControl` polls harmlessly to detect when the user relinquishes control without forcing a takeover
- `claimTaskSpace` transfers ownership from user to agent, while `takeOverTaskSpace` bypasses ownership checks for forceful control reclamation

## Frequently Asked Questions

### What happens if I call handOffTaskSpace on a user-owned task space?

The function returns immediately with `{ done: false, skipped: "user-owned" }` without invoking the native `ego.handOffTaskSpace` API. This prevents redundant handoff attempts and potential race conditions when the user already controls the session.

### How does the agent detect when the user has finished manual interaction?

Use `waitForAgentControl` to poll the task space until the ownership state changes back to agent control. This function probes via harmless snapshots rather than forcing a takeover, allowing the agent to resume automation only when the user explicitly relinquishes control or closes the manual session.

### What is the difference between handOffTaskSpace and takeOverTaskSpace?

`handOffTaskSpace` performs strict ownership validation and only transfers control from agent to user, skipping execution if the space is already user-owned. In contrast, `takeOverTaskSpace` performs no ownership checks and directly invokes the native `ego.takeOverTaskSpace` API, making it suitable for emergency recovery or forced automation resumption regardless of current state.

### Where is the taskSpaces API defined and exposed to external modules?

The façade is created in [`src/helpers.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/helpers.ts) (lines 85-96) within the `createTaskSpacesFacade` function, which maps `handOff` to `handOffTaskSpace` and other convenience methods. This API is then exposed through [`src/index.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/index.ts) for consumption by CLI tools and external automation scripts.