# How to Manage Cross-Agent Handoffs in ai-memory: A Complete Implementation Guide

> Master cross-agent handoffs in ai-memory with our guide. Learn to implement a three-state machine and REST API for seamless project context transfer.

- Repository: [Fabio Akita/ai-memory](https://github.com/akitaonrails/ai-memory)
- Tags: how-to-guide
- Published: 2026-09-01

---

**Cross-agent handoffs in ai-memory use a dedicated SQLite-backed handoff schema with a three-state machine (Open → Accepted → Expired) to transfer project context between agents, enforced by ownership filters and exposed via REST API endpoints.**

Managing continuity when switching between AI coding agents requires a durable, queryable record of work-in-progress states. The `akitaonrails/ai-memory` repository implements **cross-agent handoffs** as first-class database records rather than inferred log entries, ensuring reliable project continuity across Claude-Code, Codex, and other agent types.

## Core Architecture of the Handoff Schema

The handoff implementation centers on four primary structures defined in [`crates/ai-memory-core/src/handoff.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-core/src/handoff.rs), backed by the `ai-memory-store` crate's single-writer SQLite actor.

### NewHandoff Input Payload

The `NewHandoff` struct serves as the creation payload when a session terminates. It carries:
- **Identifiers**: `workspace_id`, `project_id`, and optional `from_session_id`
- **Agent Context**: `from_agent` (the source agent type) and optional `to_agent` (target hint)
- **Working State**: `cwd` (current working directory), `summary`, `open_questions`, `next_steps`, and `files_touched`
- **Ownership**: Optional `owner_user` field (None indicates a shared handoff)

### Handoff Materialized View

The `Handoff` struct represents the persisted record, composed of four flattened sub-structures to maintain a clean MCP wire format:
- `HandoffScope`: Workspace and project identifiers
- `HandoffOrigin`: Source session and agent information
- `HandoffContent`: Summary, questions, files, and context
- `HandoffLifecycle`: State, timestamps, and acceptance metadata

### HandoffState State Machine

State transitions are governed by the `HandoffState` enum with three distinct variants:
- **Open**: Initial state upon creation, visible to eligible agents
- **Accepted**: Terminal state after successful claiming by a receiving agent
- **Expired**: Terminal state for stale handoffs after periodic decay sweeps

### HandoffAcceptance Structure

The `HandoffAcceptance` struct captures the claiming transaction details:
- Target `handoff_id`, `workspace_id`, and `project_id`
- `accepting_agent` (the agent type claiming the handoff)
- `accepting_session` and optional `accepting_user`
- `owner_filter` (enforces `OwnerFilter::OwnOrPublic` or similar constraints)
- `receiving_cwd` (used for automatic supersession detection)

## Handoff Lifecycle and State Transitions

The complete lifecycle of cross-agent handoffs follows five distinct phases enforced by the store layer.

### 1. Creation and Persistence

When a session ends, the agent CLI calls the creation routine, building a `NewHandoff` from session context. The `ai-memory-store` persists this via `store.writer.insert_handoff(new_handoff).await` into a dedicated `handoff` table (separate from the wiki tree), initializing the state as `Open`.

### 2. Discovery via API

Agents query available context using `GET /workspaces/{workspace}/projects/{project}/handoffs`. The API filters results based on the caller's `OwnerFilter` and excludes `Expired` records. The project overview's `pending_handoff_count` reflects these visibility rules.

### 3. Acceptance and State Transition

The claiming agent submits a `POST` request to `/workspaces/{workspace}/projects/{project}/handoffs/{handoff_id}/accept` with a `HandoffAcceptance` payload. The server validates ownership permissions via the admission layer before atomically updating the state from `Open` to `Accepted`.

### 4. Supersession Logic

If the `receiving_cwd` in the acceptance payload matches the handoff's original `cwd`, the system automatically supersedes the handoff, linking the previous context to the new session. Mismatched directories create fresh handoff records rather than continuing the chain.

### 5. Expiration and Audit

Background decay sweeps periodically transition stale `Open` handoffs to `Expired`. These records remain in the database for audit purposes but are excluded from active API responses.

## Ownership and Access Control

The admission layer in [`crates/ai-memory-web/src/routes/api.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-web/src/routes/api.rs) enforces strict visibility rules through the `OwnerFilter` mechanism, with logic verified in the test suite located in [`crates/ai-memory-web/tests/routes.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-web/tests/routes.rs).

### Owned Handoffs

When `owner_user` contains an identity key (e.g., `"alice"`), the handoff is private to that operator. The body content is hidden from non-owners when the server runs with authentication enabled. Root-level callers may bypass these restrictions in root-only mode.

### Shared Handoffs

Records with `owner_user: None` are visible to all operators. However, the full body remains hidden from unauthenticated callers or non-owners depending on the server's authentication configuration.

### Root-Only Visibility Mode

When configured with root tokens, the server treats all handoffs as requiring ownership validation. Non-root callers see only handoffs they own, and the `pending_handoff_count` in project overviews reflects this filtered view.

## REST API Endpoints

The web crate exposes three primary endpoints for handoff management in [`crates/ai-memory-web/src/routes/api.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-web/src/routes/api.rs):

- **`GET /workspaces/{workspace}/projects/{project}/handoffs`**: Lists handoffs with optional state filtering. Response bodies may be truncated based on ownership.
- **`GET /workspaces/{workspace}/projects/{project}/handoffs/{handoff_id}`**: Retrieves specific handoff details. The full content is visible only to owners or in unauthenticated mode.
- **`POST /workspaces/{workspace}/projects/{project}/handoffs/{handoff_id}/accept`**: Claims an open handoff, requiring a valid `HandoffAcceptance` JSON body and proper authentication headers.

## CLI Usage and Programmatic Integration

The `ai-memory` CLI provides the `handoffs` subcommand for interactive use, while the Rust API enables programmatic control within agent implementations.

### CLI Commands

Creating a handoff at session end:

```bash
ai-memory handoffs create

```

Listing available handoffs:

```bash
ai-memory handoffs list

```

Accepting a specific handoff:

```bash
ai-memory handoffs accept 3f5c2e7a-...

```

### Rust API Example

Programmatic control using types from `ai-memory-core`:

```rust
use ai_memory_core::{
    handoff::{NewHandoff, HandoffAcceptance, OwnerFilter},
    ids::{WorkspaceId, ProjectId, SessionId, AgentKind},
};
use std::path::PathBuf;

// 1. Create a handoff from the current session context
let new_handoff = NewHandoff {
    workspace_id: WorkspaceId::new("default"),
    project_id: ProjectId::new("scratch"),
    from_session_id: Some(SessionId::new("sess-123")),
    from_agent: AgentKind::ClaudeCode,
    to_agent: None,
    cwd: Some(PathBuf::from("/path/to/project")),
    summary: "Implemented the billing API".into(),
    open_questions: vec!["How to handle refunds?".into()],
    next_steps: vec!["Write integration tests".into()],
    files_touched: vec!["src/billing.rs".into()],
    owner_user: Some("alice".into()),
};

// 2. Persist via the store writer
store.writer.insert_handoff(new_handoff).await?;

// 3. Accept an existing handoff as a new agent
let acceptance = HandoffAcceptance {
    handoff_id: target_handoff_id,
    workspace_id: WorkspaceId::new("default"),
    project_id: ProjectId::new("scratch"),
    accepting_agent: AgentKind::Codex,
    accepting_session: Some(SessionId::new("sess-456")),
    accepting_user: Some("bob".into()),
    owner_filter: OwnerFilter::OwnOrPublic,
    receiving_cwd: Some("/path/to/project".into()),
};
store.writer.accept_handoff(acceptance).await?;

```

## Summary

- **Cross-agent handoffs** are first-class SQLite records, not inferred from logs, defined in [`crates/ai-memory-core/src/handoff.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-core/src/handoff.rs).
- The **state machine** (`Open` → `Accepted` → `Expired`) ensures predictable lifecycle management.
- **Ownership filters** (`OwnerFilter`) enforce that agents only access authorized handoffs, with special handling for root-only mode.
- The **REST API** provides three endpoints for listing, fetching, and accepting handoffs, with visibility enforced at the admission layer.
- **Supersession** automatically links contexts when the receiving working directory matches the original.
- The **CLI** (`ai-memory handoffs`) and Rust API (`insert_handoff`, `accept_handoff`) provide both interactive and programmatic interfaces.

## Frequently Asked Questions

### What is the difference between an owned and shared handoff?

An **owned handoff** has an `owner_user` field set to a specific identity key, restricting visibility to that operator (or root users). A **shared handoff** has `owner_user: None`, making it visible to all agents, though the full body may still be hidden depending on server authentication settings.

### How does ai-memory prevent agents from accessing unauthorized handoffs?

The system enforces visibility through `OwnerFilter` checks in the admission layer ([`crates/ai-memory-web/src/routes/api.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-web/src/routes/api.rs)). When authentication is enabled, the API masks handoff bodies for non-owners and excludes owned handoffs from listings unless the caller matches the `owner_user` or possesses root privileges.

### Can a handoff be transferred between different agent types?

Yes. The `from_agent` and `to_agent` fields (using the `AgentKind` enum) track the source and intended recipient, but any authorized agent can accept an open handoff regardless of the `to_agent` hint. The acceptance record captures the actual `accepting_agent` for audit purposes.

### What happens to handoffs that are never accepted?

Stale `Open` handoffs transition to the `Expired` state via periodic background decay sweeps. While expired handoffs remain in the database for audit, they are excluded from active API listings and cannot be accepted by agents.