Record that the memory tiers passed independent review on round five

第 5 轮 PASS,无 D45。评审自己起两边服务复现了 D42 的判别子(`4c6af87` 的
`/api/memory/working` 404、HEAD 200),复现了 D43 的三个主语与 D44 的四对,
并用实验支持了「不用未改名主语兜底比对」:主语不等时退回比值键,
`Room 3/Room 4` 立刻变回 `('holds','22','24')`,即 D37 的误报复活。

两条精度说明采纳:P-e 把 D44 的成因写成一般情形——改名后主语键各自吃进自己
的数字,两侧都改名也永不相等(我另测 `温度 22 度 湿度 40` + `温度 24 度 湿度 40`
→ 0 条);P-f 把「指纹补上 8799 那一口」限定为旧构建那一口,同树残留服务仍
只有 busy-port 守卫能挡。

门禁:CHECKLIST -only,改后 `pytest -q` 与 `ruff check` 仍绿(数字见记录行)。
This commit is contained in:
ZacharyZhang-NY committed 2026-10-09 13:23:21 -04:00
1 parent 3d131e6a7e
commit 209f2d0a1c
1 file changed
+3 -2
+3 -2
View File
@@ -409,7 +409,7 @@
- [x] p2-3.1 工作记忆层:`working <holder> <session>` 按记录顺序返回该 session 记下的记忆,不经检索;HTTP `GET /api/memory/working` 与 MCP `working_set` 走同一份实现(Go 只转发,写规则仍在一处)。空 session 直接拒:`a working set needs a session; without one every memory is working memory`。验收:`pytest backend/tests/test_memory_tiers.py backend/tests/test_memory_cli.py backend/tests/test_memory_api.py`(13 + 14 + 8,合跑 35 passed)+ `bash scripts/mcp_write_smoke.sh` 里跨进程那一段(session 名不合并两个 holder;陌生 session 返回空而不是别人的记忆)。提交 `dc9cfb5`。
- [x] p2-3.2 巩固台账:`consolidations` 表按 `(holder, scenario)` 存 watermark + rollup atom id,一条 lane 只有一份常驻摘要、原地替换;`lane_atoms` 用严格 `ingested_at > watermark`,所以 rollup 自己不会被再算一次。**实测出一个真缺陷**:`watermark_at` 原来写成本轮时钟,用落后于记忆的时钟重跑会把同一批算两遍(第二次 `atom_count=4`),现改成 `max(atom.ingested_at for atom in batch)`。验收:`test_a_lane_rolls_up_once_and_keeps_rolling_from_where_it_stopped`(第二次返回 None,第三次 `atom_count=3` 且 `rollup_atom_id` 不变)。
- [x] p2-3.3 时间/价值字段:schema v10 给 atom 加 `session_id` / `importance` / `recall_count` / `last_recalled_at` / `archived_at`;`v_atoms_current` 加 `archived_at IS NULL`,且 `apply()` 先 `DROP VIEW v_atoms_current`——`CREATE VIEW IF NOT EXISTS` 会留着旧视图体、把归档当没看见,升级测试钉住这点。**`decay` 没有落成字段**:没测过的衰减公式就是编故事,所以归档规则把条件写在命令里(`--older-than-days`、`--max-importance`、`recall_count = 0`),`importance` 是人声明的、`recall_count` 是 store 自己观察的。`importance` **未设也算不值钱**(SQL 是 `importance IS NULL OR importance <= ?`),rule 字符串早先只写 `importance at or under 0.5`、漏了 NULL,被评审判为 D30,现写 `importance unset or at or under 0.5`。验收:`test_a_search_counts_the_memory_it_handed_back`、`test_a_memory_a_search_already_returned_is_not_stale`、`test_an_archived_memory_stops_answering_without_leaving_the_store`、`test_the_archive_rule_states_its_own_terms_and_can_be_read_before_it_acts`(dry-run 先点名,`atom_count` 不减)、`test_a_restore_counts_the_memories_it_brought_back_and_only_those`(`restore_atoms` 加了 `archived_at IS NOT NULL`:一个从未归档的 id 早先打印 `{"restored": 1}` 却什么都没变)。
- [x] p2-3.4 冲突记录持久化:`conflicts` 表存(旧, 新, 共享标签, 两个值, 是否已裁决),只在同 holder 同 scenario 且**数字**变了时记录;纯措辞分歧不记(换句话就会每次触发)。数字按「主语 + 最近标签」双键归一(实测 `numeric_values('恒温器放在书房,设定 22 度')` 交出的值键是 `恒温` 与 `设定`,不是 `书房`——评审第 2 轮 D38 纠正的就是这里)。**冲突必须两侧主语相同**(`trust.quantity_conflict`),而且**写在主语上的裸数字是名字、不是量**(`_subject_and_rename_slot`)。评审第 1 轮实测出 D31:只共享谓语的不同事实早先被记成冲突(`客厅的恒温器调到 22 度` + `卧室的恒温器调到 24 度` → `调到` 22→24;`她买了 3 本书` + `他买了 5 本书` → `买了` 3→5),后果是 `compile("客厅的恒温器现在几度")` 的 `packed_text` 只剩卧室那条——问客厅拿到卧室的数字。第 2 轮又实测出 D37,方向相反:主语相同、但那个数字其实在给对象编号,也被当成一个量(`Room 3 holds 22 degrees` + `Room 4 holds 24 degrees` → `room` 3→4,搜 "degrees" 只剩 Room 4;`The router 1 forwards 220 packets` + `The router 2 forwards 240 packets` → `router` 1→2;`2026 saw 3 releases` + `2027 saw 5 releases` → `saw` 2026→2027)。现在的规则:紧挨在主语**之后**的裸数字并入主语键、不当值(改名槽就是主语 token 的下一个,`trust.py:275-276`;`room3` vs `room4`、`router1` vs `router2` → 各 0 条);开头就是数字时那个数字自己成为主语、不向前融合(`2026 saw …` 走 `trust.py:270-272` 的首数字分支,实测 `3 room holds 22 degrees` 主语是 `3` 而不是 `3room`;`2026` vs `2027` 因此也各 0 条),而**同一个主语**下数字变了仍是冲突(`The router forwards 220/240 packets` → 报 `router`,因为主语自己带着值;`2026 saw 3 releases` + `2026 saw 5 releases` → `saw` 3→5,早先读出 0 条,属漏报)。数字只要站在某个词之后、且不在紧挨主语那个位置,就是量不是名字(`The rent is 2200 dollars` + `The rent is 2400 dollars` → `rent` 2200→2400;`恒温器 is 22 degrees` + `恒温器 is 24 degrees` → `恒温` 22→24),CJK 串里融合的数字也仍是量(`温度22度` + `温度24度` → `温度` 22→24)。**但写在主语上的裸数字只有后面还留着别的数字时才是名字**(`_numbers_follow`):句子里再没有第二个数字时,那个数字就是这条记忆要报的量——`温度 22 度` + `温度 24 度`、`室温`、`设定`、`预算 5000 元` + `预算 8000 元`、`The thermostat 22 degrees` + `The thermostat 24 degrees` 各 1 条(评审第 3 轮 D40:早先两字主语后的数字被并进主语、`values` 空,整条 lane 一条都不记,三字主语 `恒温器 22/24 度` 却记 1 条——主语长度决定记不记,当时没写)。主语落在虚词后面要扫到第一个实词(`The router …` 的主语是 `router` 不是 `the`),扫不到主语就不记(fail-closed)。明写的限制(每条都有测试钉住):store 比的是标签不是语义,同主语量不同东西仍算一个量——`恒温器设定 22 度` 与 `恒温器功率 100 瓦` 记一条冲突,由人裁决(`test_the_same_subject_measuring_two_things_still_reads_as_one_quantity`);给主语加限定词会被当成另一个东西,这是**漏报**(`恒温器调到 22 度` 然后 `客厅的恒温器调到 24 度` → 0 条,主语从 `恒温` 变成 `客厅`;两条都留着、什么都没藏,只是这次更新没被当成不一致,人仍可 `supersede`);只有**数字**能给主语改名,字母不行、英文限定词也不行,这是中英不对称的**误报**(`Room A holds 22 degrees` + `Room B holds 24 degrees` → 1 条;`The router model X forwards 220 packets` + `The router forwards 240 packets` → 1 条,而中文 `三楼的恒温器` / `四楼的恒温器` 会分开成 0 条);主语后面没有第二个数字时,编号会被当成量——`Room 3 is big` + `Room 4 is big` → 1 条(`room` 3→4),标签分不出编号和读数,这是误报方向、交人裁决(`test_an_identifier_with_no_reading_after_it_reads_as_a_quantity`);值键仍会把主语之后的第二个数字挂到最近的标签上(`Room 3 holds 22 degrees at 5 pm` 的 `degrees` 键拿到 `5`,`Room 3 holds 22 degrees in 2026` 的 `degrees` 键拿到年份 `2026`——第二个数字,含年份,都会这样挂上去),本轮比对先看主语键所以没暴露后果(评审精度注 P5);`_numbers_follow` 的**漏报**侧:改不改名取决于每条记忆自己后面还留着没有第二个数字,所以同一主语在两条记忆里可能一条被改名、另一条没被改名,两侧键永不相等——`温度 22 度` + `温度 24 度 湿度 40`、`温度 22 Line truncated
- [x] p2-3.4 冲突记录持久化:`conflicts` 表存(旧, 新, 共享标签, 两个值, 是否已裁决),只在同 holder 同 scenario 且**数字**变了时记录;纯措辞分歧不记(换句话就会每次触发)。数字按「主语 + 最近标签」双键归一(实测 `numeric_values('恒温器放在书房,设定 22 度')` 交出的值键是 `恒温` 与 `设定`,不是 `书房`——评审第 2 轮 D38 纠正的就是这里)。**冲突必须两侧主语相同**(`trust.quantity_conflict`),而且**写在主语上的裸数字是名字、不是量**(`_subject_and_rename_slot`)。评审第 1 轮实测出 D31:只共享谓语的不同事实早先被记成冲突(`客厅的恒温器调到 22 度` + `卧室的恒温器调到 24 度` → `调到` 22→24;`她买了 3 本书` + `他买了 5 本书` → `买了` 3→5),后果是 `compile("客厅的恒温器现在几度")` 的 `packed_text` 只剩卧室那条——问客厅拿到卧室的数字。第 2 轮又实测出 D37,方向相反:主语相同、但那个数字其实在给对象编号,也被当成一个量(`Room 3 holds 22 degrees` + `Room 4 holds 24 degrees` → `room` 3→4,搜 "degrees" 只剩 Room 4;`The router 1 forwards 220 packets` + `The router 2 forwards 240 packets` → `router` 1→2;`2026 saw 3 releases` + `2027 saw 5 releases` → `saw` 2026→2027)。现在的规则:紧挨在主语**之后**的裸数字并入主语键、不当值(改名槽就是主语 token 的下一个,`trust.py:275-276`;`room3` vs `room4`、`router1` vs `router2` → 各 0 条);开头就是数字时那个数字自己成为主语、不向前融合(`2026 saw …` 走 `trust.py:270-272` 的首数字分支,实测 `3 room holds 22 degrees` 主语是 `3` 而不是 `3room`;`2026` vs `2027` 因此也各 0 条),而**同一个主语**下数字变了仍是冲突(`The router forwards 220/240 packets` → 报 `router`,因为主语自己带着值;`2026 saw 3 releases` + `2026 saw 5 releases` → `saw` 3→5,早先读出 0 条,属漏报)。数字只要站在某个词之后、且不在紧挨主语那个位置,就是量不是名字(`The rent is 2200 dollars` + `The rent is 2400 dollars` → `rent` 2200→2400;`恒温器 is 22 degrees` + `恒温器 is 24 degrees` → `恒温` 22→24),CJK 串里融合的数字也仍是量(`温度22度` + `温度24度` → `温度` 22→24)。**但写在主语上的裸数字只有后面还留着别的数字时才是名字**(`_numbers_follow`):句子里再没有第二个数字时,那个数字就是这条记忆要报的量——`温度 22 度` + `温度 24 度`、`室温`、`设定`、`预算 5000 元` + `预算 8000 元`、`The thermostat 22 degrees` + `The thermostat 24 degrees` 各 1 条(评审第 3 轮 D40:早先两字主语后的数字被并进主语、`values` 空,整条 lane 一条都不记,三字主语 `恒温器 22/24 度` 却记 1 条——主语长度决定记不记,当时没写)。主语落在虚词后面要扫到第一个实词(`The router …` 的主语是 `router` 不是 `the`),扫不到主语就不记(fail-closed)。明写的限制(每条都有测试钉住):store 比的是标签不是语义,同主语量不同东西仍算一个量——`恒温器设定 22 度` 与 `恒温器功率 100 瓦` 记一条冲突,由人裁决(`test_the_same_subject_measuring_two_things_still_reads_as_one_quantity`);给主语加限定词会被当成另一个东西,这是**漏报**(`恒温器调到 22 度` 然后 `客厅的恒温器调到 24 度` → 0 条,主语从 `恒温` 变成 `客厅`;两条都留着、什么都没藏,只是这次更新没被当成不一致,人仍可 `supersede`);只有**数字**能给主语改名,字母不行、英文限定词也不行,这是中英不对称的**误报**(`Room A holds 22 degrees` + `Room B holds 24 degrees` → 1 条;`The router model X forwards 220 packets` + `The router forwards 240 packets` → 1 条,而中文 `三楼的恒温器` / `四楼的恒温器` 会分开成 0 条);主语后面没有第二个数字时,编号会被当成量——`Room 3 is big` + `Room 4 is big` → 1 条(`room` 3→4),标签分不出编号和读数,这是误报方向、交人裁决(`test_an_identifier_with_no_reading_after_it_reads_as_a_quantity`);值键仍会把主语之后的第二个数字挂到最近的标签上(`Room 3 holds 22 degrees at 5 pm` 的 `degrees` 键拿到 `5`,`Room 3 holds 22 degrees in 2026` 的 `degrees` 键拿到年份 `2026`——第二个数字,含年份,都会这样挂上去),本轮比对先看主语键所以没暴露后果(评审精度注 P5);`_numbers_follow` 的**漏报**侧:改不改名取决于每条记忆自己后面还留着没有第二个数字,所以改名一旦发生,主语键就吃进那条记忆自己的数字,**两侧只要都改名就永不相等**(`温度 22 度 湿度 40` + `温度 24 度 湿度 4Line truncated
- 验收(原文两条):
- LongMemEval 风格两类**有分数**:`bench/reports/LEGS.md` 的 memory 表把 150 条可答 conversation 查询**整集**测了(不是 15 条抽样),shipped 配置下 knowledge-update **1.000**、temporal-reasoning **0.879**、multi-session 0.857、single-session-preference 0.800、single-session-user 0.960;开 dense 腿后 temporal-reasoning **0.950**、multi-session 0.920、preference 0.960。报告另有一列「最新的那条是否第一个回来」:words-only 0.52 / 0.48 / 0.37,dense 0.40 / 0.46 / 0.47——recall 满分不等于排序把被替换的旧值让开了位。
- 被 supersede 的旧事实不当作现行:`v_atoms_current` 过滤 `valid_to`(`backend/tests/test_memory_schema.py:163 test_v_atoms_current_hides_closed_retired_and_unbegun_memories`),Go store 读的也是这个视图(p0-2 时实测跨语言断过),smoke 里 supersede 之后旧 atom 不再出现在 `search_atoms` 结果里。
@@ -418,7 +418,8 @@
- **评审第 1 轮 FAIL**(同一独立 pi,8 项全跑,~2 h / 462 工具轮):D30 rule 字符串漏 NULL、D31 标签粒度把不同事实当冲突(真行为缺陷,见 p2-3.4)、D32 没写同页条件、D33 「前后逐字相同」无产物、D34 引了未提交的 p50、D35 引了不存在的测试名——六条已全部按实测改掉,并新增 8 条测试钉住边界(两间房、两个人、一台路由器、同主语不同量、无主语、主语加了限定词、首数字当值、restore 只数真正归档过的)。该轮同时实测确认:全部门禁数字与本报告一致、两个反向实验复现、归档 dry-run 零副作用(sqlite `mtime_ns` 前后相同)、22→24→26 链产生 3 条冲突、跨 holder/跨 scenario 各 0 条、`supersede` 不记冲突、裁决后两侧回且 `valid_to` 仍 None、harness blob 三处全同、json 与 markdown 四表逐字一致、它自己的 pre-p2-3 对照复现了每个 recall 格子。
- **评审第 2 轮 FAIL**(同一独立 pi,8 项全跑,D30–D35 的修法全部实测成立):四条——D36 纪律:本轮新增两处整行 `#` 连续两行(`trust.py:234-235`、`:270-271`),并且它更正了自己第 1 轮的口径:按本仓 p1-6.4 那条规则(tokenize、任意缩进)`9cb46c1` 是 0 块、`deabbf7` 是 4 块(`memory.py:325`、`migrations.py:318`、`test_memory_schema.py:287`、`bench/leg_check.py:326`)、`3daface` 是 6 块,六处全部收成一行;D37 记录与行为不符且**方向与我写的相反**(见上面 p2-3.4 那条:误报同样会把一条记忆从答案页上摘掉,我原先只写了漏报一侧);D38 「最近标签是 `书房`」实测是 `设定`;D39 三处措辞错(「两个方呰」「然候」「主语自己变了就点主语」——代码条件是主语**自己带着**那个值)。修法不是加披露而是换机制:写在主语上的裸数字是名字不是量。评审另给两条精度说明(不算缺陷):那条 worktree 对照直接跑会死在 `123 store sources are not corpus files`,要在主仓里跑并 `PYTHONPATH=<worktree>/backend/src`;「shipped 臂」是我起的标签,产物里的名字是 `arms[1]`「words only (the configured 24)」(来自 `config.py:43 retrieval_top_k: int = 24`)。
- **评审第 3 轮 FAIL**(同一独立 pi,8 项全跑,~534 工具轮;D36–D39 的修法全部实测成立,16 对冲突逐对复现):两条——D40 机制盲区:主语是两字 CJK 词或单个拉丁词时,紧跟的裸数字被并进主语、`values` 空,`_note_conflicts` 直接返回,整条 lane 一条不记(`温度 22/24 度`、`室温`、`设定`、`预算 5000/8000 元`、`The thermostat 22/24 degrees` 全部 0 条,HTTP 上也是 `[]`),而三字主语 `恒温器 22/24 度` 记 1 条——**主语长度决定记不记**,且 `:412` 那句「数字只要站在某个词之后就是量不是名字」被 `温度 22 度` 直接推翻;D41 归因错:我写「一条记忆只有数字没有词时没有主语可比」,实测 `numeric_values('22 度').subject == '22'`,0 条是因为两侧主语不同。修法换成机制(`_numbers_follow`)而不是加披露,实测 33 对里只动 6 对(五条漏报变记上,加一条新披露边界 `Room 3 is big` + `Room 4 is big` → 1 条),其余逐字不变。该轮同时实测确认:门禁全绿、五个攻击(标点隔开、数字开头、融合写法、一句多数字、反向实验)全部量过,`Room 3 holds 22 degrees at 5 pm` 报 `room3` 22→24 是真读数变化、但 `degrees` 键拿到了 `5`(P5,本轮无害);反向实验删掉改名那四行恰好让两条测试变红(我预测的第三条不变红,因为「数字开头当主语」住在 `_subject_and_rename_slot` 第一个分支 `trust.py:254`);抑制只在页面(搜只给新侧,`GET /api/atoms` 两侧都在且 `valid_to` 仍 None)。另有四条精度说明:P1 smoke 要加指纹断言,否则 8799 上的旧服务能替这棵树答题(它自己就撞上一次:答题体没有 p2-3 字段)——第 3 轮加在 `scripts/mcp_write_smoke.sh` 的 `recall_count` 断言被第 4 轮判为**空判别子**(D42):`ely-eye-mcp/internal/mcpserver/write_tools.go:61-70` 把 HTTP 答复解进强类型 `domain.Memory` 再原样发出,旧服务缺的字段解成 `0` 又被 Go 侧重新发出,所以旧 Python 服务照样通过(评审用 `4c6af87` 的服务 + HEAD 编的 MCP 二进制实测,答复里是 `"recall_count":0`)。指纹现在打在 **Python 答复**上:就绪后先探 `GET /api/memory/working`,实测 `4c6af87` 返 404、HEAD 返 200(两边我都自己起服务量过),旧服务替这棵树答题会当场 FAIL;MCP 侧那条断言留着,但文案改成它真正钉住的东西——脚本自己现编的那个 Go 二进制是不是这份树编的;P2 「整行」口径(已写进 p1-6.4);P3 本报告里门禁行与第 1 轮记录行各重复一份(已去重);P4 `LEGS.md` 的 stamp(`a52c104`,harness blob `a1e9842d`)比改名后的 HEAD(blob `977d19eb`)旧——实测 `git diff a52c104:bench/leg_check.py HEAD:bench/leg_check.py` 只有一行注释被压成一行、零代码变动,`bench/leg_report.py` 两处 blob 全同,所以报告里的数字就是这份代码跑出来的,不必重测(重测只会把延迟换成另一个机器负载下的值)。
- **评审第 4 轮 FAIL**(同一独立 pi,7 项全跑,~557 工具轮;D40/D41 的修法实测成立,16 对逐字复现、五对 D40 全记上):三条——D42 就是上面 P1 那句被实验推翻(Go 层重发缺失字段,旧服务照样过);D43 `:412` 的机制句**方向写反**,我写「主语之前的裸数字并入主语键」,而代码的改名槽是 `index + 1`(主语 token 的**下一个**,`trust.py:275-276`),实测 `3 room holds 22 degrees` 主语是 `3`、不会向前融合,三个例子里只有 `2026 saw …` 数字在前、走的是 `trust.py:270-272` 首数字分支;D44 `_numbers_follow` 的**漏报侧没披露**:改不改名取决于每条记忆自己后面还留着没有第二个数字,`温度 22 度` + `温度 24 度 湿度 40`(`温度`/`温度24`)、`温度 22 度 湿度 40` + `温度 24 度`、`温度 22 度 湿度 40` + `温度 24 度 湿度 60`(`温度22`/`温度24`)、`预算 5000 元 超了 8000` + `预算 6000 元`(`预算5000`/`预算`)四对全 0 条,方向 fail-closed(两条都留在页上)。D44 按评审建议只**披露 + 加测试**(`test_a_second_number_in_one_memory_hides_the_update`),不用「未改名主语兜底比对」,那会把 `Room 3 holds 22 degrees` + `Room 4 holds 24 degrees` 退回 D37 的误报。四条精度说明全部采纳:P-a `degrees` 键也吞年份(`in 2026` → `{'degrees':'2026'}`),P5 那句已改成「第二个数字,含年份」;P-b 反向实验的红名单是 4 条不是 5 条(全套 4 failed / 379 passed,无连带);P-c 「数字只要站在某个词之后就是量不是名字」已就地加限定词(且不在紧挨主语那个位置);P-d 8799 守卫仍只在开跑时探一次,现在指纹补上了这一口。该轮同时实测确认:门禁全绿且 mypy 两边逐字相同、tokenize 0 块、最大文件 `bench/systems.py` 498 行;D41 的新测试文案与实测一致;P4 的 stamp 判断站得住(`bench/leg_report.py` 两处 blob 全同 `40596d29…`)。判定:p2-3.1/3.2/3.3 与两条验收都能 `[x]`,p2-3.4 因 D43/D44 的记录级缺陷暂缓。
- **评审第 4 轮 FAIL**(同一独立 pi,7 项全跑,~557 工具轮;D40/D41 的修法实测成立,16 对逐字复现、五对 D40 全记上):三条——D42 就是上面 P1 那句被实验推翻(Go 层重发缺失字段,旧服务照样过);D43 `:412` 的机制句**方向写反**,我写「主语之前的裸数字并入主语键」,而代码的改名槽是 `index + 1`(主语 token 的**下一个**,`trust.py:275-276`),实测 `3 room holds 22 degrees` 主语是 `3`、不会向前融合,三个例子里只有 `2026 saw …` 数字在前、走的是 `trust.py:270-272` 首数字分支;D44 `_numbers_follow` 的**漏报侧没披露**:改不改名取决于每条记忆自己后面还留着没有第二个数字,`温度 22 度` + `温度 24 度 湿度 40`(`温度`/`温度24`)、`温度 22 度 湿度 40` + `温度 24 度`、`温度 22 度 湿度 40` + `温度 24 度 湿度 60`(`温度22`/`温度24`)、`预算 5000 元 超了 8000` + `预算 6000 元`(`预算5000`/`预算`)四对全 0 条,方向 fail-closed(两条都留在页上)。D44 按评审建议只**披露 + 加测试**(`test_a_second_number_in_one_memory_hides_the_update`),不用「未改名主语兜底比对」,那会把 `Room 3 holds 22 degrees` + `Room 4 holds 24 degrees` 退回 D37 的误报。四条精度说明全部采纳:P-a `degrees` 键也吞年份(`in 2026` → `{'degrees':'2026'}`),P5 那句已改成「第二个数字,含年份」;P-b 反向实验的红名单是 4 条不是 5 条(全套 4 failed / 379 passed,无连带);P-c 「数字只要站在某个词之后就是量不是名字」已就地加限定词(且不在紧挨主语那个位置);P-d 8799 守卫仍只在开跑时探一次,现在指纹补上了**旧构建**答题这一口——同树残留的服务占住端口时指纹仍给 200(评审实测:同树同 `ELY_EYE_AUTH_TOKEN` 的服务把 5 条 atom 落进它自己的 home,直到挂载卡带才失败),那种情形仍只有 busy-port 守卫能挡。该轮同时实测确认:门禁全绿且 mypy 两边逐字相同、tokenize 0 块、最大文件 `bench/systems.py` 498 行;D41 的新测试文案与实测一致;P4 的 stamp 判断站得住(`bench/leg_report.py` 两处 blob 全同 `40596d29…`)。判定:p2-3.1/3.2/3.3 与两条验收都能 `[x]`,p2-3.4 因 D43/D44 的记录级缺陷暂缓。
- **评审第 5 轮 PASS**(同一独立 pi,7 项全跑,本轮 diff 仅 3 个文件 +31/−5、机制 blob 全同:`trust.py d0b1a090`、`memory.py 13b0ebbf`、`retrieval.py 305d6e3a`、`compiler.py 5d341ca5`、`api_memory.py 22a03666`):D42/D43/D44 全部按实测改掉并被独立复现——D42 的判别子它自己起两边服务量过(`4c6af87` 的 `/api/memory/working` 404、HEAD 200);D43 三个主语实测 `3` / `room3` / `2026`;D44 四对逐对 0 条且贴出了页面(两条都留着),新测试的两条断言它逐条读过(第二条真的检查了「都在页上」)。**「不用未改名主语兜底比对」这个判断它用实验支持了**:把 `quantity_conflict` 改成主语不等时退回比值键,`Room 3 holds 22 degrees` + `Room 4 holds 24 degrees` 立刻变回 `('holds','22','24')`,即第 2 轮 D37 的误报复活。反向实验(去掉 `_numbers_follow` 条件)红名单仍是 4 条、全套 4 failed / 379 passed,**新加那条不变红**(无条件改名下它两条断言仍成立)。20 对冲突 + D40 五对 + `Room 3 is big` 边界逐字复现。门禁全部重跑且与本报告逐格一致(384 passed、六文件 83、conflicts 27、三文件 35、mypy 与 `9cb46c1` 文件集合与去行号文本逐字相同、tokenize 148 个 `.py` 0 块、最大文件 `bench/systems.py` 498、go `-count=1` 全套 + gofmt 空、`index-CSDHCFNo.js 221.48 kB`、smoke PASS)。两条精度说明(不算缺陷)已采纳:P-e 把 D44 成因从「一侧改名」改成「改名后主语键各自吃进自己的数字,两侧都改名也永不相等」(我另测 `温度 22 度 湿度 40` + `温度 24 度 湿度 40` → 0 条证实对称情形);P-f 把「指纹补上 8799 那一口」限定为旧构建那一口。判定:p2-3.1/3.2/3.3/3.4 与两条验收全部 `[x]`。
- 顺带:`cli.py` 495→432 行(写路径拆进 `cli_memory.py`),`bench/leg_check.py` 拆出 `bench/leg_report.py`;typer 0.27.2 用**关键字**调用回调,`*atom_ids: str` 形式的命令一调就 `TypeError`,改成 `list[str]`——这才是之前 `test_maintenance` 那条 TypeError 的真因,命令重名只是表象。报告 stamp 早于 harness 的缺陷也修了(`a52c104`)。
- [ ] **p2-4** 共享记忆
- [ ] p2-4.1 多用户/多 agent 作用域 + ACL