首页
/ mempalace migrate-wings:修复 Wing 名称归一化引发的记忆分裂——数据恢复与实现详解

mempalace migrate-wings:修复 Wing 名称归一化引发的记忆分裂——数据恢复与实现详解

2026-09-06 14:04:42作者:管翌锬

本文围绕 mempalace 的恢复文档 wing-name-migration.md 展开,讲解 mempalace migrate-wings 命令如何把因 wing(翼)名称归一化规则变更而"分裂"的历史记忆重新合并到同一个 wing 下。读完后你能掌握:如何识别"记忆变少、一个项目出现两个 wing"的分裂症状,如何用 dry-run 预览并安全执行迁移,以及该迁移在源码层面改了什么、刻意不改什么,从而在升级后自主完成数据恢复。

一、背景:wing 名称归一化规则与"分裂"从何而来

mempalace 中的 wing 是按项目(通常是路径编码的目录名)划分的记忆分区。wing 名在落库前会经过统一归一化,核心函数是 normalize_wing_name

def normalize_wing_name(name: str) -> str:
    """Lower-case + collapse separators (`-`, ` `) to `_` for wing slugs. ..."""
    return name.lower().replace(" ", "_").replace("-", "_").strip("_")

规则是:小写化,把空格和连字符折叠为下划线,并剥离首尾的分隔符。因此一个路径编码的目录名如 -home-user-proj,归一化后得到的 wing 是 home_user_proj,而不是旧规则下带前导下划线的 _home_user_proj

问题出在升级前后的行为差异上:

  • 升级前挖掘(mine)的宫殿,把抽屉(drawer,即记忆单元)归档在旧的分隔符填充名下,如 _home_user_proj
  • 升级后新的挖掘和日记(diary)写入走新规则,落在 home_user_proj 下。

两个名字不再相遇——历史记忆不是丢失了,而是"分裂"(split)成了两条平行线:搜索新名字只能命中新挖掘的记忆,旧抽屉留在旧 wing 里无人查询。mempalace migrate-wings 就是为消除这种分裂而设计的恢复命令。

为什么 MCP 写入会连带被拒

恢复文档还指出一个连带症状:MCP 向带前导下划线的旧 wing 写入时可能被拒绝。其根源在 sanitize_name,它对 wing/room/entity 名称执行安全字符集校验,而合法字符集由 config.py 中这条正则决定:

MAX_NAME_LENGTH = 128
_SAFE_NAME_RE = re.compile(r"^(?:[^\W_]|[^\W_][\w .'-]{0,126}[^\W_])$")

该正则要求名称首尾字符必须是"非下划线的字字符"[^\W_]),因此 _home_user_proj 这类以 _ 开头的名字会直接抛 ValueError。这正是 normalize_wing_name 剥离首尾分隔符的动机——让归一化结果能通过 MCP 写入工具链的校验(见 config.py 源码注释:leading-underscore slug 会被 sanitize_name 及 MCP write tools 拒绝)。

二、症状识别:如何判断自己的宫殿发生了分裂

按恢复文档 Symptom 一节,升级后出现以下现象即符合分裂特征:

  1. 一个项目的记忆"变少"了:以前能稳定命中的项目记忆,升级后搜索结果明显少于预期;
  2. mempalace status 显示一个项目对应两个 wing:例如同时存在 _home_user_proj(旧抽屉)与 home_user_proj(新挖掘的);
  3. MCP 写入被拒:Agent 经 MCP 向带前导下划线的旧 wing 写记忆时报错,原因即上节的 sanitize_name 校验。

判断是否分裂的关键动作就是用 mempalace status 对比项目目录与 wing 名的对应关系——一个项目出现"新旧两副面孔"即可确认。

三、恢复操作:先 dry-run 预览,再确认应用

3.1 预览(绝不修改任何数据)

恢复文档强调预览优先,--dry-run 模式不产生任何写入:

mempalace migrate-wings --dry-run
mempalace migrate-wings --dry-run --palace /path/to/palace
  • 不带 --palace 时,命令使用默认宫殿路径(取自 MempalaceConfigpalace_path);
  • --palace /path/to/palace 用于指定非默认的特定宫殿目录。

