Files
Cyrene_v2/internal/runtime/memory/tidy_schema.go
T
AskaEth 91a3209c9b
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
feat(memory): 记忆整理 v1(design/23)——低峰定期扫描 + 事实包含判定 + 规则否决 + 只产建议队列(不自动应用)
2026-10-05 11:29:36 +08:00

123 lines
5.2 KiB
Go

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
}