feat(memory): 记忆整理 v1(design/23)——低峰定期扫描 + 事实包含判定 + 规则否决 + 只产建议队列(不自动应用)
This commit is contained in:
1 parent
012e9f3ad4
commit
91a3209c9b
25 files changed
+3418
-13
No files matched your search
@@ -0,0 +1,122 @@
|
||||
package memory
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// 本文件是记忆整理(design/23 §10)的 schema 侧:新表(候选 / 运行)的 DDL、
|
||||
// memory_items 整理列的前滚,以及两者的一处装配入口。
|
||||
//
|
||||
// 口径与 memory_items 一致(见 store.go 末尾的说明):领域表的权威机制是
|
||||
// internal/db 的迁移(0019_memory_tidy),但记忆存储必须自包含、可在任意
|
||||
// *sql.DB 上使用,因此 NewStore 也幂等地创建这些表、前滚这些列。两处定义
|
||||
// 必须保持一致(tidy_schema_test.go 锁定)。
|
||||
|
||||
// createMemoryTidyCandidatesTable 是人工确认队列(design/23 §10)。
|
||||
// idempotency_key 唯一:同一批数据重复跑不重复产候选(design/23 §9)。
|
||||
const createMemoryTidyCandidatesTable = `
|
||||
CREATE TABLE IF NOT EXISTS memory_tidy_candidates (
|
||||
id TEXT PRIMARY KEY,
|
||||
persona_id TEXT NOT NULL,
|
||||
scope TEXT NOT NULL,
|
||||
project_id TEXT NOT NULL DEFAULT '',
|
||||
kind TEXT NOT NULL DEFAULT '',
|
||||
action TEXT NOT NULL,
|
||||
source_ids TEXT NOT NULL DEFAULT '[]',
|
||||
target_id TEXT NOT NULL DEFAULT '',
|
||||
proposed_content TEXT NOT NULL DEFAULT '',
|
||||
merged_content TEXT NOT NULL DEFAULT '',
|
||||
reason TEXT NOT NULL DEFAULT '',
|
||||
evidence TEXT NOT NULL DEFAULT '{}',
|
||||
rule_version TEXT NOT NULL DEFAULT '',
|
||||
score_min REAL NOT NULL DEFAULT 0,
|
||||
score_max REAL NOT NULL DEFAULT 0,
|
||||
status TEXT NOT NULL DEFAULT 'pending',
|
||||
idempotency_key TEXT NOT NULL UNIQUE,
|
||||
created_at TEXT NOT NULL,
|
||||
decided_at TEXT NOT NULL DEFAULT '',
|
||||
decided_by TEXT NOT NULL DEFAULT ''
|
||||
)`
|
||||
|
||||
const createMemoryTidyCandidatesStatusIndex = `
|
||||
CREATE INDEX IF NOT EXISTS idx_memory_tidy_candidates_status
|
||||
ON memory_tidy_candidates (status, scope)`
|
||||
|
||||
// createMemoryTidyRunsTable 是整理运行的留痕(design/23 §4.1 F:留痕不靠
|
||||
// 会话)。vetoed / veto_reasons 是本实现在设计稿列集之上补的两列:design/23
|
||||
// §9 要求「每轮记否决原因分布」,只落日志就无法查询,故一并入库。
|
||||
const createMemoryTidyRunsTable = `
|
||||
CREATE TABLE IF NOT EXISTS memory_tidy_runs (
|
||||
id TEXT PRIMARY KEY,
|
||||
started_at TEXT NOT NULL,
|
||||
finished_at TEXT NOT NULL DEFAULT '',
|
||||
scanned INTEGER NOT NULL DEFAULT 0,
|
||||
candidates INTEGER NOT NULL DEFAULT 0,
|
||||
applied INTEGER NOT NULL DEFAULT 0,
|
||||
skipped INTEGER NOT NULL DEFAULT 0,
|
||||
errors INTEGER NOT NULL DEFAULT 0,
|
||||
vetoed INTEGER NOT NULL DEFAULT 0,
|
||||
veto_reasons TEXT NOT NULL DEFAULT '{}',
|
||||
rule_version TEXT NOT NULL DEFAULT ''
|
||||
)`
|
||||
|
||||
const createMemoryTidyRunsStartedIndex = `
|
||||
CREATE INDEX IF NOT EXISTS idx_memory_tidy_runs_started_at
|
||||
ON memory_tidy_runs (started_at)`
|
||||
|
||||
// tidyMemoryColumns 是 memory_items 上记忆整理所需的列与前滚 DDL。
|
||||
// 全部带默认值 ⇒ 既有行行为逐字不变(design/23 §10)。
|
||||
var tidyMemoryColumns = []struct {
|
||||
name string
|
||||
ddl string
|
||||
}{
|
||||
{"status", `ALTER TABLE memory_items ADD COLUMN status TEXT NOT NULL DEFAULT 'active'`},
|
||||
{"merged_into", `ALTER TABLE memory_items ADD COLUMN merged_into TEXT NOT NULL DEFAULT ''`},
|
||||
{"merged_from", `ALTER TABLE memory_items ADD COLUMN merged_from TEXT NOT NULL DEFAULT '[]'`},
|
||||
{"merge_count", `ALTER TABLE memory_items ADD COLUMN merge_count INTEGER NOT NULL DEFAULT 1`},
|
||||
{"occurred_at", `ALTER TABLE memory_items ADD COLUMN occurred_at TEXT NOT NULL DEFAULT ''`},
|
||||
{"valid_to", `ALTER TABLE memory_items ADD COLUMN valid_to TEXT NOT NULL DEFAULT ''`},
|
||||
{"expires_at", `ALTER TABLE memory_items ADD COLUMN expires_at TEXT NOT NULL DEFAULT ''`},
|
||||
}
|
||||
|
||||
const createMemoryItemsStatusIndex = `
|
||||
CREATE INDEX IF NOT EXISTS idx_memory_items_status
|
||||
ON memory_items (persona_id, status)`
|
||||
|
||||
// ensureTidySchema 幂等地创建整理所需的两张新表、状态索引,并前滚
|
||||
// memory_items 的整理列。它由 NewStore 调用,因此整理能力在任何
|
||||
// *sql.DB 上都可用(与 memory_items 自身同一口径)。
|
||||
func ensureTidySchema(ctx context.Context, db *sql.DB) error {
|
||||
for _, ddl := range []string{
|
||||
createMemoryTidyCandidatesTable,
|
||||
createMemoryTidyCandidatesStatusIndex,
|
||||
createMemoryTidyRunsTable,
|
||||
createMemoryTidyRunsStartedIndex,
|
||||
createMemoryItemsStatusIndex,
|
||||
} {
|
||||
if _, err := db.ExecContext(ctx, ddl); err != nil {
|
||||
return fmt.Errorf("memory: tidy schema: %w", err)
|
||||
}
|
||||
}
|
||||
return ensureTidyMemoryColumns(ctx, db)
|
||||
}
|
||||
|
||||
// ensureTidyMemoryColumns 把 memory_items 的整理列前滚到既有表上:列缺失时
|
||||
// 补加,已存在时忽略重复列错误。它是 SQLite 特有的(记忆存储基于 SQLite)
|
||||
// 且幂等,与 ensureProjectIDColumn 同一手法。
|
||||
func ensureTidyMemoryColumns(ctx context.Context, db *sql.DB) error {
|
||||
for _, col := range tidyMemoryColumns {
|
||||
if _, err := db.ExecContext(ctx, col.ddl); err != nil {
|
||||
// 该列已存在(由 CREATE TABLE 或上一次运行创建)时报
|
||||
// "duplicate column name: <col>",这是预期路径。
|
||||
if strings.Contains(err.Error(), "duplicate column name") {
|
||||
continue
|
||||
}
|
||||
return fmt.Errorf("memory: roll forward tidied column %s: %w", col.name, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in new issue
Block a user