How ai-memory Handles Multi-Session and Multi-User Access: Isolation Modes Explained

ai-memory uses a process-wide active-project pointer with three isolation modes—Single, PerSession, and PerActor—to safely handle concurrent sessions and multiple users without data corruption.

The ai-memory project (akitaonrails/ai-memory) provides a memory layer for AI agents that must operate reliably across parallel tool executions and distinct operator identities. When an MCP tool receives a request, the system needs to know which workspace and project context applies—without requiring every call to explicitly pass this state. The solution is an active-project pointer that resolves context dynamically based on who is calling and from which session.

The ActiveProject Pointer and Isolation Modes

At the core is ActiveProject, defined in crates/ai-memory-core/src/active_project.rs. This structure maintains a globally accessible pointer that hook routers set and tool handlers read. The pointer supports three isolation behaviors controlled by the ActiveProjectMode enum:

Mode Isolation Level Use Case
Single No isolation; single global slot Legacy/single-operator setups only
PerSession Isolated by session_id Same user, multiple parallel agent runs
PerActor (default) Isolated by composite ActorKey { user, session_id } Full multi-user, multi-session safety

Single Mode: Last-Write-Wins

In Single mode (lines 94-100), there is one shared slot for the entire process. The last hook event overwrites it unconditionally. This works for a single operator running one agent at a time, but concurrent hooks from different repositories or sessions corrupt each other's state.

PerSession Mode: Session-Scoped Isolation

PerSession keys entries by session_id alone (lines 30-33). Each parallel run of the same user gets its own workspace/project entry, preventing cross-repository interference even when the same bearer token is used across multiple concurrent agents.

PerActor Mode: Full User + Session Isolation

PerActor (the default) uses an ActorKey combining user and session_id (lines 82-101, 166-176). This provides complete separation between:

  • Different users (distinct bearer tokens)
  • Concurrent sessions from the same user
  • Legacy anonymous calls (fallback to single slot when no actor identity is present)

Storage, TTL, and Capacity Management

The pointer entries live in a PerActorMap—a HashMap<ActorKey, Entry> protected by an RwLock. Each entry stores:

  • workspace_id and project_id
  • default_global flag indicating whether this is a fallback default

To prevent unbounded memory growth, the implementation enforces:

  • Time-to-live: Entries expire after a configurable duration (default 1 hour)
  • Capacity cap: Maximum 4096 entries (default)
  • Lazy eviction: Expired entries are purged on every read and write operation (lines 44-58)

Actor Identification and Authentication

The ActorKey originates in the authentication layer. In crates/ai-memory-mcp/src/auth.rs, the system extracts a stable user identifier from bearer tokens. For interactive sessions, crates/ai-memory-mcp/src/human_auth.rs provides human-authentication specifics. The ActorContext struct in crates/ai-memory-mcp/src/actor.rs carries this identity through the MCP layer.

The authentication flow combines user and session identifiers:

use ai_memory_core::active_project::{
    ActiveProject, ActiveProjectMode, ActorKey,
};

// Create the pointer with default PerActor mode
let active_proj = ActiveProject::new();

// Construct actor identity from authenticated request
let actor = ActorKey {
    user: Some("alice".into()),
    session_id: Some("sess-42".into()),
};

Resolving Projects: The Lookup Outcome

Tool handlers call ActiveProject::get_for(actor_key) to resolve context. The method returns an ActiveProjectLookup with three possible outcomes (lines 82-94):

  • Resolved(ws, proj) — Concrete workspace and project found for this actor
  • Mismatch — Caller supplied specific coordinates but no matching entry exists; prevents accidental fallback to another user's data
  • Unset — No actor information available; system falls back to legacy default project

Setting and Updating the Active Project

When repository hooks fire (e.g., directory change, file operation), the hook router publishes the new active project:

use ai_memory_core::active_project::ActiveProject;

// After resolving workspace and project from cwd or config
let new_ws = /* WorkspaceId */;
let new_proj = /* ProjectId */;
active_proj.set_for(actor, new_ws, new_proj, true);

The set_for method handles actor-keyed storage, TTL refresh, and capacity enforcement atomically.

Integration in MCP Server Handlers

In crates/ai-memory-mcp/src/server.rs, every tool handler follows this pattern:

  1. Extract ActorContext from the request (authentication layer)
  2. Call ActiveProject::get_for(&actor_key) to resolve workspace/project
  3. Match on ActiveProjectLookup to determine execution context or reject the call

This design guarantees that a tool operating on user Alice's repository cannot accidentally access Bob's data, even when both users connect through the same running ai-memory server process.

Summary

  • Three isolation modes (Single, PerSession, PerActor) trade off simplicity against concurrent safety
  • PerActor default provides complete separation using composite ActorKey { user, session_id }
  • PerActorMap with RwLock, TTL, and capacity bounds keeps memory safe and bounded
  • Authentication layer in auth.rs extracts stable identities for the pointer system
  • Three-way lookup result (Resolved, Mismatch, Unset) enables explicit failure modes instead of silent cross-user data leaks

Frequently Asked Questions

What happens if two users with different bearer tokens connect simultaneously?

With the default PerActor mode, each user receives a distinct ActorKey. Their active-project entries are stored separately in the PerActorMap, so neither can read or overwrite the other's workspace/project context. The Mismatch result explicitly prevents fallback to another user's slot.

Can the same user run multiple agent sessions in parallel without conflict?

Yes. PerSession or PerActor mode isolates by session_id. Each parallel agent run receives a unique session identifier, resulting in separate ActorKey entries and independent active-project pointers. This prevents repository A's hooks from corrupting repository B's context.

How does ai-memory prevent memory exhaustion from stale sessions?

Entries expire after a configurable TTL (default 1 hour) and the map enforces a capacity cap (default 4096 entries). Expired or overflowed entries are lazily evicted during read and write operations, keeping the ActiveProject structure bounded regardless of connection churn.

What is the fallback behavior for unauthenticated or legacy requests?

When no user or session identity is present, get_for returns ActiveProjectLookup::Unset. The MCP server then falls back to a legacy default project, preserving backward compatibility with clients that predate the authentication layer.

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 →