# How ai-memory move-project Handles Merging vs. True Moves Between Workspaces

> Understand how ai-memory move-project handles true moves or merges between workspaces. Learn about the copy-purge strategy and lossless true-move options.

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

---

**The `move-project` command in ai-memory automatically chooses between a lossless "true-move" strategy when the destination is empty, or a "copy-purge" merge strategy when a project already exists, reporting the chosen path via the `moved_via` response field.**

The `move-project` operation is the only ai-memory command that relocates an entire project across workspace boundaries. Implemented across [`crates/ai-memory-wiki/src/wiki.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-wiki/src/wiki.rs) and [`crates/ai-memory-store/src/ops.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-store/src/ops.rs), this operation inspects the destination workspace at runtime and executes one of two mutually exclusive strategies—each with distinct data fidelity guarantees, transactional semantics, and conflict resolution behaviors.

## What Triggers Each Strategy

The destination workspace state determines which path executes:

| Destination Condition | Strategy | Response Value |
|----------------------|----------|----------------|
| No project with matching name exists | **True-move** | `"moved_via": "true-move"` |
| Project with same name already exists | **Copy-purge merge** | `"moved_via": "copy-purge"` |

This branching logic resides in [`crates/ai-memory-mcp/src/admin.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-mcp/src/admin.rs), which handles the `POST /admin/move-project` endpoint and constructs the final JSON response.

## True-Move: Atomic Project Relocation

When the destination workspace has no conflicting project, `move-project` performs a **true-move**—an O(1) operation that preserves every project artifact without rewriting history.

### Execution Flow

1. **Source validation**: Resolve `(from_workspace, project)`; return 404 if absent per `docs/lifecycle-ops.md#L231-L236`.

2. **Same-workspace guard**: Reject if source and destination match (use `rename-project` instead) per `docs/lifecycle-ops.md#L31-L33`.

3. **Workspace provisioning**: Get-or-create the destination workspace row via `wiki.rs#L284-L300`—no new project row is created.

4. **Admission gate**: Acquire the wiki's exclusive mutation lock and fire `op=move_project` webhooks; any reject-policy aborts before changes per `admission.rs#L96-L104`.

5. **Atomic directory rename**: Verify destination absence, then rename `<wiki>/<from_ws>/<proj>` → `<wiki>/<to_ws>/<proj>` atomically per `wiki.rs#L40-L45`.

6. **Transaction re-stamp**: Update `workspace_id` across all domain tables—`projects`, `pages`, `sessions`, `observations`, `handoffs`, `audit_log`, auto-improvement state—in one SQLite transaction per `ops.rs#L2919-L2965`. The `project_id` remains unchanged, preserving all foreign-key relationships.

7. **Response**: Return `"moved_via":"true-move"` with new location per `move_project.rs#L1-L30`.

### Key Guarantees

- **Lossless**: Embeddings, session history, handoffs, observations, and audit trails travel intact
- **Fast**: Constant-time filesystem rename plus single database transaction
- **Safe**: Blocked by live-session guard unless `--force` supplied

## Copy-Purge Merge: Content Consolidation

When ai-memory detects an existing project with the same name at the destination, it switches to **copy-purge** strategy—merging content while discarding source project identity.

### Execution Flow

1. **Collision detection**: Identify existing destination project per `docs/lifecycle-ops.md#L78-L84`.

2. **Page copy**: Invoke `Wiki::write_page` for each source page, triggering sanitization, link re-resolution, FTS updates, and admission/git-mirror webhooks per `wiki.rs#L325-L340`.

3. **Embedding migration**: Copy source embeddings verbatim (stored separately from markdown) per `ops.rs#L2920-L2930`.

4. **Source purge**: Delete source directory and all source DB rows except copied pages, marking `merged_into_existing:true` and `source_purged:true` per `ops.rs#L5305-L5315`.

5. **Conflict resolution** via `--on-conflict`:
   - `block` (default): Abort with 409, list conflicting paths
   - `overwrite`: Source page replaces destination page
   - `duplicate`: Save as `<stem>-from-<src_workspace>.md` using `DEDUP_FROM_TOKEN` constant per `docs/lifecycle-ops.md#L100-L112`

6. **Response**: Return `"moved_via":"copy-purge"` with `conflicts`, `merged_into_existing`, `source_purged` details per `move_project.rs#L45-L60`.

### Critical Limitations

- **Sessions lost**: Source project sessions are not migrated
- **Observations dropped**: Handoffs and observations belonging to source are purged
- **History truncated**: Only latest page bodies copied, not full revision history
- **Identity change**: Destination retains its original `project_id`; source `project_id` ceases to exist

## Live-Session Protection Guard

Both strategies implement identical active-project protection. If a running session has pending observations that would receive a stale `workspace_id`, the move aborts unless `--force` (CLI) or `"force": true` (JSON API) is explicitly provided. The guard lifts only the admission-router check—all transactional and conflict-policy protections remain enforced.

## Practical Usage Examples

### True-Move to Empty Workspace

```bash
ai-memory move-project \
  --from-workspace default \
  --project my-project \
  --to-workspace archive \
  --confirm

```

Expected response: `"moved_via":"true-move"` with complete project preservation.

### Merge with Conflict Overwrite

```bash
ai-memory move-project \
  --from-workspace default \
  --project my-project \
  --to-workspace archive \
  --confirm \
  --on-conflict overwrite

```

Expected response: `"moved_via":"copy-purge"` with `merged_into_existing:true`.

### Direct API Call for Forced Merge

```bash
curl -X POST https://localhost:49374/admin/move-project \
  -H "Content-Type: application/json" \
  -d '{
        "workspace":"default",
        "project":"my-project",
        "destination_workspace":"archive",
        "destination_project":"my-project",
        "confirm":true,
        "on_conflict":"overwrite"
      }'

```

## Summary

- **True-move** (`"moved_via":"true-move"`): Atomic, lossless, O(1) relocation when destination empty—ideal for archiving or reorganizing
- **Copy-purge** (`"moved_via":"copy-purge"`): Content-merging consolidation when destination occupied—useful for deduplication, sacrifices source identity and session data
- Both paths: Transactional, webhook-respecting, protected by live-session guard with `--force` override

## Frequently Asked Questions

### What happens to my embeddings during a move-project operation?

Embeddings are always preserved. In **true-move**, they travel with the unchanged `project_id` via database re-stamping in `ops.rs#L2920-L2930`. In **copy-purge**, they are copied verbatim to the destination project before source purge.

### Can I recover my source project after a copy-purge merge?

No. The copy-purge strategy permanently deletes the source directory and all associated database rows per `ops.rs#L5305-L5315`, except for pages successfully copied. The source `project_id` ceases to exist; only its content survives merged into the destination.

### Why does move-project require --confirm?

The `--confirm` flag acknowledges awareness of destructive potential, particularly for copy-purge merges where source project identity is extinguished. This requirement is enforced at the CLI layer in [`move_project.rs`](https://github.com/akitaonrails/ai-memory/blob/main/move_project.rs) before forwarding to the MCP admin endpoint, preventing accidental workspace consolidations.