Files
HomeAgent/docs/zh/legacy-table-retirement.md
JianFeeeee cf14b840ef docs: 补齐文档状态分类 + 校正两轮代码改动后的失真
## 背景

上一次文档校订(b9b5ac0)之后又有两次代码提交(5194a96 / 8293282),
把 future-plan §1.1 整节变成了错的 —— 它还写着「档位没有生效点」
和「在 prepareInputTask 里加一次 activateSecurityProfile 调用」,
而后者正是被后来的收敛**否定掉的方案**。

## 新增 docs/zh/README.md(文档索引)

32 篇文档此前混着三种时态(当时实测 / 历史推演 / 设计规划),外观相同。
索引给出**状态分类与可信度规则**:

| 状态 | 能不能当事实用 |
|---|---|
| 已实现并部署 | ✅ 直接信 |
| 部分实现 | ⚠️ 只信标了"生效"的部分 |
| 设计 / 未实现 | ❌ 只能当设计意图 |
| 归档说明 | ❌ 不要改、也不要信 |

并写死**判据名可信度规则**:文档里的 `TestXxx` 不保证还存在。
给了核对命令,以及当前基线(27 条缺失的分布 —— 18 条属"设计未实现"类
文档的规划名、7 条是 §2.7.3 有意保留的历史对照表左栏、2 条属视频能力
未纳入契约)。判定标准:**在"已实现"类文档正文里找不到才是缺陷**。

## 重写 future-plan §1.1(已结案)

原节写的是错的方案。改成记录**真正的缺陷分三层**:

```
第 3 层(表面):activateSecurityProfile 零调用方
第 2 层:agent/core 在替 security 做策略决策
         (自己抠 security_profile / 自己选判定链 /
           自己写 Kind:"resident" 声明主体类别 —— 提权口子)
第 1 层(根因):security 缺少「输入接入点」
```

并补上「每 agent 一个实例」的最终形态(共享注册表 + per-agent 执行视图,
sessionProfile / longtimeProfile 两层,优先级 session > longtime 本身是安全语义)。

§〇「机制 ≠ 接线」表格里档位那一条从 🔴 改为 ✅。

## 各文档状态行

此前 14 篇缺状态行(runbook / 实测报告 / 计划类)。逐篇补上**符合实际内容**
的声明,而不是统一套一句。特别地:

- `commondev-design.md` 标题本就写「设计 · 未实现」→ 归到"设计"类
- `live-run-evidence-2026-10-04.md` → 明确标注"历史证据快照,不要用它的
  结论覆盖现在的实现判断"
- `legacy-issues.md` → 写明它与 future-plan 的分工(前者收已知问题,
  后者收能力缺失与零调用方)

## 正文里两处判据名漂移

- `TestBootCtxCloseAllIsReverseOrder` → 实际是
  `TestKernelStateCloseAllIsReverseOrder`(`BootCtx` 改名时判据跟着改的)
- `TestNilTypedBusSafety` → 标注为"规划名,仓库中不存在"

## 新增 §2.7.5 模块产物的实例化粒度

依赖图保证"设施已就绪",但没说该有几个实例 —— 这是独立决策。
记下 security 的两个答案:全局基线共享(否则插件声明对部分 agent 不生效)、
轮次状态 per-agent。并给出给未来新模块的两条判断标准。

## 安全文档

状态头补「架构形态」段(防火墙定位、per-agent 视图、两层档位);
接线状态表里 `ActivateProfile` 从 🔴 改为 ✅(2026-10-31 接通)。

## README

文档列表加索引与 future-plan 入口 —— 此前从 README 找不到 docs/zh 的任何指引。

## 核对

- docs/zh 32 篇全部有状态行
- 索引里列的文档全部存在,无遗漏
- 分类与各文档自述一致(逐篇抽查)
- go build 仍通过(纯文档改动)
2026-10-31 07:50:58 +08:00

18 KiB
Raw Blame History

旧表清理可行性评估(2026-10-04)

状态:计划(收尾中)。 遗留表退役的执行记录;剩余读方见文内 §八 / §十二。

结论:现在不能清理。55 处生产调用点仍在读写 entities / relations / sentences。 这份评估回答「能不能删」以及「删之前要先做什么」。

一、数据侧已就绪

清理的数据前置已全部通过(TestRetire_清理前置,生产快照实测):

