# How Project Scope Is Determined in ai-memory: The ScopeResolver Mechanism

> Discover how ai-memory determines project scope using the ScopeResolver mechanism. Learn about explicit parameters, project markers, and default fallbacks.

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

---

**ai-memory determines project scope through the `ScopeResolver` struct in [`crates/ai-memory-store/src/scope.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-store/src/scope.rs), which isolates data by workspace and project using a combination of explicit parameters, active project detection via [`.ai-memory.toml`](https://github.com/akitaonrails/ai-memory/blob/main/.ai-memory.toml) markers, and fallback defaults.**

Understanding how project scope is determined in ai-memory is essential for managing isolated data environments across collaborative AI workflows. The akitaonrails/ai-memory repository implements a strict isolation mechanism centered around the `ScopeResolver` type, which guarantees that every operation executes within the correct workspace and project boundary defined by the user, CLI context, or explicit API parameters.

## The ScopeResolver Core Architecture

Every request initializes a `ScopeResolver` by calling `ScopeResolver::new` with a borrowed `ReaderPool` and the default workspace and project IDs. According to the source code at [`scope.rs:L167-L173`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-store/src/scope.rs#L167-L173), this constructor establishes the baseline isolation context, typically referencing the "current-project" defaults.

The resolver supports two optional helper attachments configured via builder methods:

- **`with_writer`** – Attaches a `WriterHandle` for requests that may create new scopes during write operations
- **`with_active_project`** – Attaches the `ActiveProject` map when the request should honor the current directory's project context

These attachments are implemented at [`scope.rs:L30-L42`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-store/src/scope.rs#L30-L42).

## Resolving Read Operations

The `resolve_read_args` method determines which workspace and project to use for read queries. Located at [`scope.rs:L357-L376`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-store/src/scope.rs#L357-L376), this function implements a strict lookup-only policy:

1. **Explicit parameters** – If both `workspace` and `project` are supplied, the resolver calls `lookup_existing_scope` without creating any new entries
2. **Partial parameters** – If only one is supplied, the missing value defaults to the active project (if attached) or the default IDs
3. **No parameters** – Falls back entirely to the default workspace and project established during resolver construction

## Resolving Write Operations

Write operations follow a similar resolution path via `resolve_write_args` at [`scope.rs:L382-L401`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-store/src/scope.rs#L382-L401), with one critical distinction: this method can create missing scopes. When a write request targets a non-existent workspace or project, the resolver invokes `create_explicit_scope` to insert the new entities into the underlying SQLite store before proceeding with the operation.

## Auto-Scope Detection via .ai-memory.toml

The **auto-scope** mechanism enables automatic project detection without explicit parameters. As documented in [`docs/auto-scope.md`](https://github.com/akitaonrails/ai-memory/blob/main/docs/auto-scope.md) and implemented in [`crates/ai-memory-core/src/routing_snippet.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-core/src/routing_snippet.rs), the system searches the current directory and its ancestors for a [`.ai-memory.toml`](https://github.com/akitaonrails/ai-memory/blob/main/.ai-memory.toml) marker file. The `ActiveProject` map extracts workspace and project names from this file, allowing the CLI to default to the "current-project" context when launched inside a recognized project directory. If no marker is found, the system falls back to the scratch workspace defined at startup.

## Global Scope Handling

ai-memory defines a special **global scope** via the constant `GLOBAL_SCOPE_PROJECT` within the default workspace. The `lookup_global_scope` and `create_global_scope` functions at [`scope.rs:L35-L63`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-store/src/scope.rs#L35-L63) manage this shared context. Read operations automatically union results from the global scope unless the caller explicitly sets `global=false`. Write operations targeting the global scope use the dedicated creation path to ensure the shared project exists.

## Multi-Scope Resolution

For operations spanning multiple contexts, the `resolve_many_existing_scopes` function at [`scope.rs:L83-L102`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-store/src/scope.rs#L83-L102) handles de-duplication and enforces a hard limit on the number of scopes processed in a single request. This prevents resource exhaustion during bulk operations by validating the scope list against existing records before execution.

## Server Integration

The MCP/HTTP server instantiates the resolver once per request via the `scope_resolver()` helper at [`server.rs:L1233-L1235`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-mcp/src/server.rs#L1233-L1235). It feeds the resolver workspace and project IDs extracted from URL path parameters or the request's active-project context, ensuring consistent isolation across API boundaries.

## Practical Implementation Examples

```rust
// Resolve a read request inside a running CLI session
let resolver = ScopeResolver::new(&reader_pool, default_ws, default_prj)
    .with_active_project(&active_project);
let resolved = resolver
    .resolve_read_args(Some("my_ws"), None, &actor_key)
    .await?;               // → uses "my_ws" and the active project as defaults

```

```rust
// Resolve a write request that may create a new project
let resolver = ScopeResolver::new(&reader_pool, default_ws, default_prj)
    .with_writer(&writer_handle);
let resolved = resolver
    .resolve_write_args(Some("new_ws"), Some("new_prj"), &actor_key)
    .await?;               // → creates “new_ws/new_prj” if they do not exist

```

```rust
// Direct lookup for admin routes that must not create anything
let existing = lookup_existing_scope(&reader_pool, "existing_ws", "existing_prj")
    .await?;               // Returns ResolvedScope or an error if missing

```

```rust
// Resolving many scopes for a bulk operation
let scopes = vec![
    ScopeName { workspace: "ws1".into(), project: "prjA".into() },
    ScopeName { workspace: "ws2".into(), project: "prjB".into() },
];
let resolved = resolve_many_existing_scopes(&reader_pool, &scopes, 10).await?;

```

## Summary

- **ScopeResolver** is the single source of truth for project scope determination, instantiated per-request with default workspace and project IDs
- **Read operations** strictly lookup existing scopes via `resolve_read_args`, while **write operations** can create missing scopes via `resolve_write_args` and `create_explicit_scope`
- **Auto-scope detection** relies on the [`.ai-memory.toml`](https://github.com/akitaonrails/ai-memory/blob/main/.ai-memory.toml) marker file and the `ActiveProject` map to automatically determine context from the current working directory
- **Global scope** is automatically included in reads unless explicitly disabled, providing a shared context across all workspaces
- **Multi-scope operations** are validated and de-duplicated through `resolve_many_existing_scopes` to enforce limits and prevent conflicts

## Frequently Asked Questions

### What happens if I don't specify a workspace or project in ai-memory?

The resolver falls back to the default workspace and project IDs passed during `ScopeResolver::new` construction. In CLI contexts, these defaults correspond to the "current project" detected via the [`.ai-memory.toml`](https://github.com/akitaonrails/ai-memory/blob/main/.ai-memory.toml) marker file or the scratch workspace defined at startup if no marker exists.

### How does ai-memory automatically detect the current project?

When `with_active_project` is called on the resolver, it activates the **auto-scope** mechanism. This searches the current directory and its ancestors for a [`.ai-memory.toml`](https://github.com/akitaonrails/ai-memory/blob/main/.ai-memory.toml) file, extracting workspace and project names to use as defaults. If the file is not found, the system relies on explicit parameters or fallback defaults.

### Can I write to multiple scopes at once in ai-memory?

Yes, using `resolve_many_existing_scopes`, which accepts a vector of `ScopeName` structs. This function de-duplicates the list and enforces a hard limit on the number of scopes to prevent resource exhaustion, but note that it only resolves existing scopes and does not create new ones during the bulk lookup.

### What is the global scope in ai-memory and how is it accessed?

The global scope is a special project (`GLOBAL_SCOPE_PROJECT`) within the default workspace that provides shared data across all contexts. It is automatically unioned with specific project results during read operations unless `global=false` is explicitly set. Writes to the global scope use dedicated `create_global_scope` and `lookup_global_scope` functions to ensure the shared project exists.