# How is memory scoped and isolated?

> Agent memory layers — a good answer covers: User / agent / session / tenant scoping; multi-tenancy; access control; privacy controls.

Canonical page: https://llms-technical-reviews.com/memory/q/scoping/

## Verdict

Cognee and Honcho enforce isolation in the memory layer itself. Most other projects filter on an id that the caller passes and trust the application to pass the right one.

**Access control built in.** [Cognee](/p/cognee/) stores read, write, delete and share rights as ACL rows per dataset. When the backends support it, each user and dataset gets its own graph and vector database, and authentication is forced on. [Honcho](/p/honcho/) uses JWT claims for admin, workspace, peer and session, and keeps a separate collection for each (observer, observed) pair. An empty session allowlist returns nothing, and only explicit conclusions are served under one. Auth is off until you set `AUTH_USE_AUTH`. [MemOS](/p/memos/) scopes each request to memory cubes (one per user by default). It has scoped, expiring API keys, but `AUTH_ENABLED` is off by default, and a database per user is opt-in. In [claude-mem](/p/claude-mem/)'s server mode, API keys are bound to a team (and optionally a project) with read and write scopes. Local mode is single-user, scoped by project.

**Partition by id.** [Hindsight](/p/hindsight/) filters every query by `bank_id` and supports tag modes (`any`, `all`, the strict variants and `exact`) inside a bank. A Postgres schema per tenant exists only when `HINDSIGHT_API_TENANT_EXTENSION` is set. [Graphiti](/p/graphiti/) uses `group_id`. On FalkorDB that becomes a separate graph, but on Neo4j it is only a property filter, and neither server has auth. [Mem0](/p/mem0/) requires `user_id`, `agent_id` or `run_id` on writes and searches, but `get`, `update` and `delete` take only a memory id. [Memori](/p/memori/) skips memory entirely without an `entity_id`. Only BYODB augmentation hashes the ids, and cloud mode sends them raw. [Supermemory](/p/supermemory/) scopes by container tag (default `sm_project_default`) and leaves enforcement to its API.

**Weak inside a project.** [MemMachine](/p/memmachine/) partitions by organisation and project. User and agent separation is a metadata filter built by the client that the server does not enforce, and the rolling summary is shared by the whole project.

Pick: Cognee or Honcho when several users share one deployment.
Pick: Hindsight for clean per-agent banks with tag filters.
Pick: Mem0 or Graphiti when your backend already controls which ids reach memory.

## Per-project answers

### thedotmack/claude-mem (answered)

Memory is scoped by a **team → project → session** hierarchy with multi-tenancy at the team level. **Team/Project isolation**: Every Postgres observation carries `team_id` and `project_id`, and every query enforces scoped WHERE clauses (`src/storage/postgres/observations.ts:199-200`). API keys are bound to a team (and optionally a project: `src/storage/postgres/schema.ts:122-131`). The auth middleware requires `memories:read` or `memories:write` scopes (`src/server/routes/v1/ServerV1Routes.ts:62-71`). The `ProviderObservationGenerator` validates scope bullMQ payloads against canonical Postgres rows at execution time (`src/server/generation/ProviderObservationGenerator.ts:202-227`) and refuses to run on mismatch. **Session scoping**: Observations link to `server_session_id`, and the `server_sessions` table records `platform_source` (claude-code, opencode, cursor, etc.), enabling platform-aware filtering (`src/storage/postgres/schema.ts:149-167`). Search queries support filtering by platform source (`src/storage/postgres/observations.ts:223-240`). **Multi-tenancy**: Teams are the tenant boundary with `team_members` controlling roles (owner, admin, member, viewer: `src/storage/sqlite/schema.ts:44-53`). Foreign keys cascade from team through projects, sessions, events, and observations — deleting a team cascades all its data (`src/storage/postgres/schema.ts:91-256`). **Privacy controls**: Event payloads pass through `stripTags` before extraction (`src/server/generation/providers/shared/prompt-builder.ts:149`), removing `<private>`, `<claude-mem-context>`, `<system-reminder>` tags. Subagent observations are dropped early via `shouldSkipAgentObservation` (`src/cli/handlers/observation.ts:59-67`), and excluded projects are skipped via `shouldTrackProject`. The `excludeSubagents` filter (`src/storage/postgres/observations.ts:208-221`) keeps subagent-derived rows out of context injection when configured.


