fix(memory): 读侧过 status 闸 —— 软删行不再被检索、不再进面板
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled

整理(tidy)与入库前审查(D3)都只做软动作:status 改 merged/archived,
正文一个字不改、行一行不删。因此只要有一条「能看到什么记忆」的读路径
漏了 status 闸,清理就等于白清 —— 生产库上那 10 条 merged 行就是反例。

- store.go:ListByPersona / List 加 status='active'
- rules.go:ListScope / ListProject 加 status='active'
- Get 刻意不过闸(撤销软合并要先读得到被吸收的行),并在注释里写明
- gateway/admin.go collectMemories 只加注释:口径只有一处,不重复过滤

验证:go build ./... + go vet 绿;memory 包全量测试绿;
新增 status_gate_test.go(4 条读路径逐条断言 + Retriever 端到端
「内容逐字相同的两条,软合并后只召回一条」+ Get 反向护栏);
用 -overlay 把闸门变异成恒真 ⇒ 5 个读路径用例与检索端到端用例全红。
This commit is contained in:
AskaEth committed 2026-10-07 09:48:37 +08:00
1 parent c2e413ec38
commit 9126d653e5
4 files changed
+233 -11

No files matched your search

+16 -7
View File
@@ -284,31 +284,40 @@ func (s *Store) Get(ctx context.Context, id string) (MemoryItem, error) {
// 命名空间(design §4)。它只是检索候选集的*自有*
// 那一半 —— 检索器在排名前把它与共享的用户/项目作用域
// 取并集(design §3/§4,见 Retriever.visibleCandidates)。
//
// 只返回**在册**(status='active')条目:整理与入库前审查都只做软动作,
// 被合并/被归档的行内容原样留在表里,因此「能看到什么记忆」的每条读路径
// 都必须过 status 闸,否则清理过的行照样会被检索到、照样占满面板。
func (s *Store) ListByPersona(ctx context.Context, personaID string) ([]MemoryItem, error) {
if strings.TrimSpace(personaID) == "" {
return nil, ErrMissingPersona
}
rows, err := s.db.QueryContext(ctx, `SELECT id, persona_id, role_id, project_id, scope, kind,
content, tags, confidence, source_conversation, created_at, updated_at,
decay_weight FROM memory_items WHERE persona_id = ?
ORDER BY created_at DESC, rowid DESC`, personaID)
decay_weight FROM memory_items WHERE persona_id = ? AND status = ?
ORDER BY created_at DESC, rowid DESC`, personaID, string(tidyStatusActive))
if err != nil {
return nil, fmt.Errorf("memory: list persona items: %w", err)
}
return scanItems(rows, "persona item")
}
// List 返回跨所有角色的每条记忆条目,最新的在前。rowid
// List 返回跨所有角色的每条**在册**记忆条目,最新的在前。rowid
// 打破 created_at 并列,使同一时钟刻内写入的条目保持
// 稳定的最新优先顺序。它是聚合/管理视图,枚举全部
// 已持久化记忆,不受角色隔离边界限制;必须
// 稳定的最新优先顺序。它是聚合/管理视图,不受角色隔离边界限制;必须
// 遵守隔离的调用方应改用 ListByPersona 或 ListScope。返回的
// 切片非 nil。
//
// 「在册」= status='active':被软合并('merged')与被归档('archived')
// 的行**内容与行都还在**,但不再参与检索、不再进上下文、也不再出现在
// 管理面板的记忆列表里(design/23 §8/§10 的软动作口径)。要看它们走
// 审查留痕 `GET /admin/memories/reviews` 或按 id 用 Get —— 后者刻意
// 不过 status 闸,因为撤销软合并必须先读得到被吸收的行。
func (s *Store) List(ctx context.Context) ([]MemoryItem, error) {
rows, err := s.db.QueryContext(ctx, `SELECT id, persona_id, role_id, project_id, scope, kind,
content, tags, confidence, source_conversation, created_at, updated_at,
decay_weight FROM memory_items
ORDER BY created_at DESC, rowid DESC`)
decay_weight FROM memory_items WHERE status = ?
ORDER BY created_at DESC, rowid DESC`, string(tidyStatusActive))
if err != nil {
return nil, fmt.Errorf("memory: list items: %w", err)
}