# How to Resolve a Project Scope from Caller Coordinates in ai-memory

> Learn how to resolve project scope from caller coordinates in ai-memory. Use resolve_read_args or resolve_write_args to map names to IDs efficiently.

- Repository: [Fabio Akita/ai-memory](https://github.com/akitaonrails/ai-memory)
- Tags: how-to-guide
- Published: 2026-09-11

---

**Call the `resolve_read_args` or `resolve_write_args` methods on a `ScopeResolver` instance to map caller-supplied workspace and project names to internal `WorkspaceId` and `ProjectId` pairs, using the resolution logic implemented in [`crates/ai-memory-store/src/scope.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-store/src/scope.rs).**

The ai-memory system identifies resources through internal ID pairs, but callers typically provide human-readable coordinates like workspace and project names. Understanding how to resolve a project scope from caller coordinates is essential when building MCP servers, HTTP handlers, or CLI tools that interact with the ai-memory store.

## Understanding the ScopeResolver Architecture

The **ScopeResolver** type acts as the central authority for translating caller coordinates into internal identifiers. Located in [`crates/ai-memory-store/src/scope.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-store/src/scope.rs), this struct encapsulates all resolution logic and maintains references to the storage backend and default configurations.

### Internal IDs vs Caller Coordinates

Project scopes in ai-memory are identified by two integers: `WorkspaceId` and `ProjectId`. However, external callers (MCP clients, HTTP requests, or CLI commands) supply *coordinates*—optional string names for the workspace and project they want to access. The resolver bridges this gap by querying the SQLite store and applying fallback rules when coordinates are incomplete.

### Core Components

The resolver relies on three primary dependencies:

- **`ReaderPool`** – Provides async read access to the SQLite store for looking up existing workspaces and projects
- **`WriterHandle`** – Enables create-on-write behavior for resolving scopes during mutations
- **`ActiveProject`** – Tracks per-actor "current project" state, allowing callers to omit explicit coordinates when operating within an established context

## Building a ScopeResolver Instance

Before resolving any coordinates, you must construct the resolver with appropriate defaults and optional helper attachments.

### Constructor and Defaults

Initialize the resolver using `ScopeResolver::new()`, passing a read-only `ReaderPool` and the default workspace/project IDs that the server should use when callers supply no explicit coordinates:

```rust
use ai_memory_store::{ScopeResolver, ReaderPool};

let resolver = ScopeResolver::new(
    reader_pool,
    default_workspace_id,
    default_project_id
);

```

According to the source code at lines 24-31 in [`crates/ai-memory-store/src/scope.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-store/src/scope.rs), this constructor stores the reader and defaults within the struct, preparing the resolver for subsequent resolution calls.

### Attaching Optional Helpers

For advanced use cases, chain builder methods to attach write capabilities and active-project tracking:

- **`with_writer(writer)`** – Attaches a `WriterHandle` for create-on-write resolution
- **`with_active_project(active_project)`** – Attaches an `ActiveProject` map for current-project fallbacks

```rust
let resolver = ScopeResolver::new(reader, default_ws, default_proj)
    .with_writer(writer_handle)
    .with_active_project(active_project_map);

```

These methods, found at lines 41-53 in [`scope.rs`](https://github.com/akitaonrails/ai-memory/blob/main/scope.rs), simply set optional fields on the resolver struct.

## Resolving Read Operations

For read-only operations, use `resolve_read_args` to convert caller coordinates into validated internal IDs without modifying the store.

### The resolve_read_args Method

This method accepts optional workspace and project names plus an `ActorKey` identifying the caller. It follows a strict resolution hierarchy defined at lines 68-82 in [`scope.rs`](https://github.com/akitaonrails/ai-memory/blob/main/scope.rs):

1. If both workspace and project names are provided, resolve them directly
2. If only one is provided, return a `WorkspaceProjectPairRequired` error
3. If neither is provided, delegate to `resolve_current_or_project` for fallback logic

```rust
let scope = resolver
    .resolve_read_args(
        optional_workspace_name,
        optional_project_name,
        &actor_key
    )
    .await?;

// Returns ResolvedScope { workspace_id, project_id }

```

### Current-Project Fallback Logic

When callers supply no explicit coordinates, `resolve_current_or_project` (lines 84-115) attempts three fallbacks in order:

1. The caller's active-project entry from the `ActiveProject` map
2. The default workspace's project
3. The server-wide default workspace/project pair

If the caller provides a project name without a workspace, the resolver prefers the active workspace's project; otherwise, it returns the active project or defaults directly.

## Handling Write Operations

Write paths require `resolve_write_args`, which behaves like the read-side counterpart but can create missing workspaces or projects on demand.

### Create-on-Write Behavior

When `resolve_write_args` receives a project name without explicit coordinates, it first attempts the active-project fallback. If a project name **is** supplied, the method requires an attached writer handle and calls `writer.get_or_create_workspace()` and `writer.get_or_create_project()` to ensure the entities exist before returning their IDs.

This logic spans lines 33-86 in [`scope.rs`](https://github.com/akitaonrails/ai-memory/blob/main/scope.rs) and handles the "ambiguous unscoped write" case by either using the active project or creating new entities based on the writer configuration.

For batch operations, `resolve_many_existing` deduplicates a list of explicit `(workspace, project)` pairs without creating anything, delegating to `resolve_many_existing_scopes` at lines 86-93.

## Integration Examples

The resolver appears throughout the ai-memory codebase, demonstrating consistent patterns for both server and CLI contexts.

### MCP Server Usage

In [`crates/ai-memory-mcp/src/server.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-mcp/src/server.rs) (lines 1244-1245), the MCP server builds a resolver once per request and exposes it via a `scope_resolver()` method. This pattern ensures that each incoming MCP request gets a fresh resolver instance with the current actor's context and server defaults.

### CLI Serve Command

The CLI's HTTP server follows the same architecture in [`crates/ai-memory-cli/src/commands/serve.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-cli/src/commands/serve.rs). When handling incoming HTTP calls, the command constructs a `ScopeResolver` with the configured defaults and attaches the appropriate reader and writer pools to handle both read and write operations from API clients.

## Summary

- **ScopeResolver** in [`crates/ai-memory-store/src/scope.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-store/src/scope.rs) handles all coordinate-to-ID translation
- Build resolvers with `ScopeResolver::new()` and chain `with_writer()` or `with_active_project()` as needed
- Use `resolve_read_args()` for queries; it errors with `WorkspaceProjectPairRequired` if coordinates are incomplete
- Use `resolve_write_args()` for mutations; it creates missing workspaces/projects via an attached `WriterHandle`
- Leverage `ActiveProject` maps to support "current project" semantics for CLI and interactive contexts
- Both the MCP server and CLI serve command instantiate per-request resolvers following identical patterns

## Frequently Asked Questions

### What happens if I provide only a workspace name without a project name?

The resolver returns a `WorkspaceProjectPairRequired` error. According to the implementation in `resolve_read_args`, ai-memory requires either both coordinates (workspace and project) or neither, relying on the fallback chain to determine the scope. Partial coordinates are considered ambiguous and are rejected.

### How does the active project map work in scope resolution?

The `ActiveProject` map tracks the current project for each `ActorKey`. When no explicit coordinates are provided, `resolve_current_or_project` queries this map first. If the caller has established a current project in previous interactions, that scope is used. This enables CLI workflows where users "cd" into a project and subsequent commands operate implicitly within that context.

### Can I resolve multiple scopes at once?

Yes. Use `resolve_many_existing`, which accepts a list of `(workspace, project)` tuples and deduplicates them without creating new entities. This method delegates to `resolve_many_existing_scopes` and is useful for batch validation operations where you need to verify that multiple referenced scopes exist in the database.

### What is the difference between `resolve_read_args` and `resolve_write_args`?

**`resolve_read_args`** strictly validates existing scopes and never modifies the database, making it suitable for queries. **`resolve_write_args`** handles mutation contexts: it can create workspaces and projects on demand using an attached `WriterHandle`, and it follows slightly different fallback rules to handle "unscoped writes" that should target the active project or create new entities.