Friend 后端 Memory Continuity Gauntlet Gates:canonical 记忆流水线的六级连续性验证关卡
【免费下载链接】FriendAI that sees your screen, listens to your conversations and tells you what to do项目地址: https://gitcode.com/GitHub_Trending/fr/Friend
导读
本文系统讲解 Friend(omi)开源仓库后端记忆系统在**记忆连续性(memory continuity)**维度上设立的一整套"关卡(Gauntlet Gates)"验证体系。它回答一个核心工程问题:当 canonical memory 从采集(capture)→ 合并(consolidate)→ 晋升(promote)→ 读取(read)整条流水线被重构、或上线新记忆版本(memory v3)时,如何用可重复、可留痕、可区分的多层测试来证明"记忆不会断、不会漏、不会串"?读完本文,你将掌握六类关卡的各自覆盖范围与边界、它们为何不能互相替代、以及如何在 CI 与本地环境实际运行它们(含全部命令与关键源码依据)。
一、背景:为什么需要"连续性关卡"
Friend 后端的记忆系统(canonical memory)不是单条 SQL 语句,而是一条跨 Firestore、向量库(Typesense)、短期晋升 worker、多种读取 surface 的持久化流水线。任何一环的回归——例如 projection 失败时回退读取了遗留格式记忆(legacy bleed)、晋升绕过了 durable outbox、archive 记忆被默认读取误曝——都会直接破坏用户记忆的连续性与隐私边界。
为此,仓库在 backend/docs/memory-continuity-gates.md(本文关联文档,原先是backend/AGENTS.md的一部分,因体积门槛独立成文)中定义了一套"Memory Continuity Gauntlet Gates"。文档开头特别强调了两条不可混淆的规则:
- 绿色的 live gauntlet 并不证明 hermetic 流水线不变量成立;
- hermetic 测试并不证明部署后端的连续性成立。
也就是说,这套体系刻意由互补而非互替的多个关卡构成。任何一个关卡通过,都不能替代另一个关卡;任何人在宣称"canonical-memory 上线关卡已满足"之前,都必须先读完这份文档。
二、六级关卡总览:覆盖范围与"不覆盖"边界
原文档用一张表格界定了全部关卡,这是整套体系的地图,完整继承如下:
| 关卡 | 覆盖内容 | 不覆盖内容 |
|---|---|---|
| Hermetic pipeline E2E(testing/e2e/test_canonical_memory_pipeline.py) | capture→consolidate→promote→read、archive 排除在默认读取之外、surface 默认访问矩阵、projection fail-closed 且无 legacy bleed | 部署后的 revision 身份、生产 IAM/索引差异、真实 LLM consolidation |
| Listen/Pusher durable finalization gauntlet(testing/listen_pusher_stack) | 真实持久化会话处理转换、fanout lease、MEMORY_ENABLED解析器/围栏、围栏后的 provider leaf、以及 unset-fence 负面路径 | provider 输出质量、生产配置身份、inline 协议场景中被 mock 的记忆路径 |
Gauntlet--self-check | 必需文件、canonical_memory_pipelineworkflow 注册、suite/nonce wiring(在 memory-continuity-gauntlet.py 中) | 任何记忆写入或 HTTP 探测 |
Live gauntlet(memory-continuity-gauntlet.sh,需ADMIN_KEY+ 可达后端) | 在运行中后端上按 suite 执行结构化/v3/memories探测 | Gate 2 完整合成矩阵、Gate 3 生产激活 |
Gate 2 dev-cloud proof(v3_dev_cloud_proof.py+ 已部署分支 revision) | 多用户合成矩阵、索引、IAM、auth、dev-cloud 回滚 | 本地 hermetic fakes;不等于生产激活 |
Gate 3 production proof(按docs/rollout/memory-v3-proof-order.md顺序执行) | Gate 2 GO 之后的生产特有差异 + 独立评审 | 替代 hermetic pipeline E2E 或 gauntlet self-check |
CI 只运行python3 backend/scripts/memory-continuity-gauntlet.py --self-check。Live suites 在凭据/后端不可用时如实记录NOT_RUN——绝不伪造GO。
三、关卡一:Hermetic pipeline E2E——闭环流水线与默认访问矩阵
这是整套体系中最核心的"不变量证明层",位于 backend/testing/e2e/test_canonical_memory_pipeline.py。它通过 fakes(fakes.firestore、fakes.redis、fakes.storage)与FastAPI TestClient在进程内构建隔离后端,跑通真实业务代码路径。
3.1 capture → consolidate → promote → read 闭环
主测试test_capture_consolidate_promote_read_archive_excluded_vectors_and_delete_outbox(L223)按真实时序验证:
- Capture:通过
POST /v1/conversations/{id}/reprocess会话入口(monkeypatch 掉process_conversation后走真实的write_canonical_extraction_memory),写入一条 short-term 记忆,断言tier == "short_term"、status == "active"(L233-L237)。 - Consolidate/Promote:调用真实的
run_canonical_short_term_maintenance(promotion worker 主函数),以脚本化 LLM(_scripted_consolidation_llm)驱动合并决策,断言promoted_count == 1、tier == "long_term"、graph_ready is True且生成了graph_assertion_id(L239-L255)。 - Vector outbox 纪律:晋升后立即检查向量存储,断言
provider_id not in vectors("canonical apply 不得绕过 durable outbox"),随后显式 drain 真实 canonical outbox(_drain_canonical_outbox),断言产生vector_upsertaction、向量以用户隔离的canonical_memory_provider_id出现、metadata 携带memory_layer == "long_term"(L257-L280)。 - Read:
MemoryService.read默认读取能看到晋升后的记忆;archive 条目与"short-term 之外的默认读取"保持正确的可见性集合。 - Delete 与隐私收据:
delete_canonical_memory删除条目后必须留下memory_deletion_receipt.v2收据且收据中不得包含 memory_id 本身(防止隐私元数据残留),向量同步清除(L334-L355)。
3.2 Fail-closed:projection 失败时不得回退泄漏 legacy 记忆
第二个测试test_universal_repository_failure_fails_closed_without_historical_bleed(L357)在 Fake Firestore 中预先种入一条遗留格式记忆(legacy-must-not-bleed),然后让MemoryService.read抛503 infrastructure_failure:断言 API 返回 503、错误 detail 透传、响应体中绝不出现 legacy 记忆内容。这验证了"fail-closed 优先于降级兜底"的设计。
3.3 Surface 默认访问矩阵
TestSurfaceDefaultAccessMatrix(L433)用参数化用例覆盖六种读取 surface:chat_text、chat_list、developer、agent_tools、mcp、product_search(SURFACE_MATRIX_CASES,L423-L430)。每种 surface 都断言:archive 条目被排除、可见条目可检索;同时验证无授权 consumer 时read_decision == DENY_MEMORY(rollout 读门控拒绝)。这是"archive 不参与默认读取"这一产品不变量在全部读取入口上的矩阵式证明。
四、关卡二:Listen/Pusher durable finalization gauntlet——持久化会话处理转换
Hermetic E2E 覆盖的是记忆流水线本身,而记忆的采集源头——listen WebSocket 会话 → pusher → finalization worker 的持久化链路——由独立的 backend/testing/listen_pusher_stack 本地 gauntlet 负责。运行方式(见其 README):
backend/testing/listen_pusher_stack/run.sh --keep4.1 真实栈 + 严格回环
该 harness 启动隔离 Redis、本地 ASGI 进程与 Firestore emulator(自动挑选独立端口),跑的是生产 listen 运行时main:app、真实 pusher router、二进制帧 101/102/103/104/201、真实 Cloud Tasks finalization handler。子进程环境白名单化:私有空HOME、无 provider 凭据、拒绝非 loopback Firestore 端点。
4.2MEMORY_ENABLED围栏与 post-fence leaf
这是该关卡与记忆连续性直接相关的关键设计:
- 强制 Cloud Tasks 路径设置
MEMORY_ENABLED=on,执行生产解析器和围栏(fence),然后证明围栏之后的 provider leaf 真实运行; - 负面场景 unset 该标志,证明 durable job 保持可重试、post-fence leaf 不运行、仅发出无标识符的 bounded
memory_fence诊断。
也就是说:记忆写入在 finalization 链路中被一个显式的开关围栏(fence)守护,开关关闭时宁可让任务挂起重试,也不静默写记忆。Cloud Tasks 入口只替换 provider-side leaf(LLM 摘要生成、MemoryService.ensure_canonical_mutation_ready之后的 canonical memory 工作、外部集成投递等),持久化与内存安全围栏之下全是真实代码。
4.3 十一类场景
README 列出 11 个场景(含 #9960 的 pending-finalization 恢复路径、worker 两attempt 预算耗尽后原子 dead-letter 并标记会话failed/discarded、RECORDING_SESSION_MODE=enforce下操作级故障注入等)。其定位是"确定性本地故障测试",明确不测真实 Parakeet 推理、LLM/向量质量、GCS 与外部集成投递。
五、关卡三与四:Gauntlet--self-check与 Live gauntlet
这两个关卡都由统一的驱动脚本 backend/scripts/memory-continuity-gauntlet.py 实现,shell 包装 backend/scripts/memory-continuity-gauntlet.sh 只负责exec python3透传参数(脚本头部注释里写着全部用法)。
5.1--self-check:CI 的看门狗
python3 backend/scripts/memory-continuity-gauntlet.py --self-checkself_check()(L159)不做任何记忆写入或 HTTP 探测,只做静态结构性校验:
- 必需文件存在:
REQUIRED_SELF_CHECK_PATHS(L31-L37)包括驱动 py/sh、E2E 测试、backend/tests/unit/test_inv_mem_1_guard.py与backend/testing/workflow_contracts.json; - workflow 注册:
canonical_memory_pipelineworkflow 必须存在于workflow_contracts.json且注册testing/e2e/test_canonical_memory_pipeline.py(L186-L192;contract 定义见 backend/testing/workflow_contracts.json); - suite/nonce wiring:驱动源码必须包含六个 suite token(
capture/promote/recall/archive/surfaces/resilience)、三种 nonce kind(CAPTURE/PROMOTE/ARCHIVE)以及--self-check自身标志(L173-L184)。
这正是文档表格中"--self-check只证明文件与 wiring、不证明任何记忆行为"的来源——它是一道成本极低的 CI 闸门,防止有人在删文件/改 suite 名时悄悄破坏整套 gauntlet。
5.2 六个 suite 与 nonce 证据
六类 suite(SUITE_NAMES,L39):capture、promote、recall、archive、surfaces、resilience;别名pipeline(capture→archive)与all(L40-L43)。每次运行生成三种随机 nonce(MEMGAUNTLET-{run_id}-{hex}-{KIND},L54-L57),写入被写记忆的 content 中——后续 suite 用该 nonce 作为检索 token 验证"这条记忆确实经历了 capture→promote→recall",从而把"连续性"变成可断言、可留痕的事实。
Hermetic 模式(默认,无凭据时自动回退)复用 E2E 的 fakes 与 TestClient,跑完每个 suite 的真实业务函数;surfaces在五个 surface(chat_text/chat_list/developer/agent_tools/product_search)上逐一断言 archive nonce 不出现、可见记忆出现;resilience让MemoryService.read抛 503 后断言/v3/memoriesfail-closed 且 legacy 不泄漏(L655-L686)。
5.3 Live gauntlet:运行中后端的结构化探测
# 本地起后端后: ./backend/scripts/memory-continuity-gauntlet.sh --suite capture,promote,recall MEM_GAUNTLET_API_URL=http://127.0.0.1:8080 ./backend/scripts/memory-continuity-gauntlet.sh --live模式决策逻辑(resolve_mode,L353-L359):
--live强制要求 live 模式:live_env_probe(L203-L219)检查ADMIN_KEY存在、GET /health可达;- 未传
--live时,若MEM_GAUNTLET_PREFER_LIVE=1且环境可用则走 live,否则回落 hermetic; MEM_GAUNTLET_FORCE_HERMETIC=1强制 hermetic。
Live suite 只做结构化探测:用Authorization: Bearer {ADMIN_KEY}{uid}请求GET /v3/memories,仅接受200/403/503状态(L688-L695),并摘要响应体(items_count或detail)写入证据。它不做记忆写入——这正是文档说"live gauntlet 覆盖结构探测、不覆盖完整合成矩阵"的含义。当 live 不可用时,suite 记录NOT_RUN,且 manifest 的passed只有在所选 suite 中至少一个达到 GO 且无失败时才为 true(L771-L774),从机制上杜绝"全部 NOT_RUN 却被标绿"。
5.4 证据包(evidence bundle)
每次运行在backend/.harness/memory-continuity-gauntlet/{run_id}/下生成manifest.json,记录 run_id、git SHA、模式与原因、nonce、每 suite 的 status/mode/steps/failures/warnings。通过时创建latest-green符号链接并追加INDEX.md(L88-L103);超过 7 天的非 green 运行目录会被自动清理(PRUNE_ABORTED_BUNDLE_DAYS = 7)。所有证据以时间戳 + git SHA 留痕,保证"谁在哪个 revision 上、以什么模式、断言了什么"全程可追溯。
六、关卡五与六:Gate 2 dev-cloud proof 与 Gate 3 production proof
前四道关卡是工程闸门,后两道是发布决策关卡:
- Gate 2 dev-cloud proof:在 dev-cloud 上以多用户合成矩阵验证完整记忆 v3 语义,覆盖索引、IAM、auth,并具备回滚预案。它证明"这套东西在类生产基础设施上、多用户并发下也成立"——这是本地 hermetic fakes 无法提供的证据;但 dev-cloud 不等于生产激活。
- Gate 3 production proof:仅在 Gate 2 判定 GO 之后,针对生产特有差异(真实流量、真实 IAM/索引、真实配置)做验证,并需要独立评审。它同样不能替代 hermetic pipeline E2E 或 gauntlet self-check——三道关卡各司其职。
七、实操速查与设计原则
7.1 命令速查
| 目的 | 命令 |
|---|---|
| CI 静态看门狗 | python3 backend/scripts/memory-continuity-gauntlet.py --self-check |
| 默认全 suite(hermetic 回退) | ./backend/scripts/memory-continuity-gauntlet.sh |
| 选择 suite | ./backend/scripts/memory-continuity-gauntlet.sh --suite capture,promote,recall |
| 强制 live 探测 | MEM_GAUNTLET_API_URL=http://127.0.0.1:8080 ./backend/scripts/memory-continuity-gauntlet.sh --live |
| 强制 hermetic | MEM_GAUNTLET_FORCE_HERMETIC=1 ./backend/scripts/memory-continuity-gauntlet.sh |
| listen→pusher finalization gauntlet | backend/testing/listen_pusher_stack/run.sh --keep |
| 查看证据 | backend/.harness/memory-continuity-gauntlet/INDEX.md与各 run 的manifest.json |
7.2 核心设计原则
- 关卡互补、绝不互替:green live gauntlet 不证明 hermetic 不变量,hermetic 测试不证明部署连续性,Gate 2 不证明生产激活——这是文档第一原则;
- NOT_RUN 优于假 GO:live 凭据缺失时如实记录
NOT_RUN,且"全部 NOT_RUN"永远不能算通过; - nonce 证据链:随机 nonce 贯穿 capture/promote/archive,把"连续性"变成可检索、可断言、可留痕的客观事实;
- fail-closed 纪律:projection 失败返回 503 而非降级读取 legacy;记忆围栏关闭时任务挂起重试而非静默写入;
- 证据可追溯:每次运行记录 run_id + git SHA + 模式 + 断言步骤,green 运行建立索引,aborted bundle 定期清理。
这套"六关卡 + 两级发布证明"的结构,为依赖长期记忆的产品提供了一个可复制、可留痕、抗回归的质量模型:任何宣称"canonical-memory 上线关卡已满足"的改动,都必须逐关对照本体系举证,缺一不可。
【免费下载链接】FriendAI that sees your screen, listens to your conversations and tells you what to do项目地址: https://gitcode.com/GitHub_Trending/fr/Friend
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考