How the Optional Per-User Slot System Isolates Memory Slots per Operator in ai-memory
TLDR: In akitaonrails/ai-memory, enabling [slots] per_user = true switches slot storage from a shared _slots/<name>.md namespace to an operator-scoped _slots/<operator-id>/<name>.md namespace, so each authenticated operator sees only their own personal slots while shared slots remain visible to everyone.
The ai-memory repository implements a markdown-based wiki where pages starting with the "_slots/" prefix act as transient, pinable "brief" pages used by agents for short-term context. By default, all slots live in a single shared namespace. When the optional per-user mode is enabled via the TOML configuration [slots] per_user = true, the slot subsystem activates per-operator isolation that prevents one operator's memory slots from leaking into another's brief context. This article breaks down exactly how that isolation mechanism works under the hood.
The Core Design: Shared vs. Personal Slots
The entire slot system hinges on path prefixes in the wiki's markdown hierarchy. When per_user is false (the default), all slots are stored at _slots/<slot-name>.md. When set to true, the system introduces a second namespace level that encodes the operator's identity.
| Slot type | Path pattern | Visibility |
|---|---|---|
| Shared slot | _slots/<slot-name>.md |
Visible to all operators — the historic behaviour. |
| Personal slot | _slots/<operator-id>/<slot-name>.md |
Visible only to the operator whose <operator-id> matches the authenticated user (or OIDC subject). |
This single design decision — inserting the operator ID as an extra path segment — is the mechanism that delivers per-operator isolation. Everything else is enforcement.
Path Detection: Gatekeeping with is_slot_path
All slot handling logic (pinning, decay-immunity, write-through) is gated by a single path check implemented in wiki.rs. According to the source at crates/ai-memory-wiki/src/wiki.rs lines 2198-2199, the function uses a simple starts_with check:
fn is_slot_path(path: &str) -> bool {
path.as_str().starts_with("_slots/")
}
Any wiki page whose path begins with "_slots/" is classified as a slot. This means the isolation logic doesn't need to know about operators at this level — it only needs to identify that a page is a slot. The operator-scoping happens at the store layer, one level up.
Auth-Aware Slot Resolution with SlotVisibility
When a request queries brief pages, the store calls session_brief_pages_with_slot_visibility. This function builds a SlotVisibility enum that tells the SQL/filter layer whether to include shared slots only, personal slots only, or both. Here's how the three modes shape a query:
SlotVisibility::All— includes every slot matching_slots/*, regardless of owner.SlotVisibility::Shared— includes only_slots/<name>.mdpaths with no second segment.SlotVisibility::PerUser { operator_id }— includes shared slots plus_slots/<operator_id>/<name>.md.
The test personal_slots_reach_only_their_owner in crates/ai-memory-store/tests/slot_visibility.rs lines 129-146 verifies this behaviour: when the per-user arm is used, an operator named "alice" receives both _slots/current-focus.md and _slots/alice/current-focus.md, but never _slots/bob/current-focus.md.
Ownership Stamping via ActorContext
Every write to a slot is stamped with the caller's qualified identity (ActorContext::identity_key). When per-user mode is active, slots written under _slots/<operator-id>/… are stored with that identity. Retrieval later checks that this requesting identity matches the namespace segment.
This two-way check — namespace segment at write time and identity match at read time — is what prevents one operator from reading another's personal slot through the generic _slots/* arm. The test pinned_arm_does_not_leak_other_operators_slots at slot_visibility.rs lines 204-216 explicitly verifies that a pinned personal slot belonging to one operator never surfaces in another operator's brief response, even when the query would otherwise include all slots.
The Configuration Switch: [slots] per_user
The isolation mechanism is optional, activated through the [slots] TOML section in the configuration file:
[slots]
per_user = true
The flag is read at startup in Config::load. The docs/ARCHITECTURE.md file documents this design at line 297, noting that:
per_user = false(default): all_slots/*paths are treated as shared accesses, preserving compatibility with existing single-operator or shared-wiki deployments.per_user = true: the namespace logic activates with the second path segment dedentifying the operator.
This is a deliberate choice to protect backwards compatibility — teams that don't need per-operator isolation simply leave it off.
Practical Code Examples
Here is a complete working flow that shows how to write and query personal slots in per-user mode:
// 1. Enable per-user slots in the config (TOML):
// [slots]
// per_user = true
// 2. Writing a personal slot (operator "alice"):
let alice = "alice"; // derived from auth context
let path = format!("_slots/{}/current-focus.md", alice);
wiki.write_page(&path, "Focus on project X").await?;
// 3. Writing a shared slot (visible to everyone):
wiki.write_page("_slots/current-focus.md", "Global focus").await?;
// 4. Querying brief pages with per-user visibility:
let visibility = SlotVisibility::PerUser { operator_id: alice.to_string() };
let brief = store.session_brief_pages_with_slot_visibility(ws, proj, 100, 100, visibility).await?;
assert!(brief.slots.contains(&"_slots/current-focus.md".to_string()));
assert!(brief.slots.contains(&format!("_slots/{}/current-focus.md", alice)));
When per_user is false, step 2 writes to _slots/alice/current-focus.md will still work, but every operator will also see that slot because the namespace filter is disabled.
Key Implementation Files
| Component | Path | Why It Matters |
|---|---|---|
| Slot path detection & pinning logic | crates/ai-memory-wiki/src/wiki.rs |
Implements is_slot_path and ensures slot pages are decay-immune. |
| Slot visibility tests | crates/ai-memory-store/tests/slot_visibility.rs |
Shows how personal slots scope to their owner and how shared slots stay global. |
| User-level configuration docs | docs/users.md |
Documents the [slots] per_user flag and its effect on multi-user deployments. |
| Architecture overview | docs/ARCHITECTURE.md |
Provides the high-level design rationale for the optional per-user slot switch. |
Summary
- Isolation is path-based. Personal slots live at
_slots/<operator-id>/<slot-name>.md, sharing a predictable namespace that the query layer can filter on. - Detection sits in
wiki.rs: theis_slot_pathfunction gates all slot-specific behavior — pinning, decay immunity, and write-through — on the_slots/prefix. - Visibility is explicit the
SlotVisibilityenum gives callers a choice between shared-only, per-user, or combined slots, verified by dedicated integration tests. - The flag is optional:
[slots] per_userdefaults to false for backwards compatibility; when enabled, operators gain private short-term context without any global slot becoming inaccessible.
Frequently Asked Questions
Is the per-user slot system enabled by default?
No. The per_user flag in the [slots] TOML section defaults to false. When false, all _slots/* paths are shared across every operator. Enabling takes a single-line config change, but it requires restarting the service since the flag is read once at Config::load startup.
What prevents an operator from directly querying another operator's personal slot path?
Retrieval enforces identity matching. Even if a user manually constructs a path like _slots/bob/secret.md, the session_brief_pages_with_slot_visibility query uses the SlotVisibility::PerUser enum filtered by the authenticated ActorContext::identity_key. Since the identity key won't match the "bob" namespace segment in per-user mode, the slot is excluded for non-owners.
Are shared slots still available when [slots] per_user = true is set?
Yes. Shared slots (paths without the second operator-ID segment) remain visible to all operators. The per-user mode only adds a protected namespace on top of the existing shared one; it does not restrict the global _slots/<name>.md paths. This lets teams keep a common "global focus" slot while giving individual operators private scratch space.
How do pinned slots interact with per-user isolation?
Pinning doesn't bypass the namespacing. The pinned_arm_does_not_leak_other_operators_slots test proves that even pinned personal slots are filtered out of other operators' brief responses. Pinning only protects a slot from decay—it doesn't change the owner's visibility rules.
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 →