预览会打印一份迁移计划,逐条列出每个改名,并标记将与已有 wing 发生碰撞(MERGE,合并进既有 wing)的条目

  Wing-name migration plan:
    '_home_user_proj' -> 'home_user_proj': 1284 drawer(s), 96 closet(s)  (MERGE into existing wing)

如果 topics_by_wing 实体注册表也有需要改键的条目,还会追加一行 topics_by_wing: N key(s) re-keyed(见 migrate.py 计划输出)。

3.2 应用迁移

mempalace migrate-wings           # prompts for confirmation
mempalace migrate-wings --yes     # no prompt

不带 --yes 时,命令会打印计划并交互式询问 Apply this wing-name migration? [y/N],输入 y/yes 才执行;在 EOF(如管道、CI)场景下会视为放弃并提示 Aborted(见 migrate.py 确认逻辑)。--yes 则跳过确认直接执行,适合脚本化场景。

3.3 参数一览

参数 作用 源码位置
--dry-run 只显示将发生的变更,不做任何修改 cli.py 参数定义
--palace <path> 指定目标宫殿目录,缺省用配置的默认路径 cmd_migrate_wings
--yes 跳过确认提示,直接应用迁移 cli.py 参数定义

CLI 侧的入口是 cmd_migrate_wings,它仅做路径展开后直接委托给核心实现 migrate_wing_names;命令在子命令表中的注册见 cli.py

四、源码剖析:migrate-wings 到底改了什么

核心实现在 migrate_wing_namesmempalace/migrate.py 中有一段专门注释说明该迁移是 #1675 的后续)。它的工作可拆为四步:

4.1 计算目标名:_normalized_wing_target

_normalized_wing_target 对每个 wing 值计算归一化目标:

target = normalize_wing_name(wing).strip("_")
if not target or target == wing:
    return None
return target

三个值得注意的细节:

  • 额外补一刀 .strip("_"):源码注释说明这是刻意设计——即使用户在运行迁移的构建版本里 normalize_wing_name 还是 #1675 之前的旧实现(不做首尾剥离),迁移结果依然正确;
  • 归一化后为空的 wing(如 "_")直接跳过,宁可不动也不能让抽屉"无家可归"(对应测试 test_plan_renames_ignores_empty_nonstring_and_all_separator 的注释 never strand a drawer);
  • 已是规范名的 wing 返回 None,表示无需迁移,这保证了命令的幂等性。

4.2 规划改名:plan_wing_renames

plan_wing_renames 是一个纯函数规划器,对 (id, metadata) 序列产出两份结果:

  • summary{(old_wing, new_wing): count},用于打印计划中的数量统计;
  • updates:仅包含 wing 真正变化的记录 (id, new_metadata),metadata 被整体拷贝后只重写 wing 一个键roomsource_file 等其他字段原样保留(test_migrate_wings.py 中有 by_id["d1"]["room"] == "r" 的断言佐证)。

数据遍历由 _iter_collection_items 完成:按 1000 条一批分页拉取整个集合(col.count() + offset/limit),兼容 typed result 与原始 dict 两种后端返回形态。

4.3 落盘改名:批量 update 元数据

实际写入由 _apply_wing_updates 执行,以 500 条为一批调用后端的 col.update(ids=..., metadatas=...) 原地改写 wing 元数据字段——不改 ID、不动 document 内容、不重算向量。当改名目标已存在同 wing 的记录时,新记录只是与旧记录共享同一个 wing 值,效果上即完成"合并";这正是 dry-run 计划里 (MERGE into existing wing) 标记的来源——migrate.py 中通过判断 new in all_wings(该 wing 名是否已被其他抽屉占用)来决定是否打印该标记。

迁移同时对两个集合操作:

  • drawers 集合:主记忆数据,总是处理;
  • closets 集合(更高阶的归纳记忆):通过 get_closets_collection 获取,处理失败时静默降级(closets = None),不阻断主流程。

