@rohirik/openltm-core 2.14.0 → 2.14.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/migrations/001_baseline.sql +11 -0
- package/migrations/002_phase2_columns.sql +30 -0
- package/migrations/003_embedding_index.sql +6 -0
- package/migrations/004_graph_reasoning.sql +18 -0
- package/migrations/005_clusters.sql +16 -0
- package/migrations/006_temporal_metadata.sql +19 -0
- package/migrations/007_workspaces.sql +18 -0
- package/migrations/008_add_memory_provenance.sql +28 -0
- package/migrations/009_add_memory_audit.sql +26 -0
- package/migrations/010_memory_embeddings_split.sql +30 -0
- package/migrations/011_add_decay_score_and_archive.sql +25 -0
- package/migrations/012_seed_janitor_settings.sql +11 -0
- package/migrations/013_add_created_by.sql +13 -0
- package/migrations/014_update_audit_op_check.sql +29 -0
- package/migrations/015_add_titles.sql +14 -0
- package/migrations/016_memory_layout.sql +15 -0
- package/migrations/017_node_ui_state.sql +14 -0
- package/migrations/018_relation_semantics.sql +7 -0
- package/migrations/019_user_note.sql +5 -0
- package/migrations/020_fts_coverage.sql +68 -0
- package/migrations/021_cluster_manual_label.sql +5 -0
- package/migrations/022_add_relevance_signal.sql +11 -0
- package/migrations/023_memory_files.sql +27 -0
- package/migrations/024_title_backfill.sql +38 -0
- package/migrations/025_idx_memories_hidden.sql +12 -0
- package/package.json +1 -1
- package/src/__tests__/packaging.test.ts +63 -0
- package/src/paths.ts +14 -2
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
-- Migration 001: baseline — initial schema captured in schema.sql
|
|
2
|
+
-- All core tables (memories, context_items, tags, memory_tags, memory_relations,
|
|
3
|
+
-- memories_fts, settings) are created by schema.sql on fresh installs.
|
|
4
|
+
-- This file records the baseline in the migration log so subsequent migrations
|
|
5
|
+
-- can assume the schema.sql shape exists.
|
|
6
|
+
|
|
7
|
+
-- UP
|
|
8
|
+
-- (no DDL — schema.sql CREATE IF NOT EXISTS handles fresh installs)
|
|
9
|
+
|
|
10
|
+
-- DOWN
|
|
11
|
+
-- (no rollback — dropping core tables would destroy all data)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
-- Migration 002: Phase 2 columns — status, embedding, last_used_at, memory_id
|
|
2
|
+
-- These columns were previously added by hand-rolled ALTER TABLE in shared-db.ts.
|
|
3
|
+
-- Moving them here makes the migration history authoritative.
|
|
4
|
+
|
|
5
|
+
-- UP
|
|
6
|
+
ALTER TABLE memories ADD COLUMN status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active','pending','deprecated','superseded'));
|
|
7
|
+
ALTER TABLE memories ADD COLUMN embedding BLOB;
|
|
8
|
+
ALTER TABLE memories ADD COLUMN last_used_at TEXT NOT NULL DEFAULT (datetime('now'));
|
|
9
|
+
|
|
10
|
+
ALTER TABLE context_items ADD COLUMN memory_id INTEGER REFERENCES memories(id) ON DELETE SET NULL;
|
|
11
|
+
ALTER TABLE context_items ADD COLUMN status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active','pending_promotion','promoted'));
|
|
12
|
+
|
|
13
|
+
CREATE TABLE IF NOT EXISTS settings (
|
|
14
|
+
key TEXT PRIMARY KEY,
|
|
15
|
+
value TEXT NOT NULL,
|
|
16
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
CREATE INDEX IF NOT EXISTS idx_memories_status ON memories(status);
|
|
20
|
+
CREATE INDEX IF NOT EXISTS idx_memories_last_used ON memories(last_used_at);
|
|
21
|
+
|
|
22
|
+
-- DOWN
|
|
23
|
+
-- DROP INDEX IF EXISTS idx_memories_last_used;
|
|
24
|
+
-- DROP INDEX IF EXISTS idx_memories_status;
|
|
25
|
+
-- DROP TABLE IF EXISTS settings;
|
|
26
|
+
-- ALTER TABLE context_items DROP COLUMN status;
|
|
27
|
+
-- ALTER TABLE context_items DROP COLUMN memory_id;
|
|
28
|
+
-- ALTER TABLE memories DROP COLUMN last_used_at;
|
|
29
|
+
-- ALTER TABLE memories DROP COLUMN embedding;
|
|
30
|
+
-- ALTER TABLE memories DROP COLUMN status;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
-- Migration 004: Graph reasoning convenience view
|
|
2
|
+
-- Creates a view of conflicting memory pairs for quick inspection.
|
|
3
|
+
-- No new tables needed — all data already lives in memory_relations.
|
|
4
|
+
|
|
5
|
+
CREATE VIEW IF NOT EXISTS memory_conflict_pairs AS
|
|
6
|
+
SELECT
|
|
7
|
+
r.source_memory_id,
|
|
8
|
+
r.target_memory_id,
|
|
9
|
+
r.relationship_type,
|
|
10
|
+
r.created_at,
|
|
11
|
+
a.content AS source_content,
|
|
12
|
+
b.content AS target_content,
|
|
13
|
+
a.category AS source_category,
|
|
14
|
+
b.category AS target_category
|
|
15
|
+
FROM memory_relations r
|
|
16
|
+
JOIN memories a ON a.id = r.source_memory_id
|
|
17
|
+
JOIN memories b ON b.id = r.target_memory_id
|
|
18
|
+
WHERE r.relationship_type IN ('contradicts', 'supersedes');
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
CREATE TABLE IF NOT EXISTS memory_clusters (
|
|
2
|
+
id TEXT PRIMARY KEY,
|
|
3
|
+
label TEXT NOT NULL,
|
|
4
|
+
color TEXT NOT NULL,
|
|
5
|
+
node_ids TEXT NOT NULL,
|
|
6
|
+
created_at TEXT NOT NULL,
|
|
7
|
+
updated_at TEXT NOT NULL
|
|
8
|
+
);
|
|
9
|
+
|
|
10
|
+
CREATE TABLE IF NOT EXISTS cluster_overrides (
|
|
11
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
12
|
+
cluster_id TEXT NOT NULL,
|
|
13
|
+
action TEXT NOT NULL,
|
|
14
|
+
payload TEXT NOT NULL,
|
|
15
|
+
created_at TEXT NOT NULL
|
|
16
|
+
);
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
-- Migration 006: temporal metadata for memories
|
|
2
|
+
-- UP
|
|
3
|
+
ALTER TABLE memories ADD COLUMN first_recalled_at TEXT;
|
|
4
|
+
ALTER TABLE memories ADD COLUMN last_recalled_at TEXT;
|
|
5
|
+
ALTER TABLE memories ADD COLUMN recall_count INTEGER NOT NULL DEFAULT 0;
|
|
6
|
+
ALTER TABLE memories ADD COLUMN superseded_by INTEGER REFERENCES memories(id) ON DELETE SET NULL;
|
|
7
|
+
ALTER TABLE memories ADD COLUMN superseded_at TEXT;
|
|
8
|
+
|
|
9
|
+
CREATE INDEX IF NOT EXISTS idx_memories_superseded ON memories(superseded_by);
|
|
10
|
+
CREATE INDEX IF NOT EXISTS idx_memories_recall_count ON memories(recall_count DESC);
|
|
11
|
+
|
|
12
|
+
-- DOWN
|
|
13
|
+
-- DROP INDEX IF EXISTS idx_memories_superseded;
|
|
14
|
+
-- DROP INDEX IF EXISTS idx_memories_recall_count;
|
|
15
|
+
-- ALTER TABLE memories DROP COLUMN superseded_at;
|
|
16
|
+
-- ALTER TABLE memories DROP COLUMN superseded_by;
|
|
17
|
+
-- ALTER TABLE memories DROP COLUMN recall_count;
|
|
18
|
+
-- ALTER TABLE memories DROP COLUMN last_recalled_at;
|
|
19
|
+
-- ALTER TABLE memories DROP COLUMN first_recalled_at;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
-- Migration 007: workspace_id + agent_id for multi-agent memory isolation
|
|
2
|
+
-- UP
|
|
3
|
+
ALTER TABLE memories ADD COLUMN workspace_id TEXT;
|
|
4
|
+
ALTER TABLE memories ADD COLUMN agent_id TEXT;
|
|
5
|
+
|
|
6
|
+
CREATE INDEX IF NOT EXISTS idx_memories_workspace ON memories(workspace_id);
|
|
7
|
+
CREATE INDEX IF NOT EXISTS idx_memories_agent ON memories(agent_id);
|
|
8
|
+
|
|
9
|
+
ALTER TABLE context_items ADD COLUMN workspace_id TEXT;
|
|
10
|
+
ALTER TABLE context_items ADD COLUMN agent_id TEXT;
|
|
11
|
+
|
|
12
|
+
-- DOWN
|
|
13
|
+
-- ALTER TABLE context_items DROP COLUMN agent_id;
|
|
14
|
+
-- ALTER TABLE context_items DROP COLUMN workspace_id;
|
|
15
|
+
-- DROP INDEX IF EXISTS idx_memories_agent;
|
|
16
|
+
-- DROP INDEX IF EXISTS idx_memories_workspace;
|
|
17
|
+
-- ALTER TABLE memories DROP COLUMN agent_id;
|
|
18
|
+
-- ALTER TABLE memories DROP COLUMN workspace_id;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
-- Migration 008: memory_provenance table — per-memory source traceability (C5, W9)
|
|
2
|
+
-- One-to-many: a memory may have multiple provenance entries.
|
|
3
|
+
-- memory_id FK cascades on delete so provenance rows clean up with the memory.
|
|
4
|
+
-- Backfill: all pre-existing memories get a single 'legacy' provenance row.
|
|
5
|
+
-- UP
|
|
6
|
+
CREATE TABLE IF NOT EXISTS memory_provenance (
|
|
7
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
8
|
+
memory_id INTEGER NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
|
|
9
|
+
source_type TEXT NOT NULL CHECK(source_type IN (
|
|
10
|
+
'learn','git-backfill','evaluate-session','import-bundle',
|
|
11
|
+
'user-promotion','janitor-rollup','legacy')),
|
|
12
|
+
source_ref TEXT,
|
|
13
|
+
actor TEXT,
|
|
14
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
15
|
+
metadata TEXT
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
CREATE INDEX IF NOT EXISTS idx_provenance_memory ON memory_provenance(memory_id);
|
|
19
|
+
CREATE INDEX IF NOT EXISTS idx_provenance_source_type ON memory_provenance(source_type, created_at DESC);
|
|
20
|
+
|
|
21
|
+
-- Backfill all existing memories with a 'legacy' sentinel row.
|
|
22
|
+
INSERT INTO memory_provenance (memory_id, source_type, actor)
|
|
23
|
+
SELECT id, 'legacy', 'migration-008' FROM memories;
|
|
24
|
+
|
|
25
|
+
-- DOWN
|
|
26
|
+
-- DROP INDEX IF EXISTS idx_provenance_source_type;
|
|
27
|
+
-- DROP INDEX IF EXISTS idx_provenance_memory;
|
|
28
|
+
-- DROP TABLE IF EXISTS memory_provenance;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
-- Migration 009: memory_audit table — append-only write audit log (W11, C5, C9)
|
|
2
|
+
-- memory_id is intentionally NOT a foreign key: audit rows must survive memory deletion.
|
|
3
|
+
-- No backfill — audit is forward-only from this migration onward.
|
|
4
|
+
-- Powers Phase 7 time-travel (C9) and /openltm:admin audit queries.
|
|
5
|
+
-- UP
|
|
6
|
+
CREATE TABLE IF NOT EXISTS memory_audit (
|
|
7
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
8
|
+
memory_id INTEGER NOT NULL,
|
|
9
|
+
op TEXT NOT NULL CHECK(op IN (
|
|
10
|
+
'insert','update','forget','deprecate','supersede','redact','restore')),
|
|
11
|
+
actor TEXT NOT NULL,
|
|
12
|
+
session_id TEXT,
|
|
13
|
+
before_json TEXT,
|
|
14
|
+
after_json TEXT,
|
|
15
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
CREATE INDEX IF NOT EXISTS idx_audit_memory ON memory_audit(memory_id, created_at DESC);
|
|
19
|
+
CREATE INDEX IF NOT EXISTS idx_audit_op ON memory_audit(op, created_at DESC);
|
|
20
|
+
CREATE INDEX IF NOT EXISTS idx_audit_session ON memory_audit(session_id, created_at DESC);
|
|
21
|
+
|
|
22
|
+
-- DOWN
|
|
23
|
+
-- DROP INDEX IF EXISTS idx_audit_session;
|
|
24
|
+
-- DROP INDEX IF EXISTS idx_audit_op;
|
|
25
|
+
-- DROP INDEX IF EXISTS idx_audit_memory;
|
|
26
|
+
-- DROP TABLE IF EXISTS memory_audit;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
-- Migration 010: split memories.embedding blob into memory_embeddings table
|
|
2
|
+
-- SQLite 3.35+ required for ALTER TABLE … DROP COLUMN (Bun ships 3.35+).
|
|
3
|
+
-- Runner wraps this in a transaction — no BEGIN/COMMIT here.
|
|
4
|
+
|
|
5
|
+
CREATE TABLE IF NOT EXISTS memory_embeddings (
|
|
6
|
+
memory_id INTEGER PRIMARY KEY REFERENCES memories(id) ON DELETE CASCADE,
|
|
7
|
+
embedding BLOB NOT NULL,
|
|
8
|
+
model TEXT NOT NULL DEFAULT 'unknown',
|
|
9
|
+
dim INTEGER NOT NULL DEFAULT 0,
|
|
10
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
CREATE INDEX IF NOT EXISTS idx_embeddings_memory ON memory_embeddings(memory_id);
|
|
14
|
+
|
|
15
|
+
-- Copy existing non-null blobs before dropping the column
|
|
16
|
+
INSERT OR IGNORE INTO memory_embeddings (memory_id, embedding, model, dim)
|
|
17
|
+
SELECT id, embedding, 'unknown', 0
|
|
18
|
+
FROM memories
|
|
19
|
+
WHERE embedding IS NOT NULL;
|
|
20
|
+
|
|
21
|
+
-- Drop the partial index from migration 003 (references embedding column — must drop before DROP COLUMN)
|
|
22
|
+
DROP INDEX IF EXISTS idx_memories_embedding;
|
|
23
|
+
|
|
24
|
+
-- Drop the inline blob column now that data is in the side table
|
|
25
|
+
ALTER TABLE memories DROP COLUMN embedding;
|
|
26
|
+
|
|
27
|
+
-- DOWN
|
|
28
|
+
DROP INDEX IF EXISTS idx_embeddings_memory;
|
|
29
|
+
DROP TABLE IF EXISTS memory_embeddings;
|
|
30
|
+
ALTER TABLE memories ADD COLUMN embedding BLOB;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
-- Migration 011: materialised decay_score column + memory_archive table
|
|
2
|
+
-- Runner wraps this in a transaction — no BEGIN/COMMIT here.
|
|
3
|
+
|
|
4
|
+
-- Add pre-computed decay_score to memories (defaults to 1.0; janitor refreshes on first run)
|
|
5
|
+
ALTER TABLE memories ADD COLUMN decay_score REAL NOT NULL DEFAULT 1.0;
|
|
6
|
+
CREATE INDEX IF NOT EXISTS idx_memories_decay_score ON memories (decay_score DESC);
|
|
7
|
+
|
|
8
|
+
-- Archive table for evicted deprecated memories (snapshot, not live FK)
|
|
9
|
+
CREATE TABLE IF NOT EXISTS memory_archive (
|
|
10
|
+
id INTEGER PRIMARY KEY,
|
|
11
|
+
memory_json TEXT NOT NULL,
|
|
12
|
+
archived_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
13
|
+
reason TEXT NOT NULL CHECK(reason IN ('decay','rollup','manual')),
|
|
14
|
+
decay_score REAL,
|
|
15
|
+
project_scope TEXT
|
|
16
|
+
);
|
|
17
|
+
CREATE INDEX IF NOT EXISTS idx_archive_project ON memory_archive (project_scope);
|
|
18
|
+
CREATE INDEX IF NOT EXISTS idx_archive_reason ON memory_archive (reason, archived_at DESC);
|
|
19
|
+
|
|
20
|
+
-- DOWN
|
|
21
|
+
DROP INDEX IF EXISTS idx_archive_reason;
|
|
22
|
+
DROP INDEX IF EXISTS idx_archive_project;
|
|
23
|
+
DROP TABLE IF EXISTS memory_archive;
|
|
24
|
+
DROP INDEX IF EXISTS idx_memories_decay_score;
|
|
25
|
+
ALTER TABLE memories DROP COLUMN decay_score;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
-- Migration 012: seed janitor run-tracking settings
|
|
2
|
+
-- INSERT OR IGNORE — safe on existing DBs.
|
|
3
|
+
-- Runner wraps this in a transaction — no BEGIN/COMMIT here.
|
|
4
|
+
|
|
5
|
+
INSERT OR IGNORE INTO settings (key, value, updated_at) VALUES
|
|
6
|
+
('ltm.janitor.lastRunAt', '', datetime('now')),
|
|
7
|
+
('ltm.janitor.lastDecayRefreshed', '0', datetime('now')),
|
|
8
|
+
('ltm.janitor.lastDeprecated', '0', datetime('now')),
|
|
9
|
+
('ltm.janitor.lastArchived', '0', datetime('now'));
|
|
10
|
+
|
|
11
|
+
-- DOWN (no-op: removing settings rows is safe, they'll just re-seed on next migration run)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
-- Migration 013: add created_by column to memories
|
|
2
|
+
-- Backfills from the earliest memory_provenance.actor for each memory.
|
|
3
|
+
ALTER TABLE memories ADD COLUMN created_by TEXT;
|
|
4
|
+
|
|
5
|
+
UPDATE memories
|
|
6
|
+
SET created_by = (
|
|
7
|
+
SELECT actor
|
|
8
|
+
FROM memory_provenance
|
|
9
|
+
WHERE memory_provenance.memory_id = memories.id
|
|
10
|
+
ORDER BY created_at ASC
|
|
11
|
+
LIMIT 1
|
|
12
|
+
)
|
|
13
|
+
WHERE created_by IS NULL;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
-- Migration 014: rebuild memory_audit with updated CHECK constraint to include 'archive' op.
|
|
2
|
+
-- SQLite cannot ALTER a CHECK constraint — must recreate the table.
|
|
3
|
+
-- UP
|
|
4
|
+
PRAGMA foreign_keys=OFF;
|
|
5
|
+
|
|
6
|
+
CREATE TABLE memory_audit_new (
|
|
7
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
8
|
+
memory_id INTEGER NOT NULL,
|
|
9
|
+
op TEXT NOT NULL CHECK(op IN (
|
|
10
|
+
'insert','update','forget','deprecate','supersede','redact','restore','archive')),
|
|
11
|
+
actor TEXT NOT NULL,
|
|
12
|
+
session_id TEXT,
|
|
13
|
+
before_json TEXT,
|
|
14
|
+
after_json TEXT,
|
|
15
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
INSERT INTO memory_audit_new SELECT * FROM memory_audit;
|
|
19
|
+
DROP TABLE memory_audit;
|
|
20
|
+
ALTER TABLE memory_audit_new RENAME TO memory_audit;
|
|
21
|
+
|
|
22
|
+
CREATE INDEX IF NOT EXISTS idx_audit_memory ON memory_audit(memory_id, created_at DESC);
|
|
23
|
+
CREATE INDEX IF NOT EXISTS idx_audit_op ON memory_audit(op, created_at DESC);
|
|
24
|
+
CREATE INDEX IF NOT EXISTS idx_audit_session ON memory_audit(session_id, created_at DESC);
|
|
25
|
+
|
|
26
|
+
PRAGMA foreign_keys=ON;
|
|
27
|
+
|
|
28
|
+
-- DOWN
|
|
29
|
+
-- (reversing would require removing 'archive' from CHECK — not worth automating)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
-- Migration 015: add title columns to memories and context_items (DDL only)
|
|
2
|
+
-- Title is a short human-readable label (≤60 chars).
|
|
3
|
+
--
|
|
4
|
+
-- Pure-DDL, isolated from the value backfill so this step is self-heal eligible
|
|
5
|
+
-- on a fresh install: schema.sql already pre-bakes `title`, so the ALTERs
|
|
6
|
+
-- collide and the four-form DDL self-heal gate records the version once it
|
|
7
|
+
-- proves both columns exist. On a legacy pre-015 install the columns are
|
|
8
|
+
-- genuinely missing, so the ALTERs succeed and add them here.
|
|
9
|
+
--
|
|
10
|
+
-- The data-changing backfill (UPDATE ... SET title FROM content) lives in
|
|
11
|
+
-- 024_title_backfill.sql with its own status gate and idempotency check.
|
|
12
|
+
|
|
13
|
+
ALTER TABLE memories ADD COLUMN title TEXT;
|
|
14
|
+
ALTER TABLE context_items ADD COLUMN title TEXT;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
-- Migration 016: per-view layout persistence
|
|
2
|
+
-- Positions are saved only when the user explicitly drags or pins a node.
|
|
3
|
+
-- 'view' = 'global' for the global graph, or a project name for drill-down.
|
|
4
|
+
|
|
5
|
+
CREATE TABLE IF NOT EXISTS memory_layout (
|
|
6
|
+
memory_id INTEGER NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
|
|
7
|
+
view TEXT NOT NULL,
|
|
8
|
+
x REAL NOT NULL,
|
|
9
|
+
y REAL NOT NULL,
|
|
10
|
+
pinned INTEGER NOT NULL DEFAULT 0,
|
|
11
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
12
|
+
PRIMARY KEY (memory_id, view)
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
CREATE INDEX IF NOT EXISTS idx_layout_view ON memory_layout(view);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
-- Migration 017: per-node UI state columns (DDL only)
|
|
2
|
+
-- hidden: excluded from /api/graph unless ?includeHidden=1
|
|
3
|
+
-- color: manual override; NULL means use category color
|
|
4
|
+
-- icon: optional emoji/icon key
|
|
5
|
+
--
|
|
6
|
+
-- Pure DDL, isolated from the supporting index: the four ALTER statements
|
|
7
|
+
-- self-heal cleanly on a fresh install (schema.sql pre-bakes the columns).
|
|
8
|
+
-- The partial index that backs `hidden=1` lookups lives in
|
|
9
|
+
-- 025_idx_memories_hidden.sql with its own status gate, so on fresh installs
|
|
10
|
+
-- the column step never fails just because the index target is still missing.
|
|
11
|
+
|
|
12
|
+
ALTER TABLE memories ADD COLUMN hidden INTEGER NOT NULL DEFAULT 0;
|
|
13
|
+
ALTER TABLE memories ADD COLUMN color TEXT;
|
|
14
|
+
ALTER TABLE memories ADD COLUMN icon TEXT;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
-- Migration 018: edge semantics — rationale note + base weight
|
|
2
|
+
-- Effective weight = base × decay, computed at read time (never stored).
|
|
3
|
+
-- Manual edges default to 1.0; LLM-inferred edges use confidence-derived weight.
|
|
4
|
+
|
|
5
|
+
ALTER TABLE memory_relations ADD COLUMN note TEXT;
|
|
6
|
+
ALTER TABLE memory_relations ADD COLUMN weight REAL NOT NULL DEFAULT 1.0
|
|
7
|
+
CHECK(weight BETWEEN 0.0 AND 1.0);
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
-- Migration 020: extend FTS coverage to include titles and context_items
|
|
2
|
+
-- Drops and recreates memories_fts to add the title column.
|
|
3
|
+
-- Adds context_items_fts for in-graph search coverage.
|
|
4
|
+
|
|
5
|
+
-- ── memories_fts rebuild ──────────────────────────────────────────────────────
|
|
6
|
+
|
|
7
|
+
DROP TABLE IF EXISTS memories_fts;
|
|
8
|
+
|
|
9
|
+
CREATE VIRTUAL TABLE memories_fts USING fts5(
|
|
10
|
+
title,
|
|
11
|
+
content,
|
|
12
|
+
content='memories',
|
|
13
|
+
content_rowid='id'
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
-- Repopulate from base table (title may be NULL for very old rows — coalesce to '').
|
|
17
|
+
INSERT INTO memories_fts(rowid, title, content)
|
|
18
|
+
SELECT id, coalesce(title, ''), content FROM memories;
|
|
19
|
+
|
|
20
|
+
DROP TRIGGER IF EXISTS memories_ai;
|
|
21
|
+
DROP TRIGGER IF EXISTS memories_ad;
|
|
22
|
+
DROP TRIGGER IF EXISTS memories_au;
|
|
23
|
+
|
|
24
|
+
CREATE TRIGGER memories_ai AFTER INSERT ON memories BEGIN
|
|
25
|
+
INSERT INTO memories_fts(rowid, title, content)
|
|
26
|
+
VALUES (new.id, coalesce(new.title, ''), new.content);
|
|
27
|
+
END;
|
|
28
|
+
|
|
29
|
+
CREATE TRIGGER memories_ad AFTER DELETE ON memories BEGIN
|
|
30
|
+
INSERT INTO memories_fts(memories_fts, rowid, title, content)
|
|
31
|
+
VALUES ('delete', old.id, coalesce(old.title, ''), old.content);
|
|
32
|
+
END;
|
|
33
|
+
|
|
34
|
+
CREATE TRIGGER memories_au AFTER UPDATE ON memories BEGIN
|
|
35
|
+
INSERT INTO memories_fts(memories_fts, rowid, title, content)
|
|
36
|
+
VALUES ('delete', old.id, coalesce(old.title, ''), old.content);
|
|
37
|
+
INSERT INTO memories_fts(rowid, title, content)
|
|
38
|
+
VALUES (new.id, coalesce(new.title, ''), new.content);
|
|
39
|
+
END;
|
|
40
|
+
|
|
41
|
+
-- ── context_items_fts ─────────────────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS context_items_fts USING fts5(
|
|
44
|
+
title,
|
|
45
|
+
content,
|
|
46
|
+
content='context_items',
|
|
47
|
+
content_rowid='id'
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
INSERT OR IGNORE INTO context_items_fts(rowid, title, content)
|
|
51
|
+
SELECT id, coalesce(title, ''), content FROM context_items;
|
|
52
|
+
|
|
53
|
+
CREATE TRIGGER IF NOT EXISTS context_items_ai AFTER INSERT ON context_items BEGIN
|
|
54
|
+
INSERT INTO context_items_fts(rowid, title, content)
|
|
55
|
+
VALUES (new.id, coalesce(new.title, ''), new.content);
|
|
56
|
+
END;
|
|
57
|
+
|
|
58
|
+
CREATE TRIGGER IF NOT EXISTS context_items_ad AFTER DELETE ON context_items BEGIN
|
|
59
|
+
INSERT INTO context_items_fts(context_items_fts, rowid, title, content)
|
|
60
|
+
VALUES ('delete', old.id, coalesce(old.title, ''), old.content);
|
|
61
|
+
END;
|
|
62
|
+
|
|
63
|
+
CREATE TRIGGER IF NOT EXISTS context_items_au AFTER UPDATE ON context_items BEGIN
|
|
64
|
+
INSERT INTO context_items_fts(context_items_fts, rowid, title, content)
|
|
65
|
+
VALUES ('delete', old.id, coalesce(old.title, ''), old.content);
|
|
66
|
+
INSERT INTO context_items_fts(rowid, title, content)
|
|
67
|
+
VALUES (new.id, coalesce(new.title, ''), new.content);
|
|
68
|
+
END;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
-- Migration 021: manual_label flag on memory_clusters
|
|
2
|
+
-- When 1, the user has manually renamed this cluster.
|
|
3
|
+
-- The recompute logic preserves manual labels by matching via sorted node_ids hash.
|
|
4
|
+
|
|
5
|
+
ALTER TABLE memory_clusters ADD COLUMN manual_label INTEGER NOT NULL DEFAULT 0;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
-- Migration 022: personal relevance signal on memories
|
|
2
|
+
-- Lets a user mark a memory as "works for me" / "doesn't work for me".
|
|
3
|
+
-- Human-authored, like user_note — never touched by learn() or the janitor.
|
|
4
|
+
-- relevance_signal is one of 'works' | 'doesnt' (NULL = unrated).
|
|
5
|
+
|
|
6
|
+
ALTER TABLE memories ADD COLUMN relevance_signal TEXT;
|
|
7
|
+
ALTER TABLE memories ADD COLUMN relevance_signal_at TEXT;
|
|
8
|
+
|
|
9
|
+
-- DOWN
|
|
10
|
+
ALTER TABLE memories DROP COLUMN relevance_signal;
|
|
11
|
+
ALTER TABLE memories DROP COLUMN relevance_signal_at;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
-- Migration 023: code-anchored invalidation — memory_files join + staleness columns
|
|
2
|
+
-- Runner wraps this in a transaction — no BEGIN/COMMIT here.
|
|
3
|
+
-- Anchors memories to the repo files they reference, so a commit touching those
|
|
4
|
+
-- files can flag the anchored memories stale (see flagStaleByPaths / GitCommit hook).
|
|
5
|
+
|
|
6
|
+
-- Many-to-many: a memory ↔ the repo-relative paths it references.
|
|
7
|
+
CREATE TABLE IF NOT EXISTS memory_files (
|
|
8
|
+
memory_id INTEGER NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
|
|
9
|
+
path TEXT NOT NULL, -- normalised repo-relative path
|
|
10
|
+
project_scope TEXT, -- NULL = global, else project name (cross-repo safety)
|
|
11
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
12
|
+
PRIMARY KEY (memory_id, path)
|
|
13
|
+
);
|
|
14
|
+
CREATE INDEX IF NOT EXISTS idx_memory_files_path ON memory_files(path);
|
|
15
|
+
|
|
16
|
+
-- Staleness flag (set by code-change invalidation; cleared on re-confirm/revalidate).
|
|
17
|
+
-- NULL = not flagged. Distinct from decay/supersession — driven by code change, not recall.
|
|
18
|
+
ALTER TABLE memories ADD COLUMN stale_flagged_at TEXT;
|
|
19
|
+
ALTER TABLE memories ADD COLUMN stale_reason TEXT;
|
|
20
|
+
CREATE INDEX IF NOT EXISTS idx_memories_stale ON memories(stale_flagged_at);
|
|
21
|
+
|
|
22
|
+
-- DOWN
|
|
23
|
+
DROP INDEX IF EXISTS idx_memories_stale;
|
|
24
|
+
ALTER TABLE memories DROP COLUMN stale_reason;
|
|
25
|
+
ALTER TABLE memories DROP COLUMN stale_flagged_at;
|
|
26
|
+
DROP INDEX IF EXISTS idx_memory_files_path;
|
|
27
|
+
DROP TABLE IF EXISTS memory_files;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
-- Migration 024: backfill title values from content (data-changing, own gate)
|
|
2
|
+
-- Populates title for rows that predate the title column (legacy pre-015
|
|
3
|
+
-- installs). Kept in its own migration so the DDL step (015) stays pure-DDL
|
|
4
|
+
-- and self-heal eligible; this step is its own status-gated unit like any
|
|
5
|
+
-- migration, and the runner's checksum is recorded as usual.
|
|
6
|
+
--
|
|
7
|
+
-- Idempotent by construction: only rows WHERE title IS NULL are touched, so
|
|
8
|
+
-- re-running this migration (or the runner) is a no-op on already-backfilled
|
|
9
|
+
-- rows. No destructiveness, no rename — fail-closed runner accepts it as a
|
|
10
|
+
-- data-changing step that never needs self-heal because the columns it reads
|
|
11
|
+
-- (content, title) exist for both fresh (pre-baked or added by 015) and legacy
|
|
12
|
+
-- installs by the time this version runs (015 < 016 < ... < 024).
|
|
13
|
+
|
|
14
|
+
-- Backfill memories: first sentence (up to '.', '!', '?', or newline), else first 57 chars + ellipsis.
|
|
15
|
+
UPDATE memories SET title =
|
|
16
|
+
CASE
|
|
17
|
+
WHEN instr(content, '.') > 1 AND instr(content, '.') <= 61
|
|
18
|
+
THEN trim(substr(content, 1, instr(content, '.') - 1))
|
|
19
|
+
WHEN instr(content, char(10)) > 1 AND instr(content, char(10)) <= 61
|
|
20
|
+
THEN trim(substr(content, 1, instr(content, char(10)) - 1))
|
|
21
|
+
WHEN length(trim(content)) <= 60
|
|
22
|
+
THEN trim(content)
|
|
23
|
+
ELSE substr(trim(content), 1, 57) || '…'
|
|
24
|
+
END
|
|
25
|
+
WHERE title IS NULL;
|
|
26
|
+
|
|
27
|
+
-- Backfill context_items with same heuristic.
|
|
28
|
+
UPDATE context_items SET title =
|
|
29
|
+
CASE
|
|
30
|
+
WHEN instr(content, '.') > 1 AND instr(content, '.') <= 61
|
|
31
|
+
THEN trim(substr(content, 1, instr(content, '.') - 1))
|
|
32
|
+
WHEN instr(content, char(10)) > 1 AND instr(content, char(10)) <= 61
|
|
33
|
+
THEN trim(substr(content, 1, instr(content, char(10)) - 1))
|
|
34
|
+
WHEN length(trim(content)) <= 60
|
|
35
|
+
THEN trim(content)
|
|
36
|
+
ELSE substr(trim(content), 1, 57) || '…'
|
|
37
|
+
END
|
|
38
|
+
WHERE title IS NULL;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
-- Migration 025: partial index on memories(hidden) (pure DDL, idempotent)
|
|
2
|
+
-- Supports the /api/graph exclude-hidden filter. Split out of 017 so that
|
|
3
|
+
-- migration stays pure-DDL and self-heal eligible: on a fresh install
|
|
4
|
+
-- schema.sql pre-bakes hidden/color/icon but NOT this index, so the gate
|
|
5
|
+
-- would otherwise refuse 017 because the index target doesn't exist yet.
|
|
6
|
+
--
|
|
7
|
+
-- Own status-gated unit. `CREATE INDEX IF NOT EXISTS` is natively idempotent
|
|
8
|
+
-- in SQLite, so it succeeds identically whether the index already exists
|
|
9
|
+
-- (legacy install that ran the old 017) or not (fresh install that only just
|
|
10
|
+
-- added the column). Pure DDL, indistinguishable to the fail-closed runner.
|
|
11
|
+
|
|
12
|
+
CREATE INDEX IF NOT EXISTS idx_memories_hidden ON memories(hidden) WHERE hidden = 1;
|
package/package.json
CHANGED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Packaging tests.
|
|
3
|
+
*
|
|
4
|
+
* The published npm tarball is the install path the README advertises, so a
|
|
5
|
+
* missing runtime file is a shipping bug — not a cosmetic one. These tests
|
|
6
|
+
* guard the files that must be inside the tarball and keep the duplicated
|
|
7
|
+
* migration copies from drifting.
|
|
8
|
+
*/
|
|
9
|
+
import { describe, expect, it } from "bun:test";
|
|
10
|
+
import { existsSync, readdirSync, readFileSync } from "fs";
|
|
11
|
+
import { join } from "path";
|
|
12
|
+
|
|
13
|
+
const REPO_ROOT = join(import.meta.dir, "..", "..", "..", "..");
|
|
14
|
+
const CORE_ROOT = join(REPO_ROOT, "packages", "openltm-core");
|
|
15
|
+
|
|
16
|
+
describe("migration files are shipped with the package", () => {
|
|
17
|
+
it("keeps a package-local copy of every root migration", () => {
|
|
18
|
+
const rootDir = join(REPO_ROOT, "migrations");
|
|
19
|
+
const pkgDir = join(CORE_ROOT, "migrations");
|
|
20
|
+
|
|
21
|
+
const rootFiles = readdirSync(rootDir).filter((f) => f.endsWith(".sql")).sort();
|
|
22
|
+
const pkgFiles = readdirSync(pkgDir).filter((f) => f.endsWith(".sql")).sort();
|
|
23
|
+
|
|
24
|
+
expect(pkgFiles).toEqual(rootFiles);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("keeps the two migration copies byte-identical", () => {
|
|
28
|
+
for (const file of readdirSync(join(REPO_ROOT, "migrations")).filter((f) => f.endsWith(".sql"))) {
|
|
29
|
+
const root = readFileSync(join(REPO_ROOT, "migrations", file), "utf-8");
|
|
30
|
+
const packaged = readFileSync(join(CORE_ROOT, "migrations", file), "utf-8");
|
|
31
|
+
expect(`${file}:${packaged === root}`).toBe(`${file}:true`);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("resolves the packaged copy first, not the monorepo root", async () => {
|
|
36
|
+
const { getMigrationsDir } = await import("../paths.js");
|
|
37
|
+
const resolved = getMigrationsDir();
|
|
38
|
+
expect(existsSync(resolved)).toBe(true);
|
|
39
|
+
expect(resolved).toBe(join(CORE_ROOT, "migrations"));
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe("package manifest includes runtime files", () => {
|
|
44
|
+
it("does not exclude migrations from the published tarball", async () => {
|
|
45
|
+
const pkg = JSON.parse(readFileSync(join(CORE_ROOT, "package.json"), "utf-8"));
|
|
46
|
+
const files = pkg.files as string[] | undefined;
|
|
47
|
+
// An explicit `files` allowlist that omits migrations/ would reintroduce the
|
|
48
|
+
// bug this suite exists to prevent.
|
|
49
|
+
if (files) {
|
|
50
|
+
expect(files.some((entry) => entry.includes("migrations"))).toBe(true);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("exposes the same runtime entrypoints npm consumers need", () => {
|
|
55
|
+
const pkg = JSON.parse(readFileSync(join(CORE_ROOT, "package.json"), "utf-8"));
|
|
56
|
+
for (const entry of [".", "./cli", "./mcp"]) {
|
|
57
|
+
expect(Object.keys(pkg.exports)).toContain(entry);
|
|
58
|
+
}
|
|
59
|
+
for (const bin of ["ltm", "openltm", "openltm-core"]) {
|
|
60
|
+
expect(Object.keys(pkg.bin)).toContain(bin);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
});
|
package/src/paths.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* The CLAUDE_DIR constant is intentionally absent — adapters inject paths via LtmCoreConfig.
|
|
5
5
|
*/
|
|
6
6
|
import { join } from "path";
|
|
7
|
+
import { existsSync } from "fs";
|
|
7
8
|
|
|
8
9
|
export function getDbPath(): string {
|
|
9
10
|
if (process.env["LTM_DB_PATH"]) return process.env["LTM_DB_PATH"];
|
|
@@ -15,8 +16,19 @@ export function getSchemaPath(): string {
|
|
|
15
16
|
return join(import.meta.dir, "schema.sql");
|
|
16
17
|
}
|
|
17
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Locate the versioned SQL migrations.
|
|
21
|
+
*
|
|
22
|
+
* Order matters: the package-local `migrations/` copy is checked FIRST because
|
|
23
|
+
* it is the one that ships inside the published npm tarball. The monorepo-root
|
|
24
|
+
* path is only the development fallback. A published install that resolved to
|
|
25
|
+
* the root path would silently find zero migrations and hand back a
|
|
26
|
+
* column-incomplete database (missing `decay_score`, `workspace_id`, …).
|
|
27
|
+
*/
|
|
18
28
|
export function getMigrationsDir(): string {
|
|
19
|
-
|
|
20
|
-
|
|
29
|
+
const packaged = join(import.meta.dir, "..", "migrations");
|
|
30
|
+
if (existsSync(packaged)) return packaged;
|
|
31
|
+
|
|
32
|
+
// Development inside the monorepo.
|
|
21
33
|
return join(import.meta.dir, "..", "..", "..", "migrations");
|
|
22
34
|
}
|