Repository navigation
v2.4.0 — 目录化 + 稳定标识与索引增量化 + 可脚本化与可配置
本版三条主线:目录化(记忆库不再只有扁平一层 —— 可建子目录、写入端还会按语义自动归位)、稳定标识与索引增量化(改名不再静默失联,索引不再每次全量重解析)、可脚本化与可配置(只读工具可出 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。其中三次失败的证伪实验反而推动了设计修正 ——
- 依赖时钟/调度的判据不算判据;
- 同一语义若存在「索引快路径 + 全库兜底」两条实现,反证必须两条一起禁,否则快路径会把禁用效果整体掩盖、产出假绿;
test_*是独立脚本(退出码判据),用-m pytest会No module named pytest且 rc=1 无输出,极易误读成「断言红了」。
Full Changelog: v2.3.0...v2.4.0