首页
/ MemPalace 写入路由策略(Write Routing Policy):direct / prefer / require 三档策略与单写者安全机制解析

MemPalace 写入路由策略(Write Routing Policy):direct / prefer / require 三档策略与单写者安全机制解析

2026-09-06 13:33:41作者:申梦珏Efrain

本文基于 MemPalace 仓库中的 docs/write-routing-policy.md 展开,完整梳理其定义的统一写入路由策略:三档策略(direct / prefer / require)的语义差异、direct / daemon / blocked 三种具体路由结果、环境变量与配置文件的完整配置形态及优先级链、向后兼容的遗留布尔映射,以及本地文件型后端的单写者租约保护。读完本文后,你可以准确理解 MemPalace 在 daemon 推广(Tier 3 rollout,追踪于 #1963)过程中如何统一 hook 与 CLI 的写入路径决策,并能对照 mempalace/write_routing.pymempalace/config.py 的源码验证策略解析的每一步行为。

策略的定位:一个被测试覆盖的策略模型

原始文档开宗明义:write routing policy 是 Tier 3 daemon 分级推广(#1963)所使用的共享策略。这份"foundation PR"的核心约束是——它本身不改变现有 hook 或 CLI 的路由行为,只是提供一个经过测试的策略模型,供后续 hook 与 CLI 的推广 PR 直接消费,而无需各自发明不同的 fallback 规则。

这一设计意图在源码中体现得很清楚。mempalace/write_routing.py 的模块 docstring 说明该策略"刻意与传输方式无关(transport-agnostic)":hook 和 CLI 消费方各自负责回答两个事实——"daemon 是否已经可用"以及"我是否被允许启动一个 daemon";策略模块的职责是把这些事实加上一个策略,归约为唯一一条显式路由

三个策略的语义定义如下(与原文档一致):

策略 语义
direct 始终走既有的本地直连路径(direct local path)。
prefer 优先使用可用 daemon;若调用方被允许启动 daemon 则可启动,否则回退到直连路径。
require 优先使用可用 daemon;允许启动的调用方可以启动 daemon;若两者都不可行则阻断操作,绝不回退到直连 ChromaDB 写入者

具体路由结果:direct / daemon / blocked

共享决策函数针对每次写入返回三种具体路由之一:

  • direct
  • daemon
  • blocked

除目标路由外,决策还会报告调用方是否应当自动启动 daemonauto_start_daemon)以及选择该路由的原因reason)。

调用方两类典型取值在文档中有明确约定,且被源码印证:

  • hooks 一般传 daemon_can_start=False——hook 执行有严格的延迟预算,不能冷启动一个长驻进程;
  • 交互式 CLI 命令可以传 daemon_can_start=True

核心决策逻辑实现在 choose_write_route

def choose_write_route(
    policy: WriteRoutingPolicy,
    *,
    daemon_available: bool,
    daemon_can_start: bool,
) -> WriteRoutingDecision:
    """...
    The key safety guarantee is that ``require`` never degrades to a direct
    write when the daemon is unavailable.
    """

其判定顺序是:

  1. 策略为 direct → 直接返回 direct,原因 policy-direct
  2. daemon 已可用 → 返回 daemon,原因 daemon-available(不自动启动);
  3. 允许启动 daemon → 返回 daemonauto_start_daemon=True,原因 daemon-auto-start
  4. 策略为 prefer 且以上皆否 → 回退 direct,原因 daemon-unavailable-fallback
  5. 其余情况(即 require 且 daemon 不可用、又无权启动)→ 返回 blocked,原因 daemon-required-unavailable

决策以冻结 dataclass WriteRoutingDecision 承载,字段为 policy / target / auto_start_daemon / reason,并暴露 use_daemonblocked 两个便捷属性。完整的行为矩阵由参数化测试 test_decision_matrix 固定:

策略 daemon_available daemon_can_start 结果 auto_start
direct 任意 任意 direct False
prefer True 任意 daemon False
prefer False True daemon True
prefer False False direct False
require True 任意 daemon False
require False True daemon True
require False False blocked False

其中"require 永不降级为直连写入"是这份策略最关键的安全保证,也是后文单写者安全机制成立的前提。

配置方式:环境变量与配置文件

环境变量

文档定义了三级环境变量,取值为 direct|prefer|require

# 全局环境策略
MEMPALACE_WRITE_ROUTING=direct|prefer|require

# hook 专属环境策略
MEMPALACE_HOOK_WRITE_ROUTING=direct|prefer|require

# CLI 专属环境策略
MEMPALACE_CLI_WRITE_ROUTING=direct|prefer|require

配置文件形态

配置文件中的 write_routing 对象支持全局默认值与分作用域覆盖:

{
  "write_routing": {
    "default": "direct",
    "hooks": "prefer",
    "cli": "require"
  }
}

优先级链(Precedence)

当多个来源同时配置了策略时,解析器按固定顺序取第一个已配置的值。

对 hooks 作用域,优先级从高到低:

  1. MEMPALACE_HOOK_WRITE_ROUTING
  2. MEMPALACE_WRITE_ROUTING
  3. 遗留变量 MEMPALACE_HOOKS_DAEMON
  4. write_routing.hooks
  5. write_routing.default
  6. 遗留配置 hooks.daemon
  7. 兜底 direct

对 CLI 写入

  1. MEMPALACE_CLI_WRITE_ROUTING
  2. MEMPALACE_WRITE_ROUTING
  3. write_routing.cli
  4. write_routing.default
  5. 兜底 direct

实现位于 MempalaceConfig.resolve_write_routing。其做法是把各来源构造成有序的 RoutingPolicyCandidate 列表——每个候选携带 source 名称、valuelegacy_boolean 标志——再交给 resolve_write_routing_policy 依次尝试:跳过 None,第一个可解析的值胜出,并连同来源名一起返回 ResolvedWriteRoutingPolicy;全部为空时返回默认值 direct(来源标记为 "default")。两个便捷属性 hook_write_routingcli_write_routing 分别封装了两个作用域的解析结果。作用域只允许 hookscli,传入其他名称(例如 "mcp")会直接抛出 WriteRoutingError,这一点由 test_unknown_scope_is_rejected 覆盖。

优先级行为有对应测试逐条钉住,例如:

向后兼容:遗留布尔值的映射

文档声明既有的 MEMPALACE_HOOKS_DAEMON 环境变量与 hooks.daemon 配置项继续受支持,映射规则为:

  • 遗留 true 值(1trueyesondaemon)→ 映射为 prefer
  • 遗留 false 值(0falsenooff)→ 映射为 direct

源码中这两组词表定义在 write_routing.py 的 _LEGACY_TRUE / _LEGACY_FALSE,解析入口 parse_write_routing_policy 只有在 legacy_boolean=True 时才接受这些词;对新的策略设置,布尔值、整数和空串一律拒绝(见 test_new_policy_rejects_legacy_or_invalid_values)。

另一条兼容性承诺是:既有的 MempalaceConfig.hook_use_daemon 属性在本次基础 PR 中被刻意保持不变(见 hook_use_daemon,其逻辑仍只读遗留的 MEMPALACE_HOOKS_DAEMONhooks.daemon),hook 与 CLI 的实际行为要等各自的"policy-aware"推广 PR 落地后才改变。测试 test_existing_hook_use_daemon_behavior_is_unchanged 专门验证了这一点。

非法策略值的处理:响亮失败(fail loudly)

新的策略设置只接受 directpreferrequire 三个值。非法值不会静默回退,而是抛出带来源名的错误——resolve_write_routing_policy 在捕获解析异常时会重新包装为 "{source}: {exc}" 形式抛出。文档给出的理由是:如果一次拼写错误的 require 被静默降级成直连写入,就等于违背了策略本身的安全目的。

相关行为测试:

本地后端的单写者安全

这是原文档篇幅最重的部分,也是理解整个路由策略"为什么要存在"的关键。

背景约束chromasqlite_exact 以及 Milvus Lite 这类文件型后端,每个 palace 只支持一个可写进程。仅仅序列化单次调用是不够的,因为每个长驻进程都会在两次调用之间持有 SQLite/WAL、FTS 或向量索引的状态。

文档列出的规则与源码中的对应实现:

  • 可写 daemon 在整个生命周期内持有 palace 写入租约:daemon 工作进程启动时通过 ExitStack 包住 mine_palace_lock 获得租约,worker 退出后统一 释放
  • 可写 MCP HTTP 在绑定端口前获取该租约,整个服务期持有,活跃请求结束后释放;
  • MCP stdio 在拿到租约前以只读方式打开 sqlite_exact:因此读操作可以与写者共存;而变更类工具(mutating tools)在另一个进程持有租约时会被拒绝(见 _mcp_peer_writer_refusalMEMPALACE_MCP_ALLOW_PEER_WRITER 环境变量定义),租约持有者退出后可以重新以可写方式打开存储;
  • 只读 MCP HTTP 可以与写者共存
  • 只读 sqlite_exact 客户端使用 immutable 连接(针对干净、已 checkpoint 的数据库),或在"活跃写者的完整 WAL sidecar 对必须可见"时使用 mode=ro;两条路径都启用 query_only,并跳过 schema、WAL、FTS、迁移与元数据初始化;
  • 直连 CLI 与 hook 写入不得与可写 daemon 或 MCP HTTP 持有者并存——当 daemon 拥有该 palace 时,应使用 require 把它们路由进 daemon;
  • 直连 sqlite_exact 的 collection 变更会争抢同一份 palace 租约,且全量 LLM closet 重生成会先持有租约再打开 collection、调用配置好的模型(见 repair.py 中的 _rebuild_index_under_lease)。