4.4 同步 topics_by_wing 注册表

除向量集合外,迁移还会处理实体注册表中的 topics_by_wing 键:

五、它刻意不改什么:ID 与 Tunnels 的处理哲学

恢复文档专门用 "What it leaves alone" 一节划定了迁移边界,源码注释(mempalace/migrate.py)给出了理由:

5.1 Drawer/Closet ID 保持原样

抽屉 ID 形如 drawer_<wing>_<...>,其中的 wing 是一个不透明前缀(opaque prefix)——从源码结构看,仓库中没有任何代码会把 ID 里的 wing 前缀反解析回 wing 名(迁移注释明确写道 "verified: nothing splits a wing out of an ID")。因此不改 ID 带来两个关键好处:

  1. closet → drawer_id 指针继续有效:closet 记忆通过 ID 引用其来源抽屉,ID 不动则指针不失效;
  2. 挖掘幂等性不受破坏:未来的 mining 以 source_file 等字段为幂等键跳过已挖掘文件,ID 前缀差异不会导致重复入库。

同时,drawer 的原始文本内容从不被读取或重写——迁移只碰元数据,向量与正文原样保留。

5.2 Tunnels 无需重写

Tunnels(跨 wing 的主题隧道)在读取路径上就做了归一化:palace_graph.py_normalize_wing 对所有读路径过滤统一走 normalize_wing_name,主题隧道的 key 计算(compute_topic_tunnels 相关逻辑)与 hallways 构建(mempalace/palace_graph.py)也都先归一化再比较。因此旧名字的 tunnel 记录在新名字下依然能正确解析,无需逐条重写。

六、安全特性:幂等、后端无关与测试保障

恢复文档 "Notes" 一节的三条声明都能在代码与测试中找到对应:

  • 幂等(Idempotent):第二次运行时,所有 wing 的归一化目标等于自身,plan_wing_renames 产出空更新,命令打印 All wing names are already normalized -- nothing to migrate. 并直接返回(migrate.py)。test_migrate_wings.pytest_plan_renames_noop_for_clean_wings 固定了这一行为;
  • 后端无关(Backend-agnostic):迁移只依赖后端接口的通用操作(count/get/update)与 palace.py 的集合访问封装,不绑定 ChromaDB 等特定存储,任何已配置的存储后端都可执行;
  • 碰撞语义有测试钉住_gammagamma_ 同时存在时两者都归一到 gammatest_plan_renames_collision_maps_both_to_same_target);集成测试用真实后端集合验证端到端行为,且显式提供 embeddings 保证测试不依赖嵌入模型(test_migrate_wings.py)。

七、适用前提与操作建议

结合恢复文档与实现,最后给出可直接执行的检查清单:

  1. 适用对象:仅在升级跨越了 normalize_wing_name 规则变更(即 #1675 前后)的旧宫殿上有意义。新建的宫殿天生使用归一化 wing 名,永远不需要这条命令——运行一次即可("Run it once per palace after upgrading");
  2. 标准流程:先 mempalace migrate-wings --dry-run 审查计划(重点看 MERGE 标记与数量是否符合预期),确认后 mempalace migrate-wings 交互式应用,或脚本环境用 --yes
  3. 多宫殿场景:用 --palace 逐宫殿执行,每个宫殿各跑一次;
  4. 误判自检:如果 dry-run 输出 All wing names are already normalized -- nothing to migrate.,说明该宫殿不存在分裂,无需任何操作;
  5. 相关恢复文档:同目录下的 index-metadata-recovery.md 讲解索引元数据恢复,可与本文互为参照,覆盖 mempalace 的两类典型升级后数据修复场景。

综上,mempalace migrate-wings 用一个"只改元数据、不动 ID 与内容"的窄口径设计,把归一化规则变更造成的记忆分裂重新缝合:旧抽屉、closet 与 topic 注册表在同一个 wing 下重聚,指针、幂等键与隧道全部保持有效,且整个过程可预览、可确认、可重复执行。

登录后查看全文
热门项目推荐
相关项目推荐