判据 结果
① 每个 entity 都有对应的块 ✅ 1294 → 1294,缺块 0 个
② 每条 relation 都有对应的边 ✅ 980(去重 959)→ 959
③ 每条 sentence 都有原句块 ✅ 66 → 66(55db22c 修复后)
④ 悬空的 sentence→块 边 ✅ 0
⑤ 端点不存在的块边 ✅ 0

二、代码侧远未就绪

调用点分布

internal/memory/social/social.go            7 处
internal/memory/graph.go                    6 处(Commit/Recall 实现)
internal/memory/scene.go                    4 处
internal/memory/light_memory.go             4 处
internal/plugins/webui/handler_memory.go    2 处
internal/plugins/healthcheck/plugin.go       2 处
internal/plugin/proc/corehandler_memory.go  2 处
internal/plugin/lua_plugin.go                2 处
internal/sdk/memory_impl.go                 2 处(Recall + CommitWithMedia)
────────────────────────────────────────
合计 55 处

三张表各自还被谁依赖

entities

graph.go:505  Commit 里 SELECT id FROM entities WHERE name = ?   (写入路径)
graph.go:681  hotspots ORDER BY mention_count DESC
graph.go:756  SELECT ... FROM entities WHERE LOWER(name) LIKE ?  (Recall)

relations

graph.go:532  Commit 里查重(source+target+type+session)
graph.go:705  Recall 的关系遍历
graph.go:748  SELECT MAX(r.sentence_id) FROM relations r
graph.go:362  从 sentence_ref 回填 sentences

sentences

graph.go:362  INSERT OR IGNORE INTO sentences ... FROM relations.sentence_ref
graph.go:522  CommitWithMedia 挂接媒体块的落点
(EnsureSentence 已删 —— 它是最后一个写入点,07b8c2a 之后归零)

三、为什么这三条链路必须一起换

它们不是三张独立的表,而是一条写入链 + 一条读取链:

写入  Commit ──> entities(name 唯一) + relations + sentences(挂媒体块)
读取  Recall ──> entities(LIKE) + relations(遍历) + sentence_id

只删表不改代码 ⇒ 写入报 "no such table",读取返回空。 而 Commit 是在线主路径(每轮对话都可能调用), 不是可以延后的离线工具。

四、清理前必须完成的工作

按依赖顺序:

# 工作 说明
1 Commit 改写为写块 不能再 INSERT entities;三元组应直接落 memory_blocks + 块边
2 Recall 改写为读块 现在读 entities/relations;应读块 + 块边
3 媒体挂接点迁移 CommitWithMedia 的 sentence_id 要换成原句块 ID
4 social / scene / light_memory 适配 它们的调用要改到新接口
5 WebUI / healthcheck / proc / lua / SDK 适配 对外契约不变,实现换掉
6 回滚演练 在快照上跑完 1-5,验证召回不退化

★ 第 6 步不是可选项:Commit 在线上,改错了当场影响对话记忆。

五、当前建议

不要现在清理。 理由不是"风险高",而是:

  1. 数据侧就绪 ≠ 代码侧就绪(当前 55 处调用点)
  2. Commit/Recall 在在线主路径上,不是一次性迁移
  3. 相比之下,清理旧表的收益只是"少三张空表", 而代价是重写一条在线链路

⇒ 真正该做的顺序是:先让写入与读取都走块(1-3), 旧表自然失去写入方,最后再谈删除。

附:数据侧的两个真实缺口(已修)

  • 66 条 sentence 没有原句块(55db22c 修复): 迁移只从 entities 读,从不为 sentences 建载体 ⇒ 直接清理会丢原文
  • 关系边 959 vs relations 980: 20 组 (source,target,type) 完全重复被边表去重(dd2c996 已显式报出 DedupedEdges)

六、⚠️ 一个操作错误:cp 拿不到一致的快照

验证 55db22c(sentences 原句块)时,我用 cp ${HA_DATA}/memory/graph.db /var/tmp/ha-probe/fix.db 取副本, 结果:

生产库     entities=1294  sentences=66
cp 出来的   entities=1293  sentences=67      ← 差 1,且方向相反

第一反应是「迁移改动了源表」,差点去查迁移的写入逻辑。 实际是 cp 的问题 —— 生产库在 WAL 模式下,已提交但未 checkpoint 的数据还在 -wal 文件里,cp 只拿到主库文件 ⇒ 不一致快照。

