How Workspace, Project, and Path Scopes Are Resolved in ai-memory

The ai-memory system resolves workspace, project, and path scopes through a centralized ScopeResolver that evaluates a three-tuple (workspace_id, project_id, path) at the entry points of every HTTP route and MCP tool, enforcing fail-closed policies where reads never create objects and writes require explicit parameters or an active-project pointer.

ai-memory isolates data using a strict hierarchical namespace defined by workspace, project, and path components. All public surfaces—including HTTP admin routes, MCP tools, and the read-only web API—delegate scope resolution to a single ScopeResolver implementation found in crates/ai-memory-store/src/scope.rs. This design ensures consistent behavior across the stack, whether the request originates from a Model Context Protocol (MCP) tool invocation or a direct API call.

Core Scope Concepts: Workspace, Project, and Path

Scope resolution in ai-memory operates on three fundamental primitives defined in crates/ai-memory-core/src/lib.rs:

Concept Description Implementation
Workspace Logical container aggregating multiple projects. WorkspaceId in ai_memory_core
Project Sub-container inside a workspace that holds pages and observations. ProjectId in ai_memory_core
Path Relative file path inside a project (e.g., a markdown page). Handled by the wiki layer in crates/ai-memory-wiki/src/wiki.rs

The combination of these three elements forms a unique address (workspace_id, project_id, path) that identifies any resource in the system.

ScopeResolver Construction and Configuration

The ScopeResolver is instantiated per request and configured with the necessary dependencies to perform lookups or create new entries. According to the constructor at lines 24–31 of crates/ai-memory-store/src/scope.rs, the resolver requires:

  • A read pool (&ReaderPool) for lookups that never create objects.
  • An optional writer handle (Option<&WriterHandle>) required only for write operations that may create workspaces or projects.
  • An optional active-project map (Option<&ActiveProject>) containing the per-actor "current project" pointer populated by lifecycle hooks.
  • The default workspace and project IDs used as server-wide fallbacks.
let resolver = ScopeResolver::new(&store.reader, default_ws, default_proj)
    .with_writer(&store.writer)           // for writes that can create
    .with_active_project(&active_project); // for current-project defaults

All public surfaces delegate to this resolver, ensuring a single, well-defined policy governs scope resolution across the entire application.

Read-Side Resolution Policy (resolve_read_args)

For read operations, the resolve_read_args method (lines 68–82 in scope.rs) follows a strict algorithm that never creates new workspace or project entries:

  1. Explicit pair validation – If both workspace and project names are supplied, the resolver calls lookup_existing to verify the pair exists. This path is read-only and never creates objects.
  2. Partial pair rejection – If only one of the two identifiers is present, the request immediately fails with WorkspaceProjectPairRequired.
  3. Default resolution – When no explicit scope is provided, the resolver delegates to resolve_current_or_project, which:
    • Attempts to use the actor's active-project pointer via ActiveProject::get_for_read.
    • If a project name is supplied without a workspace, it searches the active workspace first, then falls back to the default workspace.
    • If no project name is provided, it returns the active pointer or the default (default_workspace_id, default_project_id).

Missing objects in read paths result in a NotFound error; the system never auto-creates scopes during read operations.

Write-Side Resolution Policy (resolve_write_args)

Write resolution, implemented in resolve_write_args (lines 33–84), differs significantly from read resolution in two key ways: it allows creation of missing scopes and enforces stricter ambiguity checks.

When an explicit workspace/project pair is supplied, the resolver invokes create_explicit_scope via the writer handle, which atomically creates missing workspace or project entries. For unscoped writes where neither identifier is provided:

  1. The resolver looks up the active-project pointer using ActiveProject::lookup_for.
  2. If the pointer resolves to a valid workspace/project pair, that scope is used.
  3. If the pointer references a non-existent project (mismatch), the operation fails with AmbiguousUnscopedWrite, preventing silent mis-filing of data.
  4. If no pointer is set, the system defaults to the global (default_workspace_id, default_project_id).

The writer handle must be present for all write operations; otherwise, the resolver returns WriterRequired.

Scope Resolution Helpers

The ScopeResolver exposes several helper functions used by administrative routes and batch operations, all defined in crates/ai-memory-store/src/scope.rs:

  • lookup_existing_scope – Locates an existing workspace/project pair without creating entries. Used extensively by admin routes that require validation without mutation.
  • create_explicit_scope – The only code path capable of writing new WorkspaceId or ProjectId entries to the store. Called exclusively during write resolution when explicit names are provided.
  • resolve_many_existing – De-duplicates and validates a list of explicit scopes against the database. It fails closed if any entry is missing or if the list exceeds a configurable limit.

These helpers are exercised by the comprehensive test suite, including cases such as read_args_reject_partial_scope, project_only_read_prefers_active_workspace_then_default, and write_args_create_project_in_active_workspace.

Path Resolution and the Wiki Layer

Once the (workspace_id, project_id) tuple is resolved, the path component is processed by the wiki layer. In crates/ai-memory-wiki/src/wiki.rs, the Wiki::write_page and Wiki::apply_batch methods receive a PagePath (relative to the project root) and enforce atomic writes, attribution tracking, and search indexing.

The read API in crates/ai-memory-web/src/routes/page.rs uses the resolved scope to locate the actual markdown file on disk under data/<workspace>/<project>/<path>.md. Thus, the complete triple (workspace_id, project_id, path) uniquely identifies any page or observation in the system, with the ScopeResolver handling the first two components and the wiki layer handling the final path resolution.

Summary

  • Centralized Resolution – All HTTP routes and MCP tools delegate to ScopeResolver in crates/ai-memory-store/src/scope.rs to ensure consistent policy enforcement.
  • Read vs. Write Semantics – Reads use resolve_read_args and never create objects; writes use resolve_write_args and can auto-create scopes only when explicitly requested.
  • Fail-Closed Design – Partial scope pairs are rejected, ambiguous unscoped writes fail with AmbiguousUnscopedWrite, and missing objects return NotFound rather than defaulting to creation.
  • Active-Project Pointer – The ActiveProject type provides per-actor default scopes, checked after explicit parameters but before server-wide defaults.
  • Path Handling – The wiki layer resolves relative paths only after the workspace and project IDs are validated, storing files under data/<workspace>/<project>/<path>.md.

Frequently Asked Questions

What happens if I provide only a workspace name without a project name in ai-memory?

The request is immediately rejected with the WorkspaceProjectPairRequired error. The resolver requires both identifiers together or neither; partial scope specifications are not permitted to prevent ambiguous targeting.

How does ai-memory handle unscoped write operations when no explicit scope is provided?

For unscoped writes, the resolver first checks the actor's active-project pointer via ActiveProject::lookup_for. If the pointer resolves to a valid existing project, that scope is used. If the pointer is unset, the system falls back to the default workspace and project. If the pointer points to a non-existent project, the write fails with AmbiguousUnscopedWrite to prevent data mis-filing.

What is the difference between resolve_read_args and resolve_write_args in the ScopeResolver?

resolve_read_args (lines 68–82) is strictly read-only and fails if the requested workspace or project does not exist, utilizing only the read pool. resolve_write_args (lines 33–84) requires a writer handle, can create missing workspaces or projects via create_explicit_scope, and implements additional logic to validate the active-project pointer for unscoped operations.

Where are the core type definitions for WorkspaceId and ProjectId located?

The core type definitions reside in crates/ai-memory-core/src/lib.rs, which exports WorkspaceId and ProjectId as the fundamental primitives for scope identification. These types are consumed by the store layer, the wiki layer, and all API routes to maintain type safety across the workspace, project, and path hierarchy.

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 →