palace 级锁本身的实现在 mine_palace_lock:锁文件按 sha256(规范化 palace 路径) 命名并存放于 ~/.mempalace/locks/,因此不同 palace 的挖掘可以并行,只有同一 palace 的写入被串行化;锁是非阻塞的(已有持有者时直接抛错而非排队),且对同一进程可重入,允许 miner 管道外层持锁、collection 写入层再获取而不自死锁。文档同时给出了锁的健康含义:并发多份 mempalace mine 会对同一 palace 并行驱动 HNSW 插入,可能损坏 HNSW 图并产生 link_lists 膨胀——这正是"多写者必须被路由掉"的底层原因。

MEMPALACE_MCP_ALLOW_PEER_WRITER 不能绕开上述保护:对本地文件型或未知插件后端它无效;该变量只为显式远程服务型后端(qdrantpgvector 以及 Milvus server/Zilliz Cloud)保留,这些后端能自行协调并发客户端。Milvus Lite 作为本地文件型存储仍然受保护。

运维红线:不要通过删除或 unlink 活跃的 palace 锁来"夺回"所有权。正确做法是干净地停止持有进程,操作系统会自动释放其锁;怀疑数据损坏时,应备份 palace 并在没有任何可写服务运行的前提下离线执行完整性检查/修复。

hook 侧的实际消费:一次事件一个决策

文档的 "Follow-up PRs" 一节指出:hook 触发的写入现在已消费这份策略,详见 docs/hook-write-routing.md。仓库源码展示了消费方式的两个关键工程决策:

1. 每次 hook 事件只解析一次路由。 一个 Stop 或 SessionEnd 事件可能执行多笔写入(diary checkpoint、transcript ingest、project auto-ingest)。mempalace/hooks_cli.pyContextVar_HOOK_WRITE_ROUTING_CONTEXT)加上下文管理器 _hook_write_routing_context 把一次解析与一次 daemon 探活共享给整个写入突发,避免反复健康探测,也防止同一事件内不同写入选中不一致的路由。

2. daemon 探活是快速本地检查,而非冷启动。 _daemon_available 只做 localhost 健康探测(超时上限为 HOOK_PROBE_TIMEOUT),docstring 直接写明"hook 时间预算禁止冷启动一个长驻 daemon"。这与策略层 daemon_can_start=False 的约定一致(见 _compute_hook_write_routing 中对 choose_write_route 的调用)。

hook 侧还有两个值得注意的失败语义,均来自 hook-write-routing.md

  • 显式非法策略值失败关闭(fail closed):hook 写入被阻断,不尝试任何直连 ChromaDB 回退;而配置读取/运行时无关故障则保留历史直连保存行为,避免因一次配置读取失败丢掉最终 checkpoint。HookWriteRouting_compute_hook_write_routingWriteRoutingError 与普通 Exception 的分流处理正对应这两种情形;
  • 提交歧义处理:一旦尝试了 daemon 提交,任何错误都不触发直连回退——daemon 可能已在客户端观察到故障之前接受了任务,直连重试可能造成内容重复。require 模式被阻断时,hook 会在日志中记录跳过、返回可见的 systemMessage(由 _blocked_hook_output 生成),且 Stop save marker 不前进,允许后续重试。

使用 require 的受监督安装必须提前启动 daemon(登录时、插件初始化或会话建立时),例如 mempalace daemon start;SessionStart 在 require 模式下会做快速健康探测并在 daemon 不可用时尽早告警。

尚未纳入路由的操作

文档最后划定了边界,避免读者高估该策略的适用范围:

  • CLI 常规写入:剩余推广 PR 才会把策略应用到常规 CLI 写入(本次 foundation 与 hook PR 均未改变 CLI 行为);
  • 维护类操作(repair、migration、index rebuild):它们不是普通的"被路由写入",需要单独的 exclusive-maintenance 策略,不消费本策略模型。

小结与验证路径

这份策略的价值在于把"直连 / daemon / 阻断"的决策从各调用方头中收敛到一个纯函数里,并让"require 永不降级"这一安全不变量可以被单元测试穷举验证。建议按以下路径在仓库中自行核对:

  1. 策略模型本体:mempalace/write_routing.py(约 210 行,纯标准库实现,无 I/O 依赖);
  2. 配置解析与优先级:mempalace/config.pyresolve_write_routing
  3. hook 消费方式:mempalace/hooks_cli.pydocs/hook-write-routing.md
  4. 行为基线测试:tests/test_write_routing.py(策略解析、遗留映射、优先级、决策矩阵、失败语义全覆盖);
  5. 单写者基础设施:mempalace/palace.pymine_palace_lockmempalace/daemon.py 的租约获取与 mempalace/mcp_server.py 的 peer-writer 保护。

需要说明的适用前提:本策略模型当前主要由 hook 写入路径消费,CLI 侧的常规写入推广仍在后续 PR 中;如果你依赖的是某个具体发行版,应以该版本仓库内 CHANGELOG.md 与文档标注为准,确认 write_routing.hooks / write_routing.cli 是否已被对应代码路径实际读取。

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

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.8 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
531
594
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
916
1.83 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.58 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.36 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.01 K
516
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
547
388