用 sqlite3 .backup 重取后:

.backup 快照  entities=1294  sentences=66  blocks=98   ← 与生产库一致

★ 本文档与 production-recall-validation.md 里都写过 「库在写,cp 拿不到一致快照,要用 .backup」, 我自己写下的规则自己违反了。

⇒ 判据也该加一条:快照来源必须可验证。 最省事的做法是取完快照立刻与源库比对关键计数,不一致就重取 —— 差 1 就会暴露。

七、2026-10-04 进展:写入侧已完成

✅ Commit 块化(cdf0726)

Commit 现在在旧表写入之外并行块化:

三元组  主语块 --关系--> 宾语块
原句    原句块 blk_src_<hash>

四条写入路径(媒体桥 / memory_commit 工具 / 驻留子 / 记忆整理流水线) 全部经过它 ⇒ 旧表不再因块化而缺内容。

✅ 边升格为独立单位(52e4596)

去 UNIQUE(source_kind,source_id,target_kind,target_id,edge_type)
+ confidence / session_id / turn_id / status / merged_into

★ 那个 UNIQUE 才是「报告 980、实际 959」的根因 —— 边表从设计上 存不下多条同类边。详见该提交说明。

✅ scene_refs 完整迁移(07b8c23)

kind='entity'   450 条 → 'block'
kind='relation' 268 条 → 'edge'
生产快照实测:718 条迁移完成,悬空 0

✅ EnsureSentence 已删

sentences 表的最后一个写入点已移除并连同其专属测试一起删除。

八、剩余的读方(尚未切)

读方 调用处 状态
scene.go 取边、按名找邻居、悬空检查 ✅ 已改块/边(13c3292)
social.go 3 处 Recall(要「按名取实体 + 关系遍历」) 需要块侧的等价接口
indexer.go 2 处(全量实体名 → 建向量索引) 需要 AllBlockNames
distill.go 2 处(全量 → 记忆整理) 同上
light_memory.go 3 处(透传代理) 随上面改动
toolcall.go 旧路兜底(块路优先,已是生产主路) 保留兜底

需要的块侧能力(其中两项已就绪)

按名取实体        ❌ 缺(一条 SQL 的事)
按关系遍历        ✅ BFSBlocks(9df1efb)
取边 + 两端节点   ✅ AllRelationEdges / RelationEdgesBetween
全量块名          ❌ 缺(MemoryBlocks 已有,只需薄包装)

⇒ 「旧表退场」的准确说法:写入侧已全部块化,剩余是读方切换。

九、2026-10-04:读方切换全部完成

切了的(每项都有独立判据)

读方 提交 备注
scene.go 读写两侧 13c3292 6 处查询 + 补 contains 结构边
Purge 258eadf 9 处调用方
PurgeNoise 258eadf
RecallSorted(两分支) 258eadf 5a6e4a9 302 行,entityIDs 换键类型
Introspect 258eadf
MergeEntities → MergeBlocks 本轮 86 行 → 委托
social.go 5 处 本轮 见下

★ 「55 处调用点」是虚高的

indexer / social / distill / light_memory 全都不写裸 SQL,
它们都走 db.Recall(...) ⇒ 真正的收敛点是 RecallSorted 一个函数。

⇒ 按处数估工作量是错的。 逐个适配时才发现它们共用一条路。

★ 切换期最危险的状态是「混合态」

social 3 个测试当场变红,根因值得写下来:

写侧(Purge)已切块、读侧(RecallSorted)还没切
  ⇒ 软删的边在**旧表**里仍是 active
  ⇒ 召回照样返回它

混合态比全旧态更危险 —— 它看起来是「部分成功」, 而全旧态至少是一致地坏。读方必须一次切到位。

MergeBlocks(块合并)

旧 MergeEntities 85 行全在旧表,改名只改 entities.name, 块还叫「张先生」⇒ 召回照样命中。

★★ 与旧实现的本质差异:块 ID 是内容派生的 (blk_ent_<hash(name)>),「张先生」与「张三」是两个不同的块。 所以合并不是「改端点」,而是让源块消失并把它的边改指向目标块:

① 关系边重定向(source→target / target→target)
② 去自环:同端**同 edge_type** 才是重复
   ★ 只按 (source,target) 去重会把「喜欢咖啡」与「讨厌咖啡」误删一条
③ 删指向源块的结构边(否则端点悬空 → graphNodeExists 拒绝后续写入)
④ 删源块
⑤ scene_refs 把源的引用**换成目标**(不是删 ——
   「张先生那次值班」这个场景仍存在,只是人换了名字)

源块不存在必须报错,不能返回 (0, nil): 它有两种原因(已合并 / 名字写错),返回同一个结果会让后者表现为成功。

★ social:ID 空间错配是最隐蔽的一类

Entity.ID(int64,召回内序号)与 Relation.SourceID(块 ID,字符串) 是两个不同的 ID 空间。旧代码 r.SourceID == personID 永不匹配。

★ 症状极具迷惑性:GetTrait 里 if 匹配 {…} return r.SourceName 在「不匹配」时无条件执行,于是返回人物自己的名字当特质值 (判据里是 got "张三" 而非空串)—— 看不出是 ID 错配,只像数据错了。

修法是按名字匹配 —— 而且这本来就更对: 「张三的特质」与 ID 无关;按 ID 还有个隐患: 同一个人可能有两个块(历史数据里文本相同),按 ID 只认一个。

★ 块没有 type 概念

Triple.SubjectType 只写旧 entities.type,memory_blocks 无类型列。

ListPersons 原来靠 e.Type == "person" 筛选,改成从图结构推断:

人物 = 社交关系(非 trait)的任一端点 ∪ trait 关系的源

★ 这不是权宜之计 —— 它比 type 更可靠:type 是写的时候声明的 (调用方可能不声明、可能声明错),而结构是数据本身的性质。 旧实现里「AddRelation 加了人但没 SubjectType」就会漏掉那个人。


十、生产迁移已完成(2026-10-04 15:36)

执行

homed-graph-migrate -db ${HA_DATA}/memory/graph.db -apply \
  -embed-provider chineseclip -model-dir /var/tmp/ha-c/models/chinese-clip-vit-b16-onnx

耗时 161.8s,单事务,命令自动快照 graph.db.bak-20261004-153657

产出

迁移前 迁移后
块 98 2752(带向量 1391)
块边 97 2371
关系边(confidence 非空) — 980
scene_refs 旧 kind 718 0
scene_refs 新 kind — block 540 / edge 268 / document 2
悬空端点 — 0
中心向量 无 已重建(各向异性 detected,双边中心化启用)

验证(七步全通过)

① 停机确认      inactive + disabled          ✓
② WAL checkpoint 0|0|0                       ✓
③ 一致快照      integrity ok,五表逐一核对    ✓
④ 报告核对      与副本完全一致,未写库        ✓
⑤ 执行          161.8s,exit=0               ✓
⑥ 结构验证      七项全部通过                  ✓
⑦ 召回探针      7/9                          ✓

★ ④ 这一步的价值:副本上验证过还不够, 生产库的数字必须单独核对一次 —— 因为副本是快照, 而生产库在停机期间理论上不该有任何变化,但**"理论上"不是证据**。

★ ⑥ 的第 1 项(逐表核对旧表)证明了迁移确实没动源表:

entities 1294 / relations 980 / sentences 66  ← 与迁移前完全一致

召回前后对照

维度 迁移前 迁移后
casual 0/4 3/4
confusable 0/1 1/1
abstention 3/3(假象,见 abstention-criterion.md) 3/3(真实拒答)
coexist 0/1 0/1(BFS 未进召回链)
合计 3/9 7/9

回滚

主回滚点  /var/tmp/ha-prod-migrate/prod-20261004-153045.db(integrity ok)
迁移自带  ${HA_DATA}/memory/graph.db.bak-20261004-153657
代码回退  e3dea6d 及之前任一提交(全部已提交,未 push)

★ 回滚只需换代码:旧表未被改动,迁移前的读方仍能工作。

★ 过程中的两次自纠

1. 迁移报告曾报「关系 0」(d4eda47 已修)

Introspect 的口径在读侧切块后变成数块侧,而迁移报告要的是 「旧表还剩多少」;加上 SQL 语法错误被 if err == nil 吞掉 —— 一个语法错误伪装成「库里没有关系」。

★ 诊断误导比报错危险:报错了有人查,0 不会。

2. 首条执行命令因中文括号报 shell 语法错误,迁移未执行

事后确认生产库块数仍是 98 才重试。那次失败无任何影响, 但它说明执行清单里的命令必须先在目标 shell 上验证语法。

剩余工作

# 项 说明 阻塞
1 停旧表双写 commit() 里的 entities/relations INSERT 无(读方已全切块)
2 BFS 进召回链 coexist 维度 0/1:泛指提问召不回端口号 无
3 观察期后删旧表 三张表 + 约 55 处调用点 需第 1、2 项完成 + 观察
4 纯中文编造查询 契约外,需符号切分改进 无

★ 第 3 项不建议现在就做:删表不可逆,而第 1、2 项都还没做。

十一、停旧表双写已完成(2026-10-04 18:xx)

做了什么

commit() 不再写 entities / relations / sentences。 旧表停止增长,冻结为历史(生产 1294 / 980 / 66)。

读方此前已全部切块,所以这一步完全可逆:改回代码即可,旧表数据完好。

★ 判断依据是行为不是语句: 判据 Test停双写_旧表不再增长 逐表比对 Commit 前后的行数。

★★ 它暴露了六个静默失效

全部是「报成功、实际没做」或「悄悄做错事」,没有一处报错。

# 函数 现象
① memory_commit 工具层 每次都告诉模型「全部被拒,请检查实体名」 —— 而写入其实成功了。模型据此改实体名,把内容改坏
② syncIfStale 索引器永远不建立索引(旧表行数恒 0 ⇒ 连首次都判成「无需同步」)
③ ExportTriples 驻留子 agent 的回收失效(合入 0 条)
④ ClearSentenceID 一直在清理错误的行(调用方传边 ID,函数改旧表另一行;而 UPDATE 影响 0 行也是「成功」)
⑤ Archive 对块体系完全无效 ⇒「按时间衰减记忆」静默失效
⑥ archiveColdDocs 判据 ec==0 && rc==0 恒真 ⇒ L2 文档永不删除

★ ① 值得单说:「报假失败」比「报假成功」危险, 前者会诱发模型的破坏性动作(改不该改的内容)。

★ ⑥ 讽刺:它自己的注释写着 「实测 456 字图片描述得到 0 计数,随后文档被删、内容消失」。 同一个原因,方向反了 —— 从「误删」变成「永不删」。

另修复三个附带问题:PurgeOrphans 删错对象、Introspect 的 hotspots 冻结在迁移快照、FindRelations 精确查找失效。

★ 一个更隐蔽的:旧表的读取也必须停

SELECT id FROM entities WHERE name = ?

旧表删掉后它返回 no rows ⇒ Commit 直接失败 ⇒ 记忆完全写不进去。

★ 这是「停双写」时最容易漏的一环:写要停,读也要停。

★ 边去重:同会话收敛,跨会话并存

旧 relations 靠 UNIQUE(source,target,type,session_id) 保证
52e4596 为支持「同类边并存」把边表 UNIQUE 全部移除
⇒ 重复提交同一三元组产生重复边(实测同会话两次 → 2 条)

而记忆写入是高频重试的(LLM 反复提交同一事实), 重复边会让召回刷屏、场景权重虚高。

★★ 判据修正里最危险的一类

用 Commit 造旧表数据。旧表停写后迁移测试的输入天然为空, 于是「迁出 0 块 0 边」竟然通过。

★ 那不是「迁移正确」,是「测不到」—— 它让「迁移还对不对」这个问题无法回答。

已加显式的 Seed* 测试接口(明确标注仅供测试), 迁移测试现在真的测到了迁移。

判据修正汇总(约 15 处)

类型 处理
断言旧表计数 ec/rc 改用块侧信号
用 Commit 造旧表数据 改用 Seed* 接口
靠删旧表造状态 改删块侧(如「造孤立实体」)

★ Commit 的返回签名是 SDK 契约(9 处调用方),改签名代价大, 所以保留位置并置 0。 ★ 代价要说清楚:这两个返回值现在没有意义了, 依赖它们判断的调用方(若存在)应改用块侧计数。

十二、剩余工作

# 项 说明 风险
1 启动服务 生产库已迁移但服务仍停机 需确认
2 观察期后删旧表 三张表 + 若干调用点 不可逆
3 casual 3 格 / coexist 1 格 判据 want 口径窄于实际能力 无
4 Commit 返回值语义 ec/rc 已无意义,9 处调用方 无

★ 第 2 项建议先观察:旧表已冻结,删它唯一的收益是省几 MB, 而代价是不可逆 —— 万一将来要回滚分析历史数据就没了。