# How the Optional Per-User Slot System Isolates Memory Slots per Operator in ai-memory

> Learn how the optional per user slot system in akitaonrails ai-memory isolates memory slots per operator. Discover how to manage individual operator memory access.

- Repository: [Fabio Akita/ai-memory](https://github.com/akitaonrails/ai-memory)
- Tags: internals
- Published: 2026-08-23

---

**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`](https://github.com/akitaonrails/ai-memory/blob/main/wiki.rs). According to the source at [`crates/ai-memory-wiki/src/wiki.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-wiki/src/wiki.rs) lines 2198-2199, the function uses a simple `starts_with` check:

```rust

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>.md` paths 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`](https://github.com/akitaonrails/ai-memory/blob/main/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`](https://github.com/akitaonrails/ai-memory/blob/main/_slots/current-focus.md) and [`_slots/alice/current-focus.md`](https://github.com/akitaonrails/ai-memory/blob/main/_slots/alice/current-focus.md), but never [`_slots/bob/current-focus.md`](https://github.com/akitaonrails/ai-memory/blob/main/_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`](https://github.com/akitaonrails/ai-memory/blob/main/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:

```toml

[slots]
per_user = true

```

The flag is read at startup in `Config::load`. The [`docs/ARCHITECTURE.md`](https://github.com/akitaonrails/ai-memory/blob/main/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:

```rust

// 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`](https://github.com/akitaonrails/ai-memory/blob/main/_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`](https://github.com/akitaonrails/ai-memory/blob/main/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`](https://github.com/akitaonrails/ai-memory/blob/main/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`](https://github.com/akitaonrails/ai-memory/blob/main/docs/users.md) | Documents the `[slots] per_user` flag and its effect on multi-user deployments. |
| Architecture overview | [`docs/ARCHITECTURE.md`](https://github.com/akitaonrails/ai-memory/blob/main/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`](https://github.com/akitaonrails/ai-memory/blob/main/wiki.rs)**: the `is_slot_path` function gates all slot-specific behavior — pinning, decay immunity, and write-through — on the `_slots/` prefix.
- **Visibility is explicit** the `SlotVisibility` enum gives callers a choice between shared-only, per-user, or combined slots, verified by dedicated integration tests.
- **The flag is optional**: `[slots] per_user` defaults 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`](https://github.com/akitaonrails/ai-memory/blob/main/_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.