refactor(memory): 移除 media_refs/引用计数,媒体成为一等记忆块

媒体此前是"文本块 + digest 引用 + owner 账本 + 独立 GC":ContextEvent.Media
记 digest,media_refs 表用 owner_kind/owner_id 保活,ref_count 决定 GC 能否清。
这与文本记忆块的管理方式不一致,也是本次一并纠正的核心偏差。

改为与文本块完全一致的生命周期:

1. 一等记忆块直接由所在层持有
   - ContextEvent.Blocks / Doc.Blocks / GraphDB memory_blocks
   - 块带 modality/digest/MIME/size/vector/fingerprint,文本、图片、视频同构
   - Context→Document→Graph 迁移的是块本身(ID 不变),迁移后清空源容器,
     同一块不同时存在于两层

2. 删除平行生命周期账本
   - media.Store 去掉 media_refs 表、OwnerKind 常量、RefCount 字段、
     AddRef/DropRef/DropOwner/Refs、ref_count 列与索引
   - 删除 mediaGCLoop、GC(keep,minAge)、容量上限与 media.gc_* / media.max_mb 配置
   - 媒体内容在块被永久删除时一并删除(media.Store.Delete + forgetPayloads),
     与"删除文本块即删除内容"同一语义

3. L3 原生结构
   - memory_blocks / memory_block_edges(contains/depicts/derived_from)
   - 边端点必须是真实图节点,不再用 owner 字符串伪装关系
   - BlocksForNode 支持 sentence --contains--> block 反查

4. SDK 与检索同步
   - 插件附件/标记直接变成块,不再 AddRef
   - 跨模态检索改用 QueryMediaScored(CAS 内不再有孤儿缓存需要过滤)

测试全部改写为块语义:删除 refcount/media_refs/GC 断言,新增块迁移、
单层不变量、Delete 语义与并发删除回归。

注:cmd/homed/main.go 同时携带工作区中既有的 CLIP→Qwen 模型目录接线改动。
This commit is contained in:
JianFeeeee
2026-09-11 10:57:22 +08:00
parent e44164f5bd
commit dae01f9c06
27 changed files with 1391 additions and 1969 deletions

View File

@ -30,7 +30,7 @@ func newTestStores(t *testing.T) (*memory.GraphDB, *doc.Store, *text.Memory, *me
}
t.Cleanup(func() { g.Close() })
ds := doc.NewStore(filepath.Join(dir, "documents"))
ds := doc.NewStore(filepath.Join(dir, "documents"), memory.TokenizeWords)
if err := ds.Start(); err != nil {
t.Fatalf("doc store start: %v", err)
}
@ -42,7 +42,7 @@ func newTestStores(t *testing.T) (*memory.GraphDB, *doc.Store, *text.Memory, *me
}
t.Cleanup(func() { tm.Stop() })
ms, err := media.New(filepath.Join(dir, "media"), 0)
ms, err := media.New(filepath.Join(dir, "media"))
if err != nil {
t.Fatalf("media.New: %v", err)
}
@ -140,12 +140,12 @@ func TestGraphCommit_BindsMediaFromDigests(t *testing.T) {
t.Errorf("句子里没有媒体描述: %q", res.Relations[0].SentenceText)
}
refs, err := ms.Refs(media.OwnerGraphSentence, strconv.FormatInt(sid, 10))
blocks, err := g.BlocksForNode("sentence", strconv.FormatInt(sid, 10))
if err != nil {
t.Fatalf("Refs: %v", err)
t.Fatalf("BlocksForNode: %v", err)
}
if len(refs) != 1 || refs[0] != digest {
t.Errorf("句子 #%d 的媒体引用 = %v,期望 [%s]", sid, refs, digest)
if len(blocks) != 1 || blocks[0].PayloadDigest != digest {
t.Errorf("句子 #%d 的媒体块 = %+v,期望 [%s]", sid, blocks, digest)
}
}
@ -250,15 +250,12 @@ func TestDocInsertWithMedia_StoresAndBinds(t *testing.T) {
t.Errorf("正文里没有媒体标记: %q", d.Content)
}
refs, err := ms.Refs(media.OwnerDocument, d.ID)
if err != nil {
t.Fatalf("Refs: %v", err)
}
if len(refs) != 1 {
t.Fatalf("文档媒体引用 = %v,期望 1 条", refs)
blocks := ds.Blocks()
if len(blocks) != 1 || blocks[0].PayloadDigest == "" {
t.Fatalf("文档记忆块 = %+v,期望 1 条", blocks)
}
// 内容可读,说明真的落盘了而不只是记了个 digest。
got, err := ms.Get(refs[0])
got, err := ms.Get(blocks[0].PayloadDigest)
if err != nil || string(got) != "attachment-bytes" {
t.Errorf("媒体内容读回失败: %v / %q", err, got)
}
@ -279,9 +276,9 @@ func TestDocInsertWithMedia_DigestOnlyReference(t *testing.T) {
if after := ms.Stats()["count"]; after != before {
t.Errorf("媒体条数从 %v 变成 %v —— 引用已有内容不该新增", before, after)
}
refs, _ := ms.Refs(media.OwnerDocument, d.ID)
if len(refs) != 1 || refs[0] != digest {
t.Errorf("引用 = %v,期望 [%s]", refs, digest)
blocks := ds.Blocks()
if len(blocks) != 1 || blocks[0].PayloadDigest != digest {
t.Errorf("引用 = %+v,期望 [%s]", blocks, digest)
}
}
@ -350,8 +347,8 @@ func TestDocQuery_FallsBackToMarkers(t *testing.T) {
}
}
// 旧实现删文档不解引用 → 媒体永久"被引用",GC 收不掉,磁盘只增不减。
func TestDocRemove_ReleasesMediaRefs(t *testing.T) {
// 文档被删除时它持有的一等记忆块随之消失,媒体不再被任何记忆块持有。
func TestDocRemove_DropsBlocks(t *testing.T) {
_, ds, _, ms := newTestStores(t)
dm := NewDocMemoryWithMedia("tester", ds, ms)
@ -361,14 +358,14 @@ func TestDocRemove_ReleasesMediaRefs(t *testing.T) {
}}); err != nil {
t.Fatalf("InsertWithMedia: %v", err)
}
if refs, _ := ms.Refs(media.OwnerDocument, d.ID); len(refs) != 1 {
t.Fatalf("前置条件不成立,引用 = %v", refs)
if blocks := ds.Blocks(); len(blocks) != 1 {
t.Fatalf("前置条件不成立,块 = %+v", blocks)
}
dm.Remove(d.ID)
if refs, _ := ms.Refs(media.OwnerDocument, d.ID); len(refs) != 0 {
t.Errorf("删除文档后仍有 %v 条引用 —— GC 永远收不掉这份媒体", refs)
if blocks := ds.Blocks(); len(blocks) != 0 {
t.Errorf("删除文档后仍持有 %+v —— 媒体仍被记忆引用", blocks)
}
}