Citations: [src/storage/postgres/schema.ts:84-132](https://github.com/thedotmack/claude-mem/blob/0a803bdedb09b00fc7fcfdc3a55a4e75c55c3952/src/storage/postgres/schema.ts#L84-L132) · [src/storage/postgres/observations.ts:176-258](https://github.com/thedotmack/claude-mem/blob/0a803bdedb09b00fc7fcfdc3a55a4e75c55c3952/src/storage/postgres/observations.ts#L176-L258) · [src/server/generation/ProviderObservationGenerator.ts:198-251](https://github.com/thedotmack/claude-mem/blob/0a803bdedb09b00fc7fcfdc3a55a4e75c55c3952/src/server/generation/ProviderObservationGenerator.ts#L198-L251) · [src/server/routes/v1/ServerV1Routes.ts:60-71](https://github.com/thedotmack/claude-mem/blob/0a803bdedb09b00fc7fcfdc3a55a4e75c55c3952/src/server/routes/v1/ServerV1Routes.ts#L60-L71) · [src/cli/handlers/observation.ts:56-68](https://github.com/thedotmack/claude-mem/blob/0a803bdedb09b00fc7fcfdc3a55a4e75c55c3952/src/cli/handlers/observation.ts#L56-L68) · [src/server/generation/providers/shared/prompt-builder.ts:145-173](https://github.com/thedotmack/claude-mem/blob/0a803bdedb09b00fc7fcfdc3a55a4e75c55c3952/src/server/generation/providers/shared/prompt-builder.ts#L145-L173)

### mem0ai/mem0 (answered)

Memory scoping is enforced through **three mandatory entity identifiers**: `user_id`, `agent_id`, and `run_id`. At least one is required for every operation (`_build_filters_and_metadata` in `mem0/memory/main.py:314-404`). All three may be combined for composite scoping (e.g., a specific user interacting with a specific agent in a specific run).

**Write-time scoping** — when `add()` is called, the provided entity IDs are validated (trimmed, whitespace-checked, non-empty enforced via `_validate_and_trim_entity_id` at line 175-209) and stored directly in the vector store payload. Caller-supplied `metadata` dicts are **stripped** of identity keys by `_strip_identity_keys` (`mem0/memory/main.py:143-162`) — you cannot set `user_id` inside `metadata` to override or change the scope. On the update path, re-sending the existing values is silently accepted, but changed values are dropped with a warning.

**Query-time scoping** — `search()`, `get_all()`, and `delete_all()` require filters containing at least one entity ID. Top-level `user_id`/`agent_id`/`run_id` kwargs are explicitly rejected (`_reject_top_level_entity_params` at line 165-172); callers must pass `filters={"user_id": "..."}` instead. Filters are passed directly to the vector store's `search()` and `list()` methods, meaning the vector store itself enforces the scope at query time.

**Session scope & messages** — a deterministic session scope string is built from sorted entity IDs (`_build_session_scope` at `mem0/memory/main.py:412-419`), used as the key for storing recent conversation messages in the SQLite `messages` table.

**Actor filtering** — an optional `actor_id` narrows queries to memories from a specific speaker. This is separate from entity scoping and resolved via `filters["actor_id"]`.

**Multi-tenancy** — the three-identifier system provides natural multi-tenancy. Different users, agents, or runs have fully isolated memory spaces because every store operation writes the identifiers into the payload and every query filters on them. The server's entities REST endpoint (`server/routers/entities.py:43-67`) scans all payloads to aggregate entities by type, supporting admin-level cross-entity queries.

**No tenant-level access control** in the OSS SDK itself — that lives in the hosted platform. The FastAPI server (`server/main.py:1-80`) adds JWT auth and API key authentication.

> **Editor's note.** Addition: `get`, `update`, `delete` and `history` take only a memory id and do not check scope. The REST server authenticates the caller but does not bind them to the `user_id` in the request.

Citations: [mem0/memory/main.py:314-404](https://github.com/mem0ai/mem0/blob/c93420c49a6b14c3d446bdb156d96811908fd90a/mem0/memory/main.py#L314-L404) · [mem0/memory/main.py:143-162](https://github.com/mem0ai/mem0/blob/c93420c49a6b14c3d446bdb156d96811908fd90a/mem0/memory/main.py#L143-L162) · [mem0/memory/main.py:165-172](https://github.com/mem0ai/mem0/blob/c93420c49a6b14c3d446bdb156d96811908fd90a/mem0/memory/main.py#L165-L172) · [mem0/memory/main.py:1295-1322](https://github.com/mem0ai/mem0/blob/c93420c49a6b14c3d446bdb156d96811908fd90a/mem0/memory/main.py#L1295-L1322) · [mem0/memory/main.py:412-419](https://github.com/mem0ai/mem0/blob/c93420c49a6b14c3d446bdb156d96811908fd90a/mem0/memory/main.py#L412-L419) · [server/routers/entities.py:43-67](https://github.com/mem0ai/mem0/blob/c93420c49a6b14c3d446bdb156d96811908fd90a/server/routers/entities.py#L43-L67)

### vectorize-io/hindsight (answered)

Memory isolation is primarily by **bank** — each bank is an isolated memory store ("brain" for one user/agent). All operations require a `bank_id` parameter. Under the hood, tables like `memory_units`, `entities`, and `documents` carry a `bank_id` column, and all queries filter on it (e.g., `memories/pg/recall.py` parameterises every SELECT with `WHERE bank_id = $1`). **Multi-tenancy** is implemented via the `TenantExtension` (`extensions/tenant.py:54-100`). Each tenant gets a PostgreSQL **schema**; all queries use fully-qualified table names (`schema_name.memory_units`) isolated at the database level. The default is `ApiKeyTenantExtension` (`extensions/builtin/tenant.py`) which authenticates via an API key and returns the tenant's schema. **Tag-based scoping** provides a second isolation layer within a bank (`search/tags.py:1-23`). Tags are TEXT[] arrays on `memory_units`, filtered via GIN-indexed PostgreSQL operators (`&&` for overlap, `@>` for containment). Five matching modes control strictness: `any`/`any_strict` (OR), `all`/`all_strict` (AND), and `exact` (set equality for observation scopes). Compound `tag_groups` support boolean trees with AND/OR/NOT groups at any nesting depth. Each bank operation can carry `tags` and `tags_match`, and the reflect agentic loop enforces the caller's tag scope on every internal tool call (`memory_engine.py:15184-15193`). **Admission control** (`api/admission.py:1-100`) bounds concurrency per operation class (recall, retain, reflect) with lane-specific `max_in_flight` and `max_wait_seconds` limits, returning 503 when a request cannot be served within its deadline. **Memory Defense** (`retain/orchestrator.py` imports) is a configurable policy that can block or redact sensitive content before storage, acting as a privacy/safety gate at write time.

> **Editor's note.** Correction: multi-tenancy is opt-in. The built-in ApiKeyTenantExtension is loaded only when HINDSIGHT_API_TENANT_EXTENSION is set; without it every bank lives in the default Postgres schema and isolation is by bank_id (plus tags).

Citations: [hindsight-api-slim/hindsight_api/extensions/tenant.py:54-100](https://github.com/vectorize-io/hindsight/blob/8830bbb3bcde8600431b7ffcfea62074113f263f/hindsight-api-slim/hindsight_api/extensions/tenant.py#L54-L100) · [hindsight-api-slim/hindsight_api/engine/search/tags.py:1-91](https://github.com/vectorize-io/hindsight/blob/8830bbb3bcde8600431b7ffcfea62074113f263f/hindsight-api-slim/hindsight_api/engine/search/tags.py#L1-L91) · [hindsight-api-slim/hindsight_api/engine/memory_engine.py:15184-15193](https://github.com/vectorize-io/hindsight/blob/8830bbb3bcde8600431b7ffcfea62074113f263f/hindsight-api-slim/hindsight_api/engine/memory_engine.py#L15184-L15193) · [hindsight-api-slim/hindsight_api/api/admission.py:1-100](https://github.com/vectorize-io/hindsight/blob/8830bbb3bcde8600431b7ffcfea62074113f263f/hindsight-api-slim/hindsight_api/api/admission.py#L1-L100) · [hindsight-api-slim/hindsight_api/engine/retain/orchestrator.py:19-50](https://github.com/vectorize-io/hindsight/blob/8830bbb3bcde8600431b7ffcfea62074113f263f/hindsight-api-slim/hindsight_api/engine/retain/orchestrator.py#L19-L50)

### getzep/graphiti (answered)

All graph data is scoped by **`group_id`**, a string partition key present on every node and edge (`Node.group_id`, `Edge.group_id`). It serves as multi-tenancy, separating user/agent/session or tenant data. All queries — extraction, search, maintenance — accept `group_ids: list[str]` parameters. Search filters (`SearchFilters`) combine with group_ids for access control at query time. The `@handle_multiple_group_ids` decorator normalizes single/multi group_id calls.

Group-level isolation is enforced at the database level as well. When a group_id differs from the default database name, `_resolve_request_scope()` (graphiti.py:1066) clones the driver with `.clone(database=group_id)`, creating a physically separate graph in FalkorDB or a logically partitioned one in Neo4j. This prevents concurrent requests from different groups from interfering (fix for issue #1676). The MCP server caches these group-scoped driver clones in `_group_drivers` (`graphiti_mcp_server.py:188`).

Deletion is group-aware: `Node.delete_by_group_id()` removes all nodes in a partition. `build_communities()` scopes its community detection to specified group_ids. Communities, sagas, and episodes all carry `group_id`.

**Entity type scoping**: users can define custom Pydantic entity type models (e.g., `Person`, `Organization`) and pass them as `entity_types` to `add_episode()`. The system uses these to classify extracted entities, plus the `excluded_entity_types` parameter to filter out unwanted types. Similarly, `edge_types` scopes which relation types are valid between which entity type pairs.

There is no separate user/role-based access control layer — that's delegated to the application layer. The REST server implements no auth middleware; the MCP server relies on the host's transport security.


Citations: [graphiti_core/graphiti.py:1066-1094](https://github.com/getzep/graphiti/blob/689de295c209631405c00e19e0af4f9735142f13/graphiti_core/graphiti.py#L1066-L1094) · [graphiti_core/nodes.py:93-99](https://github.com/getzep/graphiti/blob/689de295c209631405c00e19e0af4f9735142f13/graphiti_core/nodes.py#L93-L99) · [graphiti_core/edges.py:49-54](https://github.com/getzep/graphiti/blob/689de295c209631405c00e19e0af4f9735142f13/graphiti_core/edges.py#L49-L54) · [mcp_server/src/graphiti_mcp_server.py:188-219](https://github.com/getzep/graphiti/blob/689de295c209631405c00e19e0af4f9735142f13/mcp_server/src/graphiti_mcp_server.py#L188-L219) · [graphiti_core/graphiti.py:1607-1635](https://github.com/getzep/graphiti/blob/689de295c209631405c00e19e0af4f9735142f13/graphiti_core/graphiti.py#L1607-L1635) · [graphiti_core/nodes.py:178-183](https://github.com/getzep/graphiti/blob/689de295c209631405c00e19e0af4f9735142f13/graphiti_core/nodes.py#L178-L183)

### topoteretes/cognee (answered)

Memory is scoped via a multi-tenant hierarchy of Tenant -> User -> Dataset -> Data with Read/Write/Delete/Share permissions enforced through ACLs (cognee/modules/users/models/ACL.py:8). The User model (cognee/modules/users/models/User.py:15) has roles, tenants, and parent_user_id for agent accounts. With ENABLE_BACKEND_ACCESS_CONTROL=true (default), each user+dataset pair gets isolated graph and vector databases through a dataset-database registry. Session records tie sessions to (user_id, dataset_id). recall() supports scope=['session', 'trace', 'session_context', 'graph', 'tools', 'code'] with session_first short-circuit to avoid graph search. The MCP server adds per-client dataset scoping (cursor_vscode_memory, etc.) from clientInfo.name. The teacher-student hierarchy via parent_user_id lets agent accounts inherit permissions. Querying unpermitted datasets raises PermissionDeniedError; name-based lookups only resolve within the caller's own datasets.

> **Editor's note.** Addition: multi-tenant mode (`ENABLE_BACKEND_ACCESS_CONTROL`) is on by default only when the configured graph and vector backends support per-dataset databases (`multi_user_support_possible`). In that mode authentication is forced on.

Citations: [cognee/modules/users/models/User.py:15-47](https://github.com/topoteretes/cognee/blob/b32d8afc59e1064d9291b9828a8a147be9cc8bab/cognee/modules/users/models/User.py#L15-L47) · [cognee/api/v1/recall/recall.py:466-494](https://github.com/topoteretes/cognee/blob/b32d8afc59e1064d9291b9828a8a147be9cc8bab/cognee/api/v1/recall/recall.py#L466-L494) · [cognee-mcp/src/server.py:921-956](https://github.com/topoteretes/cognee/blob/b32d8afc59e1064d9291b9828a8a147be9cc8bab/cognee-mcp/src/server.py#L921-L956)

### supermemoryai/supermemory (answered)

Memory scoping uses a container-tag (space) model. **Space isolation**: every tool operation is scoped by `containerTag` — a string identifier for a named space (`apps/mcp/src/shared/types.ts:39-53`). The `SupermemoryClient` constructor takes an optional `containerTag` which defaults to `'sm_project_default'` (`apps/mcp/src/server/client/index.ts:158-159`). When no explicit tag exists, `getProfile()` returns empty facts (line 271-278). **Multi-tenancy**: the `ActorContext` carries `userId`, `organizationId`, `bearerToken`, and `oauthClientId` (`apps/mcp/src/server/index.ts:227-232`). The SpaceState DurableObject (`apps/mcp/src/server/space-state.ts:26-73`) persists per-actor state keyed by `${org}:${user}`. **Access control**: the `rbac.ts` module provides `effectiveContainerTagAccess()` returning `read`/`write` permissions per tag. Session scope (`sessionScopeSchema` in shared/types.ts:13-21) discriminates `full` vs `scoped` access with optional tag and rate-limit constraints. **Privacy**: there are no client-side privacy controls (redaction, content filtering) — the API is trusted to enforce permissions server-side.


Citations: [apps/mcp/src/server/client/index.ts:158-159](https://github.com/supermemoryai/supermemory/blob/ac2180498223cce078eb263b9d240fdc2f5c3548/apps/mcp/src/server/client/index.ts#L158-L159) · [apps/mcp/src/server/client/index.ts:270-278](https://github.com/supermemoryai/supermemory/blob/ac2180498223cce078eb263b9d240fdc2f5c3548/apps/mcp/src/server/client/index.ts#L270-L278) · [apps/mcp/src/server/index.ts:227-232](https://github.com/supermemoryai/supermemory/blob/ac2180498223cce078eb263b9d240fdc2f5c3548/apps/mcp/src/server/index.ts#L227-L232) · [apps/mcp/src/shared/types.ts:13-21](https://github.com/supermemoryai/supermemory/blob/ac2180498223cce078eb263b9d240fdc2f5c3548/apps/mcp/src/shared/types.ts#L13-L21) · [apps/mcp/src/server/space-state.ts:26-73](https://github.com/supermemoryai/supermemory/blob/ac2180498223cce078eb263b9d240fdc2f5c3548/apps/mcp/src/server/space-state.ts#L26-L73)

### MemoriLabs/Memori (answered)

Memories are scoped through a **three-level hierarchy** of entity → session → conversation, with optional process-level isolation.

**Entity.** The top-level scope is an `entity_id` — a string provided by the caller (typically a user ID). It is hashed with SHA-256 before being sent to cloud endpoints for privacy (`memori/memory/augmentation/_models.py:15-18`). In BYODB mode it maps to `memori_entity.external_id`. Every fact, session, and conversation is ultimately owned by an entity. If `entity_id` is not set, memory extraction (`AdvancedAugmentation.process()` at `memori/memory/augmentation/augmentations/memori/_augmentation.py:126-127`) and recall (`inject_recalled_facts` at `memori/llm/pipelines/recall_injection.py:112-114`) are both skipped entirely.

**Process.** An optional `process_id` distinguishes which agent or process within an application is interacting on behalf of the entity. This maps to `memori_process` and enables process-attribute memories (preferences about a specific agent) separate from entity-level facts. When provided, it is included in cloud API payloads as attribution and stored in the session relationship.

**Session.** A `session_id` (UUID string) ties together entity + process into a session row in `memori_session`. Conversations are scoped to a session. A session has a configurable timeout (`session_timeout_minutes`, default 30, `memori/_config.py:84`). The `conversation.create` driver method (`memori/storage/drivers/postgresql/_driver.py:37-93`) checks the last activity time against this timeout: if the most recent message is within the window, the existing conversation is reused; otherwise a new conversation is created, effectively starting a fresh conversation within the same session.

**Multi-tenancy.** In BYODB mode, each application provides its own database, so tenant isolation is at the database level. In cloud mode, entity IDs are the isolation boundary — the API key determines the project/account, and entity IDs scope within it. There is no role-based access control or per-entity ACL enforcement in the open-source SDK; these are presumed to be handled by the hosting application.

**Privacy.** The `entity_id` is SHA-256 hashed before transmission to the cloud API, preventing direct plaintext ID leaks to the server.

> **Editor's note.** Correction: only the BYODB augmentation payload hashes entity and process ids with SHA-256. In cloud mode, recall, augmentation metadata and agent endpoints send the raw entity_id.

Citations: [memori/memory/augmentation/augmentations/memori/_augmentation.py:125-130](https://github.com/MemoriLabs/Memori/blob/574b1ea3e876f100ef82c37817d603eb7e258e59/memori/memory/augmentation/augmentations/memori/_augmentation.py#L125-L130) · [memori/memory/augmentation/_models.py:15-18](https://github.com/MemoriLabs/Memori/blob/574b1ea3e876f100ef82c37817d603eb7e258e59/memori/memory/augmentation/_models.py#L15-L18) · [memori/storage/drivers/postgresql/_driver.py:37-93](https://github.com/MemoriLabs/Memori/blob/574b1ea3e876f100ef82c37817d603eb7e258e59/memori/storage/drivers/postgresql/_driver.py#L37-L93) · [memori/_config.py:82-94](https://github.com/MemoriLabs/Memori/blob/574b1ea3e876f100ef82c37817d603eb7e258e59/memori/_config.py#L82-L94) · [memori/llm/pipelines/recall_injection.py:112-118](https://github.com/MemoriLabs/Memori/blob/574b1ea3e876f100ef82c37817d603eb7e258e59/memori/llm/pipelines/recall_injection.py#L112-L118)

### MemTensor/MemOS (answered)

Multi-layered: user, cube, session, database. User-level: MOSCore (core.py:38-68) validates via UserManager (user_manager.py:56-100, SQLite/MySQL/Redis) with roles ROOT/ADMIN/USER/GUEST + user_cube_association. Cube-level: GeneralMemCube (mem_cube/general.py:24-48) has separate stores per cube_id. SingleCubeView (single_cube.py:48-100) scopes via UserContext + search_filter. Admin API (admin_router.py) manages per-cube API keys with scopes + expiry. Database-level: PolarDB/Neo4j use_multi_db (api/config.py:868-903) gives each user separate DB; shared mode uses user_name tag. TextualMemoryItem fields: user_id, session_id, visibility private/public/session (item.py:101-148). Search auto-filters user_name + status=activated (preference.py:92-94). CompositeCubeView (composite_cube.py:18-50) fan-out writes + parallel searches across cubes.


Citations: [src/memos/mem_os/core.py:38-68](https://github.com/MemTensor/MemOS/blob/a7367d07e55db61099f7b4e2c1108bc5831a24f3/src/memos/mem_os/core.py#L38-L68) · [src/memos/mem_user/user_manager.py:56-100](https://github.com/MemTensor/MemOS/blob/a7367d07e55db61099f7b4e2c1108bc5831a24f3/src/memos/mem_user/user_manager.py#L56-L100) · [src/memos/multi_mem_cube/single_cube.py:48-100](https://github.com/MemTensor/MemOS/blob/a7367d07e55db61099f7b4e2c1108bc5831a24f3/src/memos/multi_mem_cube/single_cube.py#L48-L100) · [src/memos/api/config.py:868-903](https://github.com/MemTensor/MemOS/blob/a7367d07e55db61099f7b4e2c1108bc5831a24f3/src/memos/api/config.py#L868-L903) · [src/memos/memories/textual/item.py:101-148](https://github.com/MemTensor/MemOS/blob/a7367d07e55db61099f7b4e2c1108bc5831a24f3/src/memos/memories/textual/item.py#L101-L148) · [src/memos/multi_mem_cube/composite_cube.py:18-50](https://github.com/MemTensor/MemOS/blob/a7367d07e55db61099f7b4e2c1108bc5831a24f3/src/memos/multi_mem_cube/composite_cube.py#L18-L50)

### plastic-labs/honcho (answered)

Memory scoping has four layers. **Workspace isolation**: Nearly every table includes `workspace_name` as a composite foreign key, making cross-workspace data leakage structurally impossible at the schema level (`src/models.py` — all Document, Message, Collection, QueueItem, etc. tables). This is enforced by the FK constraints themselves, not by query filters. **Observer/Observed (Collection) isolation**: Documents live in collections keyed by `(workspace_name, observer, observed)`. This means every peer pair has its own vector namespace — peer A's view of peer B is a separate collection from C's view of B. Self-representation (`observer == observed`) is just the diagonal of this matrix. All CRUD operations query through this triple-key filter (`src/crud/document.py:81-87`). **Authentication scoping via JWT**: `src/security.py:31-63` defines JWTParams with a hierarchy — admin (ad), workspace (w), peer (p), session (s). A narrower token never falls back to wider access: `{w: ws, p: alice}` only acts on `alice`, never on a sibling peer. The `require_auth` FastAPI dependency (`security.py:143-194`) checks these claims per-route. Session-scoped peer access is gated by `allow_member_read=True` exclusively on read-only routes, enforced by an explicit allowlist in tests (`tests/routes/test_auth_route_policy.py`). **Scopes**: A `Scope` is a named grouping of sessions that acts as a visibility boundary on recall. Chat, representation, session context, and workspace search answered through a scope see only the contents of that scope's member sessions (`CLAUDE.md:126-131`). Adding a session to a scope copies its explicit conclusions into the scope's collection; removing one reconciles copies back out. Session allowlists (`session_allowlist` parameter on queries) restrict retrieval of both conclusions and messages to specified sessions — and only explicit-level conclusions (level `"explicit"`) are considered safe to scope under this mechanism (`src/utils/representation.py:24`), because higher-level consolidated observations may have been synthesized from sources outside the allowlisted sessions. An empty allowlist fails closed.

> **Editor's note.** Correction: workspace isolation comes from workspace_name filters in the CRUD queries; the composite foreign keys enforce referential integrity, not access isolation.

Citations: [src/models.py:342-382](https://github.com/plastic-labs/honcho/blob/11b22bff8b3fdd3d23d90a53357a418e0cf2e084/src/models.py#L342-L382) · [src/models.py:390-510](https://github.com/plastic-labs/honcho/blob/11b22bff8b3fdd3d23d90a53357a418e0cf2e084/src/models.py#L390-L510) · [src/security.py:31-194](https://github.com/plastic-labs/honcho/blob/11b22bff8b3fdd3d23d90a53357a418e0cf2e084/src/security.py#L31-L194) · [src/utils/representation.py:10-37](https://github.com/plastic-labs/honcho/blob/11b22bff8b3fdd3d23d90a53357a418e0cf2e084/src/utils/representation.py#L10-L37)

### MemMachine/MemMachine (answered)

Memory scoping is hierarchical and metadata-driven. At the top level, **organizations** and **projects** form the identity boundary: every API call passes `org_id` and `project_id`, which together construct the session key `{org_id}/{project_id}` (`packages/server/src/memmachine_server/server/api_v2/service.py:44`). Within a project, the client creates a `Memory` instance with a metadata dictionary that typically includes `group_id`, `agent_id`, `user_id`, and `session_id` (`packages/client/src/memmachine_client/memory.py:116`). When adding or searching memories, this metadata is automatically merged into a SQL-like filter string (e.g. `metadata.user_id='alice' AND metadata.agent_id='travel_agent'`), scoping every operation to that context. Semantic memory uses the **set_id** concept — a string keyed by metadata tags (like `["org_id", "project_id", "user_id"]`). A `SemanticSetType` defines the tag schema; `get_semantic_set_id()` derives or creates the set from concrete metadata values (`memory.py:1105`). Set types can be **org-level** (shared across projects) or project-scoped. Within a set, categories define what facts to extract, and all features carry a `set_id` plus `category_name` for filtering. The MCP server passes `org_id`, `proj_id` (project), and `user_id` via HTTP headers or environment variables (`MM_ORG_ID`, `MM_PROJ_ID`, `MM_USER_ID`) (`mcp.py:127`), stored in `ContextVars` for thread-safe access. There is no built-in cryptographic access control — scoping is by key-based partition isolation rather than authentication/authorization — and the README mentions no ACL system. On the event backend, separate VectorStore collections and SegmentStore partitions (`long_term_memory.py:140`) provide physical data isolation per session. The declarative backend uses Neo4j collection-scoped queries filtered by `session_id`. For multi-tenancy, the `DatabasesConf` supports named database instances, but tenant isolation is at the application layer (org/project keys filtering), not at the database layer.

> **Editor's note.** Correction: episodic memory is partitioned by org_id/project_id only. Per-user or per-agent scoping is a metadata filter that the client adds and the server does not enforce, and the short-term rolling summary covers the whole project and is returned even for filtered queries.

Citations: [packages/server/src/memmachine_server/server/api_v2/service.py:38-55](https://github.com/MemMachine/MemMachine/blob/c1c04bcaf775035f4aa1e61f466e0d9f1a6c6a96/packages/server/src/memmachine_server/server/api_v2/service.py#L38-L55) · [packages/client/src/memmachine_client/memory.py:116-137](https://github.com/MemMachine/MemMachine/blob/c1c04bcaf775035f4aa1e61f466e0d9f1a6c6a96/packages/client/src/memmachine_client/memory.py#L116-L137) · [packages/client/src/memmachine_client/memory.py:885-912](https://github.com/MemMachine/MemMachine/blob/c1c04bcaf775035f4aa1e61f466e0d9f1a6c6a96/packages/client/src/memmachine_client/memory.py#L885-L912) · [packages/server/src/memmachine_server/server/api_v2/mcp.py:127-195](https://github.com/MemMachine/MemMachine/blob/c1c04bcaf775035f4aa1e61f466e0d9f1a6c6a96/packages/server/src/memmachine_server/server/api_v2/mcp.py#L127-L195) · [packages/server/src/memmachine_server/episodic_memory/long_term_memory/long_term_memory.py:122-145](https://github.com/MemMachine/MemMachine/blob/c1c04bcaf775035f4aa1e61f466e0d9f1a6c6a96/packages/server/src/memmachine_server/episodic_memory/long_term_memory/long_term_memory.py#L122-L145) · [packages/server/src/memmachine_server/semantic_memory/semantic_model.py:156-166](https://github.com/MemMachine/MemMachine/blob/c1c04bcaf775035f4aa1e61f466e0d9f1a6c6a96/packages/server/src/memmachine_server/semantic_memory/semantic_model.py#L156-L166)
