fix(memory): 堵住 knowledge 工具绕过群聊/陌生人过滤的 P0 / owner 召回 hits 如实上报 / 静默吞点补来源日志
This commit is contained in:
1 parent
e792baa316
commit
eeb5f26668
5 files changed
+250
-52
No files matched your search
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user