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_idandproject_iddefault_globalflag 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 actorMismatch— Caller supplied specific coordinates but no matching entry exists; prevents accidental fallback to another user's dataUnset— 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:
- Extract
ActorContextfrom the request (authentication layer) - Call
ActiveProject::get_for(&actor_key)to resolve workspace/project - Match on
ActiveProjectLookupto 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 PerActordefault provides complete separation using compositeActorKey { user, session_id }PerActorMapwithRwLock, TTL, and capacity bounds keeps memory safe and bounded- Authentication layer in
auth.rsextracts 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →