Skip to content

v2.4.0 — 目录化 + 稳定标识与索引增量化 + 可脚本化与可配置

Choose a tag to compare

@github-actions github-actions released this 08 Oct 04:21
· 34 commits to master since this release

本版三条主线:目录化(记忆库不再只有扁平一层 —— 可建子目录、写入端还会按语义自动归位)、稳定标识与索引增量化(改名不再静默失联,索引不再每次全量重解析)、可脚本化与可配置(只读工具可出 JSON,8 个运行时阈值外置到 memory.toml 并热重载)。

从 2.3.0 到 2.4.0:工具 23 → 24、测试 17 套 / 594 断言 → 25 套 / 964 断言、运行时依赖零新增。


🆕 新增能力

1. 多级目录:可建子目录,写入端还会自动归位

此前 21 处扫描都是非递归 glob("*.md") —— 把笔记放进子目录就等于脱离检索,这也是 2.2.0 明确否决目录分层的原因。本版把枚举层统一收口后正式支持目录:

  • _iter_md_files() 成为唯一的 .md 枚举入口(os.walk + 保留目录剪枝 + 按「深度 → relpath」确定性排序),21 处 glob 全部替换,只此一处可改枚举行为

  • RESERVED_DIRS(8 项:.archive / .trash / .git / .obsidian / ai-memory / __pycache__ / .ruff_cache / .pytest_cache)内的 .md 不会被当记忆收录

  • memory_write("项目/某条目", ...) 直接写入分类子目录;不含 / 的标题行为与旧版逐字节一致

  • 写入端自动归类:新建且标题不含 / 时,按 11 个语义桶映射到规范目录。四档策略 AI_MEMORY_DIR_POLICY:

    • warn(默认) —— 只给建议、不改落点
    • auto —— 自动归类落盘
    • strict —— 无目录可归时拒绝写入
    • off —— 关闭

    默认取 warn 是刻意的:auto 作默认时 18 套测试有 11 套失败,且两套启发式叠加会让落点对写入方不可预测。

  • 三条边界:只作用于新建(更新绝不搬运,存量根目录条目不受影响);落盘目录不写进 frontmatter 的 title(title 恒为裸叶子名);标题已含 / 时完全尊重写入方

  • 非法路径(保留目录、. / .. 段、盘符、绝对路径、层级 > 3)显式拒绝并说明原因,不做静默消毒

2. permalink 稳定标识 —— 改名不再静默失联

条目此前唯一身份是 title / 文件名 stem,文件一改名(手工重命名、搬迁、第三方同步器回灌),所有指向它的引用就静默失联(表现为「查不到」而非报错)。

  • permalink 在首次写入时冻结为当时的文件名 stem,此后永不覆盖
  • 查找链升级为 permalink → title → stem 三级
  • 指向旧标识的双链不再被误报为死链
  • 存量笔记带前缀 title 被再次写入时自动回正,无需迁移脚本

3. memory_schema_diff —— 结构对账(第 24 个工具)

只读工具,把「写入端承诺写出什么」与「磁盘上实际有什么」对账。契约来源就是代码(yaml_io.build_frontmatter 是唯一写出源),不另立 schema 文件(两份真相必然分叉)。

键分三层:core(title/tags/created/updated/tier/access_count,缺 = 真实缺陷)/ v2(type/confidence/verified/scope/status/schema_version,v1 缺属预期、声明 v2 却缺 = 写入端 bug)/ opt(按需写出)。

输出六节:核心键缺失 / v2 缺键 / v1 存量迁移清单 / 未知键(附行号)/ 可选键使用率 / 无 frontmatter 文件(单列一类且不计入缺陷数 —— README 这类普通 Markdown 本就不受结构契约约束)。

4. memory.toml —— 运行时阈值外置 + 热重载

把原先散落在各模块的 8 个阈值收口到 vault 根的可选 memory.toml:

[thresholds]
dup_title_jaccard   = 0.6
dup_body_jaccard    = 0.5
body_truncate_chars = 8000
lock_timeout        = 15
heat_cold_max       = 0.1
heat_demote_hot_max = 0.5
heat_hot_min        = 3.0
heat_decay_lambda   = 0.05
  • 优先级:环境变量 AI_MEMORY_CFG_<KEY> > [thresholds] 区 > 顶层同名键 > 内置默认
  • 默认值与改动前的硬编码值逐项相同 ⇒ 不建文件行为零变化
  • 改动后 2 秒内热重载,无需重启客户端
  • 配置写错时报错并给出行号与原文,不静默取默认值(静默退回会让「我明明改了却不生效」极难排查)
  • memory_stats 新增「运行时阈值配置」小节:显示配置文件路径、8 个生效值(覆写项标 ← 已覆写)与最近一次解析错误

为什么不用 tomllib:它是 3.11+ 才进标准库,而项目仍需支持 Python 3.10。改为自写最小 TOML 子集解析器(与既有手写 YAML 解析同一条路线),对数组 / 内联表 / 多行字符串等不支持语法一律显式报错。

