fix(memory): 堵住 knowledge 工具绕过群聊/陌生人过滤的 P0 / owner 召回 hits 如实上报 / 静默吞点补来源日志
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:30:51 +08:00
1 parent e792baa316
commit eeb5f26668
5 files changed
+250 -52

No files matched your search

+26 -52
View File
@@ -69,7 +69,7 @@ func (l *Loop) memoryBlock(ctx context.Context, messages []model.Message) string
}
// retrieveMemoryBlock 返回 env 与该 turn 角色的记忆注入块。
// owner 全权限环境保持既有的 Inject 行为(候选集已并入
// owner 全权限环境保持既有的完整召回(候选集已并入
// user/project 共享域)。受限环境(群聊 / 陌生人)只注入
// 非敏感的共享域记忆 —— 用户习惯(ScopeUser)与项目记忆
// (ScopeProject,人人可读)—— 并把该块标记为群聊隔离,
@@ -78,9 +78,8 @@ func (l *Loop) memoryBlock(ctx context.Context, messages []model.Message) string
// (design/03 §5「不泄露主人敏感信息」+ 主人 2026-10-03
// 裁决「用户习惯记忆与不敏感数据:可查、不可写」)。
//
// 同样按敏感项排除的是 L4 意图记录(capacity.RecordIntent 的回写格式,
// 内容是主人的逐句原话):它虽落在共享的用户习惯域以便 L4 闭环学习,
// 但群聊/陌生人只该看到「不敏感的习惯结论」,不该看到主人原话。
// 隔离判据单一实现于 FilterSharedVisible:本条注入路径与模型可达的
// knowledge 检索工具共用它,绝不各造一套过滤。
//
// 召回本身包在一个 memory span 里(裁决 11.4.2,design/19 §1
// 旁路节点):span 只记元数据(作用域/命中条数/注入块字节数/耗时),
@@ -88,9 +87,18 @@ func (l *Loop) memoryBlock(ctx context.Context, messages []model.Message) string
func (l *Loop) retrieveMemoryBlock(ctx context.Context, env SessionEnv, personaID, query string) (string, error) {
if !env.Restricted() {
rctx, span := l.span(ctx, spanNameMemoryRecall, trace.SpanKindMemory)
block, err := l.retriever.Inject(rctx, personaID, query, 0)
l.finishMemoryRecallSpan(span, "owner", 0, len(block), err)
return block, err
results, err := l.retriever.Retrieve(rctx, personaID, query, 0)
if err != nil {
l.finishMemoryRecallSpan(span, "owner", 0, 0, err)
return "", err
}
if len(results) == 0 {
l.finishMemoryRecallSpan(span, "owner", 0, 0, nil)
return "", nil
}
block := formatMemoryBlock(results, "")
l.finishMemoryRecallSpan(span, "owner", len(results), len(block), nil)
return block, nil
}
rctx, span := l.span(ctx, spanNameMemoryRecall, trace.SpanKindMemory)
results, err := l.retriever.Retrieve(rctx, personaID, query, 0)
@@ -98,20 +106,7 @@ func (l *Loop) retrieveMemoryBlock(ctx context.Context, env SessionEnv, personaI
l.finishMemoryRecallSpan(span, "restricted", 0, 0, err)
return "", err
}
shared := make([]memory.Result, 0, len(results))
for _, r := range results {
if !sharedScope(r.Item.Scope) {
continue
}
// L4 意图记录(capacity.RecordIntent 的回写格式)是主人原话的
// 机器可读形态,属主人敏感信息:共享域可查的是「不敏感的用户
// 习惯」,主人的逐句原话绝不注入群聊/陌生人上下文
// (design/03 §5「不泄露主人敏感信息」)。
if capacity.IsIntentRecord(r.Item.Content) {
continue
}
shared = append(shared, r)
}
shared := FilterSharedVisible(results)
if len(shared) == 0 {
l.finishMemoryRecallSpan(span, "restricted", 0, 0, nil)
return "", nil
@@ -121,34 +116,6 @@ func (l *Loop) retrieveMemoryBlock(ctx context.Context, env SessionEnv, personaI
return block, nil
}
// sharedScope 报告 scope 是否为群聊/陌生人可查的非敏感共享域:
// 用户习惯(user,全局共享)与项目记忆(project,人人可读)。
// 角色日常(persona)是主人私有的敏感项,不在其列
// (design/03 §3/§5 的敏感度划分即作用域本身)。
func sharedScope(s memory.Scope) bool {
return s == memory.ScopeUser || s == memory.ScopeProject
}
// formatMemoryBlock 由检索结果构建前导系统块。非空
// marker 作为该块的前缀,使隔离的群聊读取可与
// 主人的完整读取区分开。
func formatMemoryBlock(results []memory.Result, marker string) string {
var b strings.Builder
if marker != "" {
b.WriteString(marker)
b.WriteString(":")
}
b.WriteString("相关记忆(按相关度 × 衰减排序):")
for _, res := range results {
b.WriteByte('\n')
b.WriteString("- [")
b.WriteString(string(res.Item.Kind))
b.WriteString("] ")
b.WriteString(res.Item.Content)
}
return b.String()
}
// assembleContext 为一轮构建模型可见的消息列表(design
// §1.3 每轮 assemble,循环中与 OpenClaw 的 transformContext 等价之物):它
// 重新注入该角色的相关长期记忆 —— 检索每轮都从
@@ -265,8 +232,11 @@ func (l *Loop) extractMemory(ctx context.Context, messages []model.Message, step
// 使其廉价的 "记住 X" 路径保持可观测且不变。
//
// 与 extractMemory 相同的门仍然适用:受限对话绝不
// 被提取(只读隔离,design/02 §3 / design/11 §2),缺少
// extractor 或 store 时为空操作。提取失败与 store 失败会
// 被提取(只读隔离,design/02 §3 / design/11 §2)。未配置
// extractor 时为空操作;**配置了 extractor 却没有 store** 时
// 提取出的条目会被丢弃,因此该情形带来源记一条告警(l.log 已由
// sourcedLog 标记 source=runtime/loop),绝不静默吞掉
// (审计 2026-10-05 §7 静默吞点)。提取失败与 store 失败同样
// 带循环的来源记录日志,绝不向上传播。
//
// 它同时是 loop 侧**回合落定**的记忆钩子入口(run 路径每个已落定
@@ -280,7 +250,11 @@ func (l *Loop) extractMemoryAsync(ctx context.Context, messages []model.Message,
l.log.Debug("memory extract skipped: read-only context", "step", step, "env", env.ActorEnv)
return
}
if l.extractor == nil || l.memory == nil {
if l.extractor == nil {
return
}
if l.memory == nil {
l.log.Warn("memory extract skipped: no memory store configured; extracted memories would be dropped", "step", step)
return
}
async, ok := l.extractor.(asyncExtractor)