# How the ai-memory Scope Resolution System Handles Missing Workspaces and Projects

> Discover how the ai-memory scope resolution system manages missing workspaces and projects with a fail-closed, create-on-write policy. Learn about error handling and silent creation for authorized writes.

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

---

**The ai-memory scope resolution system enforces a strict "fail-closed, create-on-write" policy that returns specific errors for missing workspaces and projects during read operations while allowing silent creation during authorized write operations.**

The `akitaonrails/ai-memory` repository implements a robust scope resolution mechanism in [`crates/ai-memory-store/src/scope.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-store/src/scope.rs) that governs how the runtime resolves workspace and project pairs for every store request. The system distinguishes strictly between read-only lookups and write-capable creation paths, ensuring predictable behavior when resources are absent.

## Explicit Scope Lookup Failure Modes

When a request provides explicit workspace and project names, the `lookup_existing_scope` function (lines 81-95) enforces strict validation. The resolution flow first calls `lookup_existing_workspace` (lines 200-214) to verify the workspace exists, then attempts to locate the project within that workspace context.

If the workspace name cannot be found, the system returns `ScopeResolutionError::WorkspaceNotFound`. If the workspace exists but the specified project is absent, it returns `ScopeResolutionError::ProjectNotFoundInWorkspace`. These errors propagate to the HTTP layer, which translates them into standard 404 responses according to the implementation in [`crates/ai-memory-web/src/routes/api.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-web/src/routes/api.rs).

## Partial Input Rejection

The system rejects incomplete scope specifications early in the resolution pipeline. When only one of the two required names (workspace or project) is supplied, the `resolve_read_args` function (lines 58-70) immediately returns `ScopeResolutionError::WorkspaceProjectPairRequired`.

This prevents ambiguous half-filled scopes from being interpreted inconsistently. The validation ensures that downstream code always operates on complete workspace-project pairs or explicit error states, never attempting to infer missing components.

## Project-Only Resolution with Fallback Logic

For requests specifying only a project name without an explicit workspace, the `resolve_current_or_project` function (lines 73-100) implements a tiered fallback strategy. The resolver first checks the *active-project* map associated with the requesting actor to identify the current workspace context.

If the project exists in the actor's active workspace, resolution succeeds. If not, the system falls back to the *default workspace* configured at startup. Only if the project cannot be found in either location does the system return `ScopeResolutionError::ProjectNotFoundInActiveOrDefault`.

## Create-on-Write Policy

Write operations follow fundamentally different rules from reads. Only code paths possessing a `WriterHandle` may invoke `create_explicit_scope`, located at lines 16-33 of [`crates/ai-memory-store/src/scope.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-store/src/scope.rs).

This function calls `writer.get_or_create_workspace` and `writer.get_or_create_project`, which silently insert missing workspaces and projects into the store. This "create-on-write" capability is restricted to ensure that read-only paths never modify the store structure, while write paths can bootstrap new scopes dynamically. Attempts to create scopes without a valid writer handle result in a `WriterRequired` error.

## Global Preferences Scope Handling

The system treats the reserved global scope differently from user-defined workspaces. The `lookup_global_scope` helper function (lines 35-44) returns `Ok(None)` when the global workspace or project does not exist.

Rather than treating this absence as an error, the system interprets it as "nothing to union in" during preference resolution. This allows the global scope to remain optional while still supporting union queries across scopes.

## Implementation Architecture

The `ScopeResolver` type bundles four critical components: a `ReaderPool`, an optional `WriterHandle`, the active-project map for actor contexts, and default workspace/project identifiers. Its public methods enforce the three core guarantees of the system:

- **Read-only paths never create missing scopes.** All lookup operations require pre-existing resources.
- **Write-on-create paths must hold an explicit writer handle.** Without `WriterHandle` authorization, creation attempts fail immediately.
- **Partial input is rejected early.** The `WorkspaceProjectPairRequired` error prevents ambiguous resolution states.

Higher-level APIs including MCP tools and HTTP admin routes delegate to these helpers. For example, the web API's `resolve_scopes` function calls `resolve_many_existing_scopes` for search queries, which uses `lookup_existing_scope` for each name pair, enforcing the no-creation rule across batch operations.

## Code Examples

The following examples demonstrate the resolution behaviors in practice:

```rust
// Resolve a read request with only a project name
let resolved = resolver
    .resolve_current_or_project(Some("my-project"), &actor_key)
    .await?;

```

If "my-project" does not exist in the actor's active workspace or the default workspace, this returns `ScopeResolutionError::ProjectNotFoundInActiveOrDefault`.

```rust
// Create missing workspace/project on write
let scope = ai_memory_store::create_explicit_scope(
    &writer_handle,
    "new-workspace",
    "new-project",
).await?;

```

The `create_explicit_scope` function automatically inserts the workspace and project if absent, requiring valid write permissions.

```rust
// Resolve explicit scopes for search (no creation allowed)
let names = vec![
    ScopeName::new("ws-1", "proj-a"),
    ScopeName::new("ws-2", "proj-b"),
];
let scopes = ai_memory_store::resolve_many_existing_scopes(&reader_pool, &names, 10).await?;

```

Missing workspaces or projects in this batch operation result in `WorkspaceNotFound` or `ProjectNotFoundInWorkspace` errors, causing the entire request to fail with a 404 status.

## Summary

- **Explicit lookups** validate both workspace and project existence in `lookup_existing_scope`, returning specific `ScopeResolutionError` variants for missing resources.
- **Partial inputs** are rejected immediately with `WorkspaceProjectPairRequired` to prevent ambiguous scope interpretation.
- **Project-only queries** check the actor's active project map first, then fall back to the default workspace before failing with `ProjectNotFoundInActiveOrDefault`.
- **Write operations** with a `WriterHandle` can create missing scopes via `create_explicit_scope`, while read operations strictly fail closed.
- **Global scopes** return `Ok(None)` when absent rather than errors, supporting optional preference union operations.

## Frequently Asked Questions

### What error does ai-memory return when a workspace does not exist?

When resolving explicit scope names, the system returns `ScopeResolutionError::WorkspaceNotFound` if `lookup_existing_workspace` cannot locate the specified workspace. This error typically propagates to the HTTP layer as a 404 response, indicating the client requested a non-existent workspace.

### Can read operations create missing projects or workspaces in ai-memory?

No. Read-only code paths strictly enforce a "fail-closed" policy via functions like `lookup_existing_scope` and `resolve_many_existing_scopes`. Only write operations possessing a valid `WriterHandle` can create missing resources through the `create_explicit_scope` function.

### How does ai-memory handle requests with only a project name but no workspace specified?

The `resolve_current_or_project` function implements a two-tier fallback: it first checks the requesting actor's active-project map, then falls back to the default workspace configured at startup. If the project exists in neither location, it returns `ScopeResolutionError::ProjectNotFoundInActiveOrDefault`.

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

The system rejects incomplete scope specifications early in the resolution pipeline. The `resolve_read_args` function returns `ScopeResolutionError::WorkspaceProjectPairRequired` when only one of the two required identifiers is supplied, preventing partial scope resolution.