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

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, 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 enforces strict visibility rules through the OwnerFilter mechanism, with logic verified in the test suite located in 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:

  • 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:

ai-memory handoffs create

Listing available handoffs:

ai-memory handoffs list

Accepting a specific handoff:

ai-memory handoffs accept 3f5c2e7a-...

Rust API Example

Programmatic control using types from ai-memory-core:

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.
  • The state machine (OpenAcceptedExpired) 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). 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.

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 →