5. 四个只读工具支持 output_format="json"

memory_read / memory_list / memory_recent / memory_orphans 可传 output_format="json",脚本化调用不必再用正则扒字段。

  • 范围即契约:报告型工具(stats / doctor / audit / schema_diff / context / graph)刻意不给 —— 它们的产出是「分节标题 + 计数说明 + 提示语」的叙事,整体转 JSON 等于另立一套真相;写工具的返回值是操作回执,结构化无意义
  • 非法取值报错而不静默回落 text:脚本化调用最怕「以为拿到 JSON、实际拿到一句中文」
  • JSON 的 body 不含中文截断说明(改由 truncated / total_chars 表达),脚本无需再正则剥中文;截断说明仍逐字节保留在文本形态里

6. 模块分层守卫(开发向)

原计划用 import-linter,但它要求声明 contracts 与打包元数据,而这个项目刻意零配置、零构建步骤。改为手写 AST 守卫(纯标准库):

叶子模块(locks / memory_config / memory_index / metrics / text_utils / yaml_io)不得 import server(含函数体内延迟 import 与 importlib 动态拉起);本地模块之间无环;叶子间依赖只能在显式白名单内且白名单不得腐烂;新增模块必须登记进清单。


⚠️ 行为变更

  • 自动生成的 MOC 索引改名为隐藏文件 .记忆索引.md:生成物混在用户笔记里(既污染 memory_list、又会被当成「一篇记忆」参与引用与检索)是长期噪声源。自带一次性保守迁移 —— 仅当旧文件的 title 恰为 记忆索引 且 tags 命中 moc / index / navigation 才判定为「确认的生成物」并删除,疑似人工文件一律跳过并告警。
  • 索引增量化:_iter_entries() 改为两层缓存(整库指纹回放 + 逐文件按 (size, mtime_ns) 只重解析变化者),_refresh_index() 增加内容签名短路。判据是「改 1 个文件只新增 1 次解析」(旧实现为 +全库)。⚠️ 刻意不做「桶级增量渲染」 —— 瓶颈在解析不在渲染。
  • memory_orphans 的反链判定改用标识映射:正文里的 [[X]] 有三种合法写法(title / 文件名 stem / permalink),原实现直接拿 target 当 key,导致「title ≠ stem」的条目全被算成孤立;现统一经映射解析。另:生成页的链接只是全库罗列而非真实引用,把它算作引用方会让孤立数恒为 0、工具直接失去意义,现予排除。
  • 安装脚本原先用 shell glob 复制模板文件,点文件会被漏掉 ⇒ 改为包含点文件的方式(.记忆索引.md 因此能正确安装)。

🐛 修复

  • 条目缓存按 title 为键导致同名文件互相污染元数据:缓存键改为 relpath,并拆出条目级缓存与 (size, mtime_ns) 指纹表。此前同名文件会让后读到的元数据覆盖前一个,且只有重启进程才能净化。
  • cleanup_residue.py 的「活索引可 open」与「真实库对账」两项判据从未生效(随本版同批修正):脚本引用了 sqlite3 却没有 import ⇒ 两项最关键的验收判据恒报失败,属永远红的假判据 —— 比「永远绿的假判据」更隐蔽,因为它看起来像环境问题而被习惯性忽略。

📦 升级说明

  • 向后兼容:不建 memory.toml、不改环境变量时,行为与 2.3.0 逐项一致(8 个阈值默认值未变、AI_MEMORY_DIR_POLICY 默认 warn 不改落点)。
  • 自动迁移:新增 permalink 列与索引表结构变更由 _migrate() 自动完成(ALTER TABLE,幂等,二次连接不重复执行);存量笔记无需手工处理。
  • 无需数据迁移脚本;存量笔记不改名、不搬迁。
  • 依赖不变:requirements.txt 仍为 mcp>=1.27,<2。

🧪 工程化

  • 测试资产:17 套 / 594 断言 → 25 套 / 964 断言
  • 本批新增:test_folders.py(43) / test_title_path.py(42) / test_dir_policy.py(71) / test_index_cache.py(17) / test_permalink.py(25) / test_schema_diff.py(32) / test_memory_config.py(79) / test_hidden_index.py(33) / test_output_json.py(76) / test_import_layers.py(35)
  • 每项新增判据都做了可证伪验证(把产品改回旧行为,断言必须变红):B5 8/8 根针、B6 7/7、C1 7/7、A2 8/8。其中三次失败的证伪实验反而推动了设计修正 ——
    1. 依赖时钟/调度的判据不算判据;
    2. 同一语义若存在「索引快路径 + 全库兜底」两条实现,反证必须两条一起禁,否则快路径会把禁用效果整体掩盖、产出假绿;
    3. test_* 是独立脚本(退出码判据),用 -m pytest 会 No module named pytest 且 rc=1 无输出,极易误读成「断言红了」。

Full Changelog: v2.3.0...v2.4.0