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

+17 -11
View File
@@ -101,7 +101,10 @@ func (l *Loop) retrieveMemoryBlock(ctx context.Context, env SessionEnv, personaI
return block, nil
}
rctx, span := l.span(ctx, spanNameMemoryRecall, trace.SpanKindMemory)
results, err := l.retriever.Retrieve(rctx, personaID, query, 0)
// 受限检索是只读的(design/03 §5「能查不能写」):关掉衰减退化的物化
// 回写,使群聊/陌生人的一次召回不会改动记忆行(哪怕是 decay_weight 这类
// 派生列,也不该在只读上下文里被写)。
results, err := l.retriever.Retrieve(memory.WithoutDecayWriteback(rctx), personaID, query, 0)
if err != nil {
l.finishMemoryRecallSpan(span, "restricted", 0, 0, err)
return "", err
@@ -290,11 +293,15 @@ func (l *Loop) extractMemoryAsync(ctx context.Context, messages []model.Message,
}
// persistMemories 把提取出的条目写入记忆 store,为每个条目
// 打上产出该 turn 的角色。该赋值刻意无条件:
// extractor 刻意不感知角色(它不再持有一个在构造期
// 内嵌的人格,cmd/cyrene/extractor.go),否则多角色
// 进程会把每条记忆都归到一个角色下。角色为空(完全未配置
// persona)时条目保持 extractor 产出时的样子。
// 打上产出该 turn 的角色。角色归属经 Store 的作用域写规则
// (memory.Store.AddScoped,design/03 §3)落定,而不是在循环里改写:
// extractor 刻意不感知角色(它不再持有一个在构造期内嵌的人格,
// cmd/cyrene/extractor.go),因此它产出的条目通常不带 PersonaID,
// AddScoped 用本回合角色(actorID)把它补上;若 extractor 送出一条
// **归属别的角色**的角色作用域条目,AddScoped 以 ErrReadOnly 拒绝,
// 绝不把它冒充成本回合角色的日常记忆(Store 层防线,审计 2026-10-05
// §1.3「作用域写规则未接线」)。角色为空(完全未配置 persona)时
// AddScoped 返回 ErrMissingPersona,条目按失败记录日志。
//
// 溯源同样在此盖真实 id(裁决 04.4.3,design/03 §2):
// source_conversation 由本持久化点从运行上下文取 Run 的
@@ -315,11 +322,8 @@ func (l *Loop) persistMemories(ctx context.Context, items []memory.MemoryItem, s
wctx, span := l.span(ctx, spanNameMemoryWrite, trace.SpanKindMemory)
written, failed := 0, 0
for _, it := range items {
if personaID != "" {
it.PersonaID = personaID
}
it.SourceConversation = source
if _, err := l.memory.Add(wctx, it); err != nil {
if _, err := l.memory.AddScoped(wctx, personaID, it); err != nil {
failed++
l.log.Warn("memory store", "step", step, "error", err)
continue
@@ -416,7 +420,9 @@ func (l *Loop) recordIntent(ctx context.Context, messages []model.Message, step
Confidence: judgment.Confidence,
SourceConversation: memory.FormatSourceConversation(loopSession(ctx), loopTurn(ctx)),
}
if _, err := l.memory.Add(ctx, item); err != nil {
// 经 Store 的作用域写规则落库(ScopeUser 对任何角色可写,design/03 §3),
// 与 persistMemories 共用同一条受规则约束的写入路径,而不是绕过规则直插。
if _, err := l.memory.AddScoped(ctx, personaID, item); err != nil {
l.log.Warn("memory: L4 intent writeback failed", "step", step, "error", err)
return
}