Skip to content

feat(migrate): mempalace migrate-wings — normalize legacy wing names (#1675 follow-up) - #1702

Merged
igorls merged 2 commits into
developfrom
feat/migrate-wing-normalize
Jun 6, 2026
Merged

feat(migrate): mempalace migrate-wings — normalize legacy wing names (#1675 follow-up)#1702
igorls merged 2 commits into
developfrom
feat/migrate-wing-normalize

Conversation

@igorls

@igorls igorls commented Jun 6, 2026

Copy link
Copy Markdown
Member

What

Adds mempalace migrate-wings [--dry-run] [--yes] — a one-time, idempotent migration that normalizes legacy wing names (strips leading/trailing separators) so palaces built before the wing-name rule (#1675) keep their memories discoverable under the new name.

The problem: a Claude-Code path-encoded dir like -home-user-proj derived _home_user_proj. The normalized derivation strips that to home_user_proj, so new mining/diary writes land on a different wing than the existing drawers — the history splits (it isn't lost, just stranded under the old name). This closes that split.

How (design verified against the codebase)

  • IDs left untouched. Drawer/closet IDs embed the wing as an opaque prefix that is never decoded back into a wing (grep-verified — nothing splits a wing out of an ID), and mining idempotency keys on source_file (prefetch_mined_set), not on recomputed IDs. So the migration only re-keys the metadata wing field in place: closet →drawer_id pointers stay valid (no document rewrite) and future mining still skips already-mined files (no duplicates). This avoids a risky extract-rebuild.
  • Stores updated: drawer metadata + closet metadata (wing), and the topics_by_wing registry (re-keyed, merging on collision). Tunnels resolve via existing read-time normalization (palace_graph._normalize_wing) and need no rewrite.
  • Backend-agnostic (uses the get/update collection abstraction), idempotent, dry-run-able, with collision merges reported.
  • The leading/trailing strip is applied by the migration itself, so it's correct whether or not the running build's normalize_wing_name already carries the fix(config): strip leading/trailing separators in normalize_wing_name #1675 change.

Tests

tests/test_migrate_wings.py: pure planner coverage (strip leading/trailing, no-op on clean wings, ignore empty/non-string/all-separator, collision→same target) + hermetic backend integration (relabel, merge-into-existing, dry-run changes nothing, idempotency). 8 tests; test_migrate.py (11) and test_cli.py (69) still pass.

Merge ordering

Copilot AI review requested due to automatic review settings June 6, 2026 04:44
@igorls
igorls requested a review from milla-jovovich as a code owner June 6, 2026 04:44

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a wing-name normalization migration (migrate-wings) to strip leading and trailing separators from legacy wing names, ensuring that older palaces remain discoverable. The changes include adding a new CLI command, implementing the migration logic in mempalace/migrate.py to update drawer/closet metadata and the topics_by_wing registry, and adding corresponding tests. The review feedback highlights two potential TypeError vulnerabilities: one where metas could be None when iterating collection items, and another where tbw[new] could be None when merging topics by wing.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread mempalace/migrate.py
Comment on lines +431 to +435
ids = batch.ids if hasattr(batch, "ids") else batch["ids"]
metas = batch.metadatas if hasattr(batch, "metadatas") else batch["metadatas"]
if not ids:
break
for rec_id, meta in zip(ids, metas):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

If the backend collection does not have metadata for any of the retrieved records, metas could be None. Zipping ids with None will raise a TypeError. Adding a defensive guard to default metas to a list of None values of the same length as ids prevents potential crashes.

Suggested change
ids = batch.ids if hasattr(batch, "ids") else batch["ids"]
metas = batch.metadatas if hasattr(batch, "metadatas") else batch["metadatas"]
if not ids:
break
for rec_id, meta in zip(ids, metas):
ids = batch.ids if hasattr(batch, "ids") else batch["ids"]
metas = batch.metadatas if hasattr(batch, "metadatas") else batch["metadatas"]
if not ids:
break
if metas is None:
metas = [None] * len(ids)
for rec_id, meta in zip(ids, metas):

Comment thread mempalace/migrate.py
Comment on lines +485 to +487
if new in tbw:
merged = list(tbw[new])
for topic in old_topics:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

If tbw[new] is None (e.g., due to manual editing or corruption in known_entities.json), calling list(tbw[new]) will raise a TypeError. Using tbw[new] or [] provides a safe fallback to prevent potential crashes.

Suggested change
if new in tbw:
merged = list(tbw[new])
for topic in old_topics:
if new in tbw:
merged = list(tbw[new] or [])
for topic in old_topics:

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new one-time mempalace migrate-wings maintenance command intended to normalize legacy wing names so pre-#1675 palaces remain discoverable after wing-name derivation changes.

Changes:

  • Implement wing-name migration planner + executor that rewrites wing metadata in-place for drawers/closets and re-keys topics_by_wing (with collision merging).
  • Add mempalace migrate-wings [--dry-run] [--yes] CLI subcommand.
  • Add a new test suite covering the pure planner and backend integration behavior for relabeling, dry-run, and idempotency.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 6 comments.

File Description
mempalace/migrate.py Adds the wing-name normalization migration implementation (planner, collection iteration, metadata updates, topics registry re-key).
mempalace/cli.py Wires the new migrate-wings subcommand into the CLI dispatcher and argparse.
tests/test_migrate_wings.py Adds planner + integration tests for the new migration behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread mempalace/migrate.py
Comment on lines +392 to +402
if not isinstance(wing, str) or not wing:
return None
# Apply the full normalization and explicitly strip leading/trailing
# separators. The strip is this migration's whole purpose (#1675); doing it
# here rather than relying on normalize_wing_name keeps the migration correct
# even when run against a build whose normalize_wing_name predates #1675, and
# matches the post-#1675 derivation exactly.
target = normalize_wing_name(wing).strip("_")
if not target or target == wing:
return None
return target
Comment thread mempalace/migrate.py
Comment on lines +484 to +492
old_topics = tbw.pop(old) or []
if new in tbw:
merged = list(tbw[new])
for topic in old_topics:
if topic not in merged:
merged.append(topic)
tbw[new] = merged
else:
tbw[new] = old_topics
Comment thread mempalace/migrate.py
Comment on lines +523 to +525
d_items = list(_iter_collection_items(drawers))
all_wings = {(m or {}).get("wing") for _, m in d_items if (m or {}).get("wing")}
d_summary, d_updates = plan_wing_renames(d_items)
Comment thread mempalace/migrate.py
Comment on lines +535 to +536
topic_renames = _plan_topics_by_wing_renames()

Comment on lines +3 to +4
normalize_wing_name strips leading/trailing separators (#1675); palaces built
before that rule filed drawers under the old name (e.g. ``_alpha``).
Comment thread mempalace/migrate.py
Comment on lines +369 to +373
# normalize_wing_name now strips leading/trailing separators, so a path-encoded
# dirname like ``-home-user-proj`` derives ``home_user_proj`` instead of
# ``_home_user_proj``. Palaces built before that rule filed drawers under the
# old, leading-underscore wing, which the new derivation no longer matches —
# searches and diary reads under the new name miss the old memories.
igorls added 2 commits June 6, 2026 03:11
Follow-up to the wing-name normalization (#1675). Palaces built before the
rule filed drawers under leading/trailing-separator wing names (e.g. a
Claude Code path-encoded dir `-home-user-proj` -> `_home_user_proj`); the
new derivation strips those, so searches/diary reads under the new name miss
the old memories — the history is split, not lost.

`migrate_wing_names` (CLI: `mempalace migrate-wings [--dry-run] [--yes]`)
re-keys the `wing` metadata field on drawers and closets to the normalized
form, merging collisions. Design (verified against the codebase):

- Drawer/closet IDs embed the wing as an opaque prefix that is never decoded
  back into a wing, and mining idempotency keys on `source_file`, so IDs are
  left untouched: closet ->drawer_id pointers stay valid and future mining
  still skips already-mined files. No risky extract-rebuild needed.
- `topics_by_wing` registry keys are re-keyed (merging on collision).
- Tunnels resolve via existing read-time normalization and need no rewrite.
- Backend-agnostic (uses the get/update collection abstraction); idempotent;
  dry-run-able. The strip is applied by the migration itself, so it is
  correct regardless of whether the running build's normalize_wing_name
  already carries the #1675 change.

Tests: pure planner coverage (strip, no-op, empty/non-string, collision) +
hermetic backend integration (relabel, merge, dry-run, idempotency).
@igorls
igorls force-pushed the feat/migrate-wing-normalize branch from ee32e56 to 8ec438e Compare June 6, 2026 06:12
@igorls

igorls commented Jun 6, 2026

Copy link
Copy Markdown
Member Author

Rebased onto develop (now includes #1700), so the inherited-red caveat above no longer applies. Local gate on the rebased branch: ruff check + ruff format --check clean, test_migrate_wings.py (8) green. Also added docs/recovery/wing-name-migration.md covering when/how to run migrate-wings.

@igorls
igorls merged commit 708ef4a into develop Jun 6, 2026
8 checks passed
@igorls
igorls deleted the feat/migrate-wing-normalize branch June 6, 2026 06:20
jphein added a commit to techempower-org/mempalace that referenced this pull request Jun 11, 2026
…nd surface

Rebase adaptation onto current develop (348 commits, including the
pluggable-backends follow-ups and wing-normalize MemPalace#1702):

  1. Gate cmd_purge behind _maintenance_requires_chroma("purge"), the
     same guard cmd_migrate / cmd_repair / cmd_repair_status grew for
     pluggable backends. A palace on a non-chroma backend now gets the
     standard "purge is Chroma-only in this release (selected backend:
     ...)" message + SystemExit(2) instead of a misleading "No palace
     found".

  2. Use MempalaceConfig().collection_name instead of hardcoding
     "mempalace_drawers", matching cmd_repair and the configured-
     collection convention develop adopted since this branch forked.

The e2e purge test sets collection_name on the patched config, same
shape as the existing cmd_repair tests. 76/76 in tests/test_cli.py.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants