Migrate memories from other local memory systems into Engram - #6
Migrate memories from other local memory systems into Engram#6jfrancoa wants to merge 3 commits into
Conversation
One-shot migration of an existing local memory store into Engram, built around a small source-adapter contract so new systems are one module + one registry entry. First source: claude-mem (SQLite, read-only). - Two ingestion paths: pre-extracted (verbatim, [date]-prefixed, explicit topic mapping validated against the live group schema) and conversation (extraction pipeline with created_at date context, submitted strictly earliest-to-latest; a slow run aborts resumably instead of skipping ahead to preserve chronology) - Checkpoint in ~/.engram/migrate/<source>.json makes every run idempotent and resumable; --rollback deletes exactly what the migration created via the server's per-run commit manifests - Dry-run by default; --execute writes. Repo scoping via git-remote probing with --map overrides; unmappable projects are skipped, never mis-filed. Group-required scope properties are checked up front (session_id auto-filled with a migration marker) - Ships as bin/engram-migrate (self-locating, plugin bin/ is on PATH) and the /engram:migrate command; stdlib-only unit tests included - core/__init__.py re-exports are now lazy and the SDK import moved inside get_client(), so dry-run and tests work without the venv Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Orca Security Scan Summary
| Status | Check | Issues by priority | |
|---|---|---|---|
| Infrastructure as Code | View in Orca | ||
| SAST | View in Orca | ||
| Secrets | View in Orca | ||
| Vulnerabilities | View in Orca |
Review findings (F1-F14) against the migrate feature, fixed: - rollback: refuse manifests of still-running runs, record non-404 delete failures instead of crashing, require a stable identity, detect an identity change via a user_id stamp in the checkpoint, and keep the checkpoint when every delete reports already-gone (possible mismatch) - conversation chronology: abort on a failed batch (not just a timed-out one), refuse to submit while earlier runs are still in flight, and cap one conversation at MAX_CONVERSATION_MESSAGES - validation: --execute refuses to run without the group schema instead of failing open per batch; --property repo_name is rejected; --topic-map and --batch-size error in conversation mode instead of being ignored - robustness: submit failures are recorded per batch instead of killing the CLI; _wait and reconcile surface the real error and release runs the server 404s; corrupt checkpoints exit cleanly with the file named - CLI args: --limit/--batch-size must be positive, --limit counts fresh (post-checkpoint) records, --repos-dir is expanduser'd, empty KEY=VALUE halves are rejected; exit code 3 marks an incomplete (pending) run - adapter: NULL observation types no longer crash the report; non-array facts JSON is kept verbatim instead of mangled - report: per-project counts, honest checkpoint label, mode-agnostic sample line, no --map advice for unmappable "(none)" records - docs: /engram:migrate strips --execute/--rollback from the dry-run step; README clarifies where engram-migrate is on PATH; stale docstrings and the launcher's env-precedence comment corrected Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds a one-shot migration tool to import memories from other local memory systems into Engram (initially claude-mem), with a dry-run-first workflow, resumable checkpointing, and optional rollback. It also refactors core imports so SDK-free entry points (migration dry-run and unit tests) can run outside the plugin venv.
Changes:
- Introduces migration adapter/engine/CLI (
core.migrate) including repo resolution, batching, checkpointing, execution, and manifest-based rollback. - Adds
claude-memSQLite adapter plus unit tests covering curation, planning, execution flow, reconciliation, and rollback. - Updates docs/command metadata and bumps plugin version to
0.2.0; refactorscoreexports and client import to keep SDK imports lazy.
Reviewed changes
Copilot reviewed 10 out of 12 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| README.md | Documents how to run migrations and explains key flags/modes. |
| plugin/tests/test_migrate.py | Adds migration unit tests (adapter behavior, planning, checkpoints, execution, rollback). |
| plugin/tests/init.py | Marks tests as a package for discovery/imports. |
| plugin/core/migrate/engine.py | Implements planning/execution, checkpointing, reconciliation, conversation mode, and rollback. |
| plugin/core/migrate/claude_mem.py | Implements the claude-mem read-only SQLite adapter and selection reporting. |
| plugin/core/migrate/main.py | Adds CLI entry point with dry-run default, execute/rollback flows, and schema/props validation. |
| plugin/core/migrate/init.py | Defines the adapter contract, Record, kinds, and adapter registry. |
| plugin/core/client.py | Makes Engram SDK import local to get_client() to support SDK-free code paths. |
| plugin/core/init.py | Switches to lazy re-exports via PEP 562 to avoid importing SDK at package import time. |
| plugin/commands/migrate.md | Adds Claude command guidance for safe dry-run-first migrations and confirmation gating. |
| plugin/bin/engram-migrate | Adds CLI launcher that derives plugin root/data env and runs migration inside the venv. |
| plugin/.claude-plugin/plugin.json | Bumps plugin version to 0.2.0. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Percent-encode the sqlite URI path so ?/#/% in --db can't smuggle URI params past mode=ro - Conversation mode excludes records without a usable created_at and reports the count (they can't be placed chronologically; pre-extracted mode carries them), instead of silently importing them first Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 12 changed files in this pull request and generated no new comments.
Suppressed comments (3)
plugin/core/migrate/main.py:51
- Same exit-code issue as above:
sys.exit(<message>)here returns 1 even though this is a usage/validation error. Exiting with code 2 keeps the documented contract and aligns with argparse’s behavior.
if not k.strip() or not v.strip():
sys.exit(f"--{flag} {p!r}: name and value must be non-empty")
plugin/core/migrate/main.py:46
_parse_kvusessys.exit(<message>), which exits with status code 1. This contradicts the module’s documented exit codes (“2 usage error”) and makes bad--map/--topic-map/--propertyvalues indistinguishable from runtime failures. Emit the message on stderr and exit(2) instead.
This issue also appears on line 50 of the same file.
if "=" not in p:
sys.exit(f"--{flag} expects NAME=VALUE, got {p!r}")
plugin/core/client.py:92
get_client()imports the Engram SDK before checking whether an API key exists. Hooks callget_client()even when memory is disabled; in environments without the SDK installed this will raiseModuleNotFoundErrorinstead of returningNone. CheckENGRAM_API_KEYfirst, and only then import the SDK (optionally raising a clearer error if the key is set but the SDK is missing).
def get_client():
# SDK import stays local: everything else in this module (key/identity resolution, the
# REST helper) is stdlib-only and must keep working where the SDK isn't installed.
from engram import EngramClient
|
Superseded by #7, which ships the same migration feature with the guided flow as a harness-independent skill instead of a slash command (Codex-portable). Keeping this open as the command-based fallback — only one of the two should merge. |
Motivation
Anyone switching to Engram from another local memory system arrives with an existing store of accumulated memories and no way to bring it along. This adds a one-shot importer (
/engram:migrate,bin/engram-migrate) with claude-mem as the first supported source. Bumps the plugin to 0.2.0.Approach
Records speaking a deliberately tiny, Engram-agnostic vocabulary of four kinds; the engine maps kinds to the group's actual topics (--topic-mapfor custom groups, validated against the live schema before anything is sent), resolves source project names toowner/repovia git-remote probing, batches per repo scope, and submits through the pre-extracted pipeline — the content was already LLM-summarized by the source, so no re-extraction. A new source is one adapter module plus asources()entry.core/__init__.pynow lazy re-exports via PEP 562 andclient.pyimports the SDK insideget_client(), keeping SDK-free entry points working outside the plugin venv.~/.engram/migrate/<source>.jsonrecords done/pending uids around every step (atomic writes); a re-run reconciles pending runs against server status and only sends what's missing.--all.--map) — a wrongrepo_namewould make memories unrecallable, which is worse than absent.--input conversationingests through the extraction pipeline with realcreated_atdate context; strictly chronological per the API contract, so an unfinished run aborts (resumably) rather than letting later days overtake it.--rollbackdeletes exactly what the migration created, via the server's per-run commit manifests.Key areas for review
plugin/core/migrate/engine.pyexecute/reconcile_pending— checkpoint discipline: pending is written before the wait, done/failed transitions after; verify no path can double-submit or lose uidsplugin/core/migrate/engine.pyrollback— deletes only memory ids listed in the migration's own run manifests (runs.get().committed_operations), so organically stored memories are untouchable by construction; checkpoint is kept when any manifest fetch failsplugin/core/__init__.py— the lazy re-export refactor sits under the existing hooks' import path; this is the only change touching live hook behaviorplugin/core/migrate/__main__.py_batch_properties— required-scope handling:session_idauto-filled with amigration:<source>marker (found via live smoke: the add API rejects writes missing any group-required property), all other required props must be explicitplugin/core/migrate/claude_mem.py— read-only guarantee is at the sqlite level (mode=roURI), not by conventionRisks and mitigations
Testing
plugin/tests/test_migrate.py) against a synthetic claude-mem DB and a fake client: curation/composition (including legacytextandfacts-only rows), read-only enforcement, batching andskip_uids, chronological conversation ordering, checkpoint roundtrip, pending reconciliation, execute commit-and-checkpoint flow, conversation input building, manifest-based rollback. Green on system python and the plugin venv.--mapguidance.🤖 Generated with Claude Code