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

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 and 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, 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

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

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

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 before forwarding to the MCP admin endpoint, preventing accidental workspace consolidations.

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 →