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 optionalfrom_session_id - Agent Context:
from_agent(the source agent type) and optionalto_agent(target hint) - Working State:
cwd(current working directory),summary,open_questions,next_steps, andfiles_touched - Ownership: Optional
owner_userfield (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 identifiersHandoffOrigin: Source session and agent informationHandoffContent: Summary, questions, files, and contextHandoffLifecycle: 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, andproject_id accepting_agent(the agent type claiming the handoff)accepting_sessionand optionalaccepting_userowner_filter(enforcesOwnerFilter::OwnOrPublicor 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 validHandoffAcceptanceJSON 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 (
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). 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →