feat(memory): 记忆 P2(项目索引物化 / decay 回写 / 作用域写规则 / 静默吞点 + loop 级只读红线测试)
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled

This commit is contained in:
AskaEth committed 2026-10-05 11:47:39 +08:00
1 parent 9b903a89bc
commit 509dc412eb
12 files changed
+660 -55

No files matched your search

+62
View File
@@ -0,0 +1,62 @@
package memory
import (
"context"
"strings"
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/memory/projectindex"
)
// 本文件是「项目记忆 → 项目索引」链接的写入侧:把项目作用域记忆携带的
// ProjectID 落成一条真实的项目索引条目,使 design/03 §3 的
// 「项目索引 → 项目描述 → 章节内容」三层在第一层就有数据可解析。
//
// 背景(审计 2026-10-05 §4/§7):project / project_chapter 两表数据为零,
// 而实库里 project 域记忆的 project_id 大量悬空 —— Retriever.projectFor
// 对这些条目恒返回 nil,模型够不着项目/章节。项目索引的权威写入源是
// 知识库导入(admin RAG,PutProject + AddChapter);但一条**先写下来**的
// 项目记忆(LLM 提取或手动添加)若引用一个尚未导入的项目 id,链接即断。
// 这里在记忆写入时确保该索引条目存在(只补不覆盖),把悬空链接就地接通,
// 而不新建知识内容(记忆 ≠ 资料,design §3)。
// attachProjectIndex 把项目索引存储接到该 store 的写入路径:此后每条经
// Add 落库的项目作用域记忆都会确保其 ProjectID 对应的索引条目存在。nil
// 索引是空操作。可并发调用,last-writer-wins。
func (s *Store) attachProjectIndex(idx *projectindex.Store) {
if s == nil || idx == nil {
return
}
s.projMu.Lock()
defer s.projMu.Unlock()
s.proj = idx
}
// projectIndex 返回当前接入的项目索引存储,未接入时为 nil。
func (s *Store) projectIndex() *projectindex.Store {
if s == nil {
return nil
}
s.projMu.RLock()
defer s.projMu.RUnlock()
return s.proj
}
// ensureProjectIndex 为项目作用域且带 ProjectID 的条目确保项目索引条目
// 存在。其它作用域、空 ProjectID、未接入项目索引时都是空操作。
//
// 失败只记日志:项目链接是元数据,接通失败绝不使记忆写入失败(与
// indexItem 的语义一致);条目的关系行已经在库里,Retriever.projectFor
// 仍有日志可循(见 retrieve.go 的静默吞点修复)。
func (s *Store) ensureProjectIndex(ctx context.Context, item MemoryItem) {
if item.Scope != ScopeProject || strings.TrimSpace(item.ProjectID) == "" {
return
}
idx := s.projectIndex()
if idx == nil {
return
}
if err := idx.EnsureProject(ctx, item.ProjectID, item.ProjectID); err != nil {
s.log.Warn("memory: ensure project index entry failed, link left dangling",
"project_id", item.ProjectID, "id", item.ID, "error", err)
}
}