fix: preserve compaction provenance through resume and harden memory retrieval

Carry allowlisted summary origin and host user ledger across the trace recovery boundary. Keep immutable snapshot links after alias failures, scope legacy padded keys, remove stale indexes under small budgets, reuse retrieval clues for labels, and add cross-platform memory CI.
This commit is contained in:
Sycun committed 2026-10-12 17:56:43 +08:00
1 parent c96c4666a9
commit b642fc7c83
17 files changed
+644 -149

No files matched your search

+8 -3
View File
@@ -14,7 +14,9 @@ var factKeyPattern = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9._/-]*$`)
// ValidateFactKey 校验事实 key(项目内唯一标识)。
func ValidateFactKey(key string) error {
key = strings.TrimSpace(key)
if key != strings.TrimSpace(key) {
return fmt.Errorf("fact_key 不允许首尾空白")
}
if key == "" {
return fmt.Errorf("fact_key 不能为空")
}
@@ -116,9 +118,12 @@ func (db *DB) SaveContextCheckpoint(f *ProjectFact) error {
return nil
}
// Match strings.TrimSpace for legacy keys without migrating or rewriting them.
const factKeyTrimCharacters = "\t\n\v\f\r \u0085\u00a0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u2028\u2029\u202f\u205f\u3000"
func contextCheckpointScopeSQL(conversationID string) (string, []interface{}) {
return " AND (substr(fact_key, 1, ?) != ? OR (source_conversation_id = ? AND ? != ''))",
[]interface{}{len(ContextCheckpointKeyPrefix), ContextCheckpointKeyPrefix, strings.TrimSpace(conversationID), strings.TrimSpace(conversationID)}
return " AND (substr(trim(fact_key, ?), 1, ?) != ? OR (source_conversation_id = ? AND ? != ''))",
[]interface{}{factKeyTrimCharacters, len(ContextCheckpointKeyPrefix), ContextCheckpointKeyPrefix, strings.TrimSpace(conversationID), strings.TrimSpace(conversationID)}
}
// CreateProject 创建项目。
@@ -0,0 +1,73 @@
package database
import (
"fmt"
"go.uber.org/zap"
"path/filepath"
"strings"
"testing"
)
func TestLegacyWhitespaceCheckpointKeysRespectConversationScope(t *testing.T) {
db, err := NewDB(filepath.Join(t.TempDir(), "legacy.db"), zap.NewNop())
if err != nil {
t.Fatal(err)
}
defer db.Close()
p, err := db.CreateProject(&Project{Name: "scope"})
if err != nil {
t.Fatal(err)
}
for i, space := range []string{" ", "\t", "\r\n", "\u00a0", "\u3000"} {
canonical := fmt.Sprintf("note/context-legacy%d", i)
f, err := db.UpsertProjectFact(&ProjectFact{ProjectID: p.ID, FactKey: canonical, SourceConversationID: "other", Summary: "LEGACY_PROGRESS"})
if err != nil {
t.Fatal(err)
}
oldKey := space + canonical + space
// Simulate an already-stored legacy row, without migrating it on read.
if _, err := db.Exec("UPDATE project_facts SET fact_key=? WHERE id=?", oldKey, f.ID); err != nil {
t.Fatal(err)
}
if !IsContextCheckpointKey(oldKey) {
t.Fatal("helper did not identify legacy key")
}
if err := ValidateFactKey(oldKey); err == nil {
t.Fatal("write validation accepts noncanonical key")
}
if _, err := db.UpsertProjectFact(&ProjectFact{ProjectID: p.ID, FactKey: space + "finding/new"}); err == nil {
t.Fatal("write boundary stored noncanonical key")
}
}
if _, err := db.UpsertProjectFact(&ProjectFact{ProjectID: p.ID, FactKey: "finding/shared", Summary: "shared"}); err != nil {
t.Fatal(err)
}
for _, filter := range []ProjectFactListFilter{{RestrictContextCheckpoints: true, ContextConversationID: "current"}, {RestrictContextCheckpoints: true, ContextConversationID: "current", Search: "LEGACY_PROGRESS"}, {RestrictContextCheckpoints: true}} {
facts, err := db.ListProjectFacts(p.ID, filter, 100, 0)
if err != nil {
t.Fatal(err)
}
for _, f := range facts {
if IsContextCheckpointKey(f.FactKey) {
t.Fatalf("foreign legacy note crossed scope: %q", f.FactKey)
}
}
}
index, err := db.ListProjectFactsForIndex(p.ID, false, "current")
if err != nil || len(index) != 1 || index[0].FactKey != "finding/shared" {
t.Fatalf("legacy note leaked into index: %#v %v", index, err)
}
history, err := db.ListProjectFacts(p.ID, ProjectFactListFilter{Search: "LEGACY_PROGRESS"}, 100, 0)
if err != nil || len(history) != 5 {
t.Fatalf("historical records lost: %#v %v", history, err)
}
for _, f := range history {
if f.FactKey == strings.TrimSpace(f.FactKey) {
t.Fatal("read mutated legacy evidence")
}
}
own, err := db.ListProjectFactsForIndex(p.ID, false, "other")
if err != nil || len(own) != 6 {
t.Fatalf("legacy origin no longer visible to owner: %#v %v", own, err)
}
}