# How Task‑Space Hand‑Off and Take‑Over Work Between Agent and User in ego‑lite

> Understand ego-lite's control handoff mechanism for agent and user task switching. Learn how handOffTaskSpace and takeOverTaskSpace manage context ownership and overlay visibility.

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

---

**In ego‑lite, control over a browsing context is transferred between agent and user via three declarative helpers—`handOffTaskSpace`, `takeOverTaskSpace`, and `completeTaskSpace`—that manage overlay visibility, ownership state, and cleanup.**

A **task space** in [citrolabs/ego‑lite](https://github.com/citrolabs/ego-lite) represents an isolated browsing session that can be owned either by the autonomous **agent** (with overlay UI visible) or by the **human user** (overlay hidden, full manual control). The runtime exposes a clean API for switching this ownership without dropping into low‑level Chrome DevTools Protocol (CDP) calls.

## Understanding Task‑Space Ownership

Before diving into hand‑off mechanics, you need to understand how ego‑lite tracks who controls a space.

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 18‑20), ownership is classified as follows:

- `"agent"` or `"agentDelegatedToUser"` → **agent‑owned** (agent retains authority)
- `"user"` → **user‑owned** (user has full control)

```typescript
// helpers.ts#L18-L20
const isAgentOwned = (space: TaskSpace) =>
  space.ownership === "agent" || space.ownership === "agentDelegatedToUser";

```

This distinction determines whether hand‑off operations proceed or short‑circuit as no‑ops.

## handOffTaskSpace: Returning Control to the User

The **`handOffTaskSpace([nameOrId])` helper** transfers the current (or specified) task space to the user. When invoked:

1. If a `nameOrId` is provided, the helper first selects that space via `selectTaskSpaceIfProvided`.
2. It checks ownership—if already user‑owned, it resolves immediately with `{ done: false, skipped: "user-owned" }`.
3. Otherwise, it delegates to the low‑level runtime `ego.handOffTaskSpace()` to hide the overlay and grant user control.

```typescript
// helpers.ts#L26-L39
export async function handOffTaskSpace(nameOrId?: string): Promise<HandoffResult> {
  const space = await selectTaskSpaceIfProvided(nameOrId);
  if (!isAgentOwned(space)) {
    return { done: false, skipped: "user-owned" };
  }
  await globalThis.ego.handOffTaskSpace(space.id);
  return { done: true };
}

```

**Use case:** Pause automation so the user can complete a CAPTCHA, verify a transaction, or inspect intermediate results.

```javascript
// Hand current space to user; overlay disappears
const result = await taskSpaces.handOff();
console.log(result); // → { done: true }

```

## takeOverTaskSpace: Reclaiming Agent Control

The **`takeOverTaskSpace([nameOrId])` helper** reverses the transfer, restoring the agent overlay and command authority:

1. Optionally switches to the specified space.
2. Invokes `ego.takeOverTaskSpace()` regardless of current ownership—user‑owned spaces are automatically reclaimed.

```typescript
// helpers.ts#L47-L53
export async function takeOverTaskSpace(nameOrId?: string): Promise<void> {
  const space = await selectTaskSpaceIfProvided(nameOrId);
  await globalThis.ego.takeOverTaskSpace(space.id);
}

```

Unlike `handOffTaskSpace`, this helper has no conditional skip logic. It forcefully regains control, making it suitable for resuming automation after human intervention.

```javascript
// Resume agent work; overlay reappears
await taskSpaces.takeOver();

```

## Detecting Control Loss: probeAgentControl and waitForAgentControl

Agents occasionally need to **probe whether they still hold control** without attempting a take‑over. The internal `probeAgentControl` function (helpers.ts#L57‑71) provides this capability:

```typescript
async function probeAgentControl(): Promise<boolean> {
  try {
    await globalThis.ego.snapshot(); // succeeds only when overlay active
    return true;
  } catch (err) {
    if (err instanceof EgoUserControlError) return false;
    throw err;
  }
}

```

When the user has control, `ego.snapshot()` throws `EgoUserControlError` (defined in [`src/ego-errors.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/ego-errors.ts)), which the probe converts to `false`.

Building on this, **`waitForAgentControl` (helpers.ts#L77‑88)** polls until the agent naturally regains control—useful when the user signals completion through external means rather than explicit API calls.

```javascript
// Poll until user returns control (without calling takeOver)
await taskSpaces.waitForAgentControl({ timeout: 60000 });

```

## completeTaskSpace: Finishing Work and Cleanup

The **`completeTaskSpace(nameOrId, { keep })` helper** terminates agent interaction with a space. Its behavior branches on the `keep` option and current ownership:

| Scenario | Behavior |
|----------|----------|
| `keep: true` + user‑owned | No‑op; resolves `{ done: false, skipped: "user-owned" }` (user already has page) |
| `keep: true` + agent‑owned | Hands off to user, keeps page open |
| `keep: false` | Claims space if user‑owned, then closes via `ego.closeTaskSpace()` |

```typescript
// helpers.ts#L63-L70, L104-L115
export async function completeTaskSpace(
  nameOrId: string,
  options: { keep?: boolean } = {}
): Promise<CompletionResult> {
  const space = await getTaskSpace(nameOrId);
  
  if (options.keep) {
    if (!isAgentOwned(space)) {
      return { done: false, skipped: "user-owned" };
    }
    await handOffTaskSpace(nameOrId); // Transfer before completing
    return { done: true };
  }
  
  // keep: false — close unconditionally
  if (!isAgentOwned(space)) {
    await takeOverTaskSpace(nameOrId); // reclaim first
  }
  await globalThis.ego.closeTaskSpace(space.id);
  return { done: true };
}

```

```javascript
// Close space entirely (user cannot see result)
await taskSpaces.complete("checkout-flow", { keep: false });

// Keep page open for user review
await taskSpaces.complete("checkout-flow", { keep: true });

```

## API Surface and Public Exposure

These helpers are exposed through the CLI API defined in [`package/ego-browser/src/format.ts`](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/format.ts) (lines 735‑762):

```typescript
// format.ts#L735-L762
taskSpaces: {
  handOff: (nameOrId?: string) => Promise<HandoffResult>;
  takeOver: (nameOrId?: string) => Promise<void>;
  complete: (nameOrId: string, options?: { keep?: boolean }) => Promise<CompletionResult>;
  waitForAgentControl: (options?: { timeout?: number }) => Promise<void>;
}

```

The low‑level runtime bindings in `src/driver/*` implement the actual CDP calls (`ego.handOffTaskSpace`, `ego.takeOverTaskSpace`, `ego.closeTaskSpace`), which the helpers abstract away.

## Summary

- **Task‑space ownership** in ego‑lite tracks whether the agent overlay is active (`"agent"`/`"agentDelegatedToUser"`) or hidden (`"user"`).

- **`handOffTaskSpace`** returns control to the user, idempotently skipping if already user‑owned.

- **`takeOverTaskSpace`** unconditionally reclaims control for the agent.

- **`completeTaskSpace`** finalizes work with flexible cleanup—preserving pages for user review or closing entirely.

- **Control detection** via `probeAgentControl` and `waitForAgentControl` lets agents gracefully handle asynchronous human‑in‑the‑loop workflows.

## Frequently Asked Questions

### What happens if I call handOffTaskSpace on a space the user already controls?

The call resolves immediately with `{ done: false, skipped: "user-owned" }`. No state change occurs, and no error is thrown. This idempotent design prevents race conditions when multiple automation steps attempt to hand off control.

### Can the agent forcefully take over a space without the user's permission?

Yes—`takeOverTaskSpace` has no conditional checks and will reclaim any space it can access. However, this is typically used after explicit user signaling (external message, webhook, or `waitForAgentControl` detection) rather than as a surprise interruption.

### How does ego‑lite detect when the user has finished their manual task?

The runtime throws `EgoUserControlError` from `ego.snapshot()` when the agent overlay is inactive. The `probeAgentControl` internal helper catches this and returns `false`, enabling `waitForAgentControl` to poll until the user completes their work. Alternatively, workflows can use external coordination (webhooks, shared state, or explicit `takeOver` calls) to resume automation.

### Is there a way to close a space without first taking it back from the user?

No—`completeTaskSpace` with `keep: false` automatically claims user‑owned spaces before closing. This ensures clean teardown without leaving dangling resources. If you want the page to remain visible to the user, use `keep: true` instead.