hippo-memory 1.14.0 → 1.16.0
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/README.md +862 -861
- package/dist/audit.d.ts +1 -1
- package/dist/audit.d.ts.map +1 -1
- package/dist/audit.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +1594 -229
- package/dist/cli.js.map +1 -1
- package/dist/customer-notes.d.ts +95 -0
- package/dist/customer-notes.d.ts.map +1 -0
- package/dist/customer-notes.js +296 -0
- package/dist/customer-notes.js.map +1 -0
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +1286 -472
- package/dist/db.js.map +1 -1
- package/dist/decisions.d.ts +91 -0
- package/dist/decisions.d.ts.map +1 -0
- package/dist/decisions.js +278 -0
- package/dist/decisions.js.map +1 -0
- package/dist/graph-extract.d.ts +39 -0
- package/dist/graph-extract.d.ts.map +1 -0
- package/dist/graph-extract.js +141 -0
- package/dist/graph-extract.js.map +1 -0
- package/dist/graph-recall.d.ts +41 -0
- package/dist/graph-recall.d.ts.map +1 -0
- package/dist/graph-recall.js +246 -0
- package/dist/graph-recall.js.map +1 -0
- package/dist/graph.d.ts +137 -0
- package/dist/graph.d.ts.map +1 -0
- package/dist/graph.js +433 -0
- package/dist/graph.js.map +1 -0
- package/dist/incidents.d.ts +100 -0
- package/dist/incidents.d.ts.map +1 -0
- package/dist/incidents.js +322 -0
- package/dist/incidents.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/memory.d.ts +6 -0
- package/dist/memory.d.ts.map +1 -1
- package/dist/memory.js +6 -0
- package/dist/memory.js.map +1 -1
- package/dist/policies.d.ts +149 -0
- package/dist/policies.d.ts.map +1 -0
- package/dist/policies.js +380 -0
- package/dist/policies.js.map +1 -0
- package/dist/processes.d.ts +104 -0
- package/dist/processes.d.ts.map +1 -0
- package/dist/processes.js +330 -0
- package/dist/processes.js.map +1 -0
- package/dist/project-briefs.d.ts +126 -0
- package/dist/project-briefs.d.ts.map +1 -0
- package/dist/project-briefs.js +453 -0
- package/dist/project-briefs.js.map +1 -0
- package/dist/search.d.ts +7 -0
- package/dist/search.d.ts.map +1 -1
- package/dist/search.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +1181 -8
- package/dist/server.js.map +1 -1
- package/dist/skills.d.ts +98 -0
- package/dist/skills.d.ts.map +1 -0
- package/dist/skills.js +339 -0
- package/dist/skills.js.map +1 -0
- package/dist/src/audit.js.map +1 -1
- package/dist/src/cli.js +1594 -229
- package/dist/src/cli.js.map +1 -1
- package/dist/src/customer-notes.js +296 -0
- package/dist/src/customer-notes.js.map +1 -0
- package/dist/src/db.js +1286 -472
- package/dist/src/db.js.map +1 -1
- package/dist/src/decisions.js +278 -0
- package/dist/src/decisions.js.map +1 -0
- package/dist/src/graph-extract.js +141 -0
- package/dist/src/graph-extract.js.map +1 -0
- package/dist/src/graph-recall.js +246 -0
- package/dist/src/graph-recall.js.map +1 -0
- package/dist/src/graph.js +433 -0
- package/dist/src/graph.js.map +1 -0
- package/dist/src/incidents.js +322 -0
- package/dist/src/incidents.js.map +1 -0
- package/dist/src/index.js +1 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/memory.js +6 -0
- package/dist/src/memory.js.map +1 -1
- package/dist/src/policies.js +380 -0
- package/dist/src/policies.js.map +1 -0
- package/dist/src/processes.js +330 -0
- package/dist/src/processes.js.map +1 -0
- package/dist/src/project-briefs.js +453 -0
- package/dist/src/project-briefs.js.map +1 -0
- package/dist/src/search.js.map +1 -1
- package/dist/src/server.js +1181 -8
- package/dist/src/server.js.map +1 -1
- package/dist/src/skills.js +339 -0
- package/dist/src/skills.js.map +1 -0
- package/dist/src/version.js +1 -1
- package/dist/src/version.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/extensions/openclaw-plugin/openclaw.plugin.json +46 -46
- package/extensions/openclaw-plugin/package.json +14 -14
- package/openclaw.plugin.json +45 -45
- package/package.json +75 -75
package/dist/src/db.js
CHANGED
|
@@ -6,83 +6,83 @@ import { cleanupArchivedMirrors } from './raw-archive-mirror-cleanup.js';
|
|
|
6
6
|
import { PACKAGE_VERSION, compareSemver } from './version.js';
|
|
7
7
|
const require = createRequire(import.meta.url);
|
|
8
8
|
const { DatabaseSync } = require('node:sqlite');
|
|
9
|
-
const CURRENT_SCHEMA_VERSION =
|
|
9
|
+
const CURRENT_SCHEMA_VERSION = 37;
|
|
10
10
|
const MIGRATIONS = [
|
|
11
11
|
{
|
|
12
12
|
version: 1,
|
|
13
13
|
up: (db) => {
|
|
14
|
-
db.exec(`
|
|
15
|
-
CREATE TABLE IF NOT EXISTS memories (
|
|
16
|
-
id TEXT PRIMARY KEY,
|
|
17
|
-
created TEXT NOT NULL,
|
|
18
|
-
last_retrieved TEXT NOT NULL,
|
|
19
|
-
retrieval_count INTEGER NOT NULL,
|
|
20
|
-
strength REAL NOT NULL,
|
|
21
|
-
half_life_days REAL NOT NULL,
|
|
22
|
-
layer TEXT NOT NULL,
|
|
23
|
-
tags_json TEXT NOT NULL,
|
|
24
|
-
emotional_valence TEXT NOT NULL,
|
|
25
|
-
schema_fit REAL NOT NULL,
|
|
26
|
-
source TEXT NOT NULL,
|
|
27
|
-
outcome_score REAL,
|
|
28
|
-
conflicts_with_json TEXT NOT NULL,
|
|
29
|
-
pinned INTEGER NOT NULL,
|
|
30
|
-
confidence TEXT NOT NULL,
|
|
31
|
-
content TEXT NOT NULL,
|
|
32
|
-
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
33
|
-
);
|
|
34
|
-
|
|
35
|
-
CREATE INDEX IF NOT EXISTS idx_memories_layer_created ON memories(layer, created);
|
|
36
|
-
CREATE INDEX IF NOT EXISTS idx_memories_last_retrieved ON memories(last_retrieved);
|
|
37
|
-
|
|
38
|
-
CREATE TABLE IF NOT EXISTS consolidation_runs (
|
|
39
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
40
|
-
timestamp TEXT NOT NULL,
|
|
41
|
-
decayed INTEGER NOT NULL,
|
|
42
|
-
merged INTEGER NOT NULL,
|
|
43
|
-
removed INTEGER NOT NULL
|
|
44
|
-
);
|
|
14
|
+
db.exec(`
|
|
15
|
+
CREATE TABLE IF NOT EXISTS memories (
|
|
16
|
+
id TEXT PRIMARY KEY,
|
|
17
|
+
created TEXT NOT NULL,
|
|
18
|
+
last_retrieved TEXT NOT NULL,
|
|
19
|
+
retrieval_count INTEGER NOT NULL,
|
|
20
|
+
strength REAL NOT NULL,
|
|
21
|
+
half_life_days REAL NOT NULL,
|
|
22
|
+
layer TEXT NOT NULL,
|
|
23
|
+
tags_json TEXT NOT NULL,
|
|
24
|
+
emotional_valence TEXT NOT NULL,
|
|
25
|
+
schema_fit REAL NOT NULL,
|
|
26
|
+
source TEXT NOT NULL,
|
|
27
|
+
outcome_score REAL,
|
|
28
|
+
conflicts_with_json TEXT NOT NULL,
|
|
29
|
+
pinned INTEGER NOT NULL,
|
|
30
|
+
confidence TEXT NOT NULL,
|
|
31
|
+
content TEXT NOT NULL,
|
|
32
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
CREATE INDEX IF NOT EXISTS idx_memories_layer_created ON memories(layer, created);
|
|
36
|
+
CREATE INDEX IF NOT EXISTS idx_memories_last_retrieved ON memories(last_retrieved);
|
|
37
|
+
|
|
38
|
+
CREATE TABLE IF NOT EXISTS consolidation_runs (
|
|
39
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
40
|
+
timestamp TEXT NOT NULL,
|
|
41
|
+
decayed INTEGER NOT NULL,
|
|
42
|
+
merged INTEGER NOT NULL,
|
|
43
|
+
removed INTEGER NOT NULL
|
|
44
|
+
);
|
|
45
45
|
`);
|
|
46
46
|
},
|
|
47
47
|
},
|
|
48
48
|
{
|
|
49
49
|
version: 2,
|
|
50
50
|
up: (db) => {
|
|
51
|
-
db.exec(`
|
|
52
|
-
CREATE TABLE IF NOT EXISTS task_snapshots (
|
|
53
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
54
|
-
task TEXT NOT NULL,
|
|
55
|
-
summary TEXT NOT NULL,
|
|
56
|
-
next_step TEXT NOT NULL,
|
|
57
|
-
status TEXT NOT NULL,
|
|
58
|
-
source TEXT NOT NULL,
|
|
59
|
-
created_at TEXT NOT NULL,
|
|
60
|
-
updated_at TEXT NOT NULL
|
|
61
|
-
);
|
|
62
|
-
|
|
63
|
-
CREATE INDEX IF NOT EXISTS idx_task_snapshots_status_updated
|
|
64
|
-
ON task_snapshots(status, updated_at DESC, id DESC);
|
|
51
|
+
db.exec(`
|
|
52
|
+
CREATE TABLE IF NOT EXISTS task_snapshots (
|
|
53
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
54
|
+
task TEXT NOT NULL,
|
|
55
|
+
summary TEXT NOT NULL,
|
|
56
|
+
next_step TEXT NOT NULL,
|
|
57
|
+
status TEXT NOT NULL,
|
|
58
|
+
source TEXT NOT NULL,
|
|
59
|
+
created_at TEXT NOT NULL,
|
|
60
|
+
updated_at TEXT NOT NULL
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
CREATE INDEX IF NOT EXISTS idx_task_snapshots_status_updated
|
|
64
|
+
ON task_snapshots(status, updated_at DESC, id DESC);
|
|
65
65
|
`);
|
|
66
66
|
},
|
|
67
67
|
},
|
|
68
68
|
{
|
|
69
69
|
version: 3,
|
|
70
70
|
up: (db) => {
|
|
71
|
-
db.exec(`
|
|
72
|
-
CREATE TABLE IF NOT EXISTS memory_conflicts (
|
|
73
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
74
|
-
memory_a_id TEXT NOT NULL,
|
|
75
|
-
memory_b_id TEXT NOT NULL,
|
|
76
|
-
reason TEXT NOT NULL,
|
|
77
|
-
score REAL NOT NULL DEFAULT 0,
|
|
78
|
-
status TEXT NOT NULL,
|
|
79
|
-
detected_at TEXT NOT NULL,
|
|
80
|
-
updated_at TEXT NOT NULL,
|
|
81
|
-
UNIQUE(memory_a_id, memory_b_id)
|
|
82
|
-
);
|
|
83
|
-
|
|
84
|
-
CREATE INDEX IF NOT EXISTS idx_memory_conflicts_status_updated
|
|
85
|
-
ON memory_conflicts(status, updated_at DESC, id DESC);
|
|
71
|
+
db.exec(`
|
|
72
|
+
CREATE TABLE IF NOT EXISTS memory_conflicts (
|
|
73
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
74
|
+
memory_a_id TEXT NOT NULL,
|
|
75
|
+
memory_b_id TEXT NOT NULL,
|
|
76
|
+
reason TEXT NOT NULL,
|
|
77
|
+
score REAL NOT NULL DEFAULT 0,
|
|
78
|
+
status TEXT NOT NULL,
|
|
79
|
+
detected_at TEXT NOT NULL,
|
|
80
|
+
updated_at TEXT NOT NULL,
|
|
81
|
+
UNIQUE(memory_a_id, memory_b_id)
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
CREATE INDEX IF NOT EXISTS idx_memory_conflicts_status_updated
|
|
85
|
+
ON memory_conflicts(status, updated_at DESC, id DESC);
|
|
86
86
|
`);
|
|
87
87
|
},
|
|
88
88
|
},
|
|
@@ -92,67 +92,67 @@ const MIGRATIONS = [
|
|
|
92
92
|
if (!tableHasColumn(db, 'task_snapshots', 'session_id')) {
|
|
93
93
|
db.exec(`ALTER TABLE task_snapshots ADD COLUMN session_id TEXT`);
|
|
94
94
|
}
|
|
95
|
-
db.exec(`
|
|
96
|
-
CREATE TABLE IF NOT EXISTS session_events (
|
|
97
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
98
|
-
session_id TEXT NOT NULL,
|
|
99
|
-
task TEXT,
|
|
100
|
-
event_type TEXT NOT NULL,
|
|
101
|
-
content TEXT NOT NULL,
|
|
102
|
-
source TEXT NOT NULL,
|
|
103
|
-
metadata_json TEXT NOT NULL,
|
|
104
|
-
created_at TEXT NOT NULL
|
|
105
|
-
);
|
|
106
|
-
|
|
107
|
-
CREATE INDEX IF NOT EXISTS idx_session_events_session_created
|
|
108
|
-
ON session_events(session_id, created_at DESC, id DESC);
|
|
109
|
-
|
|
110
|
-
CREATE INDEX IF NOT EXISTS idx_session_events_task_created
|
|
111
|
-
ON session_events(task, created_at DESC, id DESC);
|
|
95
|
+
db.exec(`
|
|
96
|
+
CREATE TABLE IF NOT EXISTS session_events (
|
|
97
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
98
|
+
session_id TEXT NOT NULL,
|
|
99
|
+
task TEXT,
|
|
100
|
+
event_type TEXT NOT NULL,
|
|
101
|
+
content TEXT NOT NULL,
|
|
102
|
+
source TEXT NOT NULL,
|
|
103
|
+
metadata_json TEXT NOT NULL,
|
|
104
|
+
created_at TEXT NOT NULL
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
CREATE INDEX IF NOT EXISTS idx_session_events_session_created
|
|
108
|
+
ON session_events(session_id, created_at DESC, id DESC);
|
|
109
|
+
|
|
110
|
+
CREATE INDEX IF NOT EXISTS idx_session_events_task_created
|
|
111
|
+
ON session_events(task, created_at DESC, id DESC);
|
|
112
112
|
`);
|
|
113
113
|
},
|
|
114
114
|
},
|
|
115
115
|
{
|
|
116
116
|
version: 5,
|
|
117
117
|
up: (db) => {
|
|
118
|
-
db.exec(`
|
|
119
|
-
CREATE TABLE IF NOT EXISTS session_handoffs (
|
|
120
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
121
|
-
session_id TEXT NOT NULL,
|
|
122
|
-
repo_root TEXT,
|
|
123
|
-
task_id TEXT,
|
|
124
|
-
summary TEXT NOT NULL,
|
|
125
|
-
next_action TEXT,
|
|
126
|
-
artifacts_json TEXT NOT NULL DEFAULT '[]',
|
|
127
|
-
created_at TEXT NOT NULL
|
|
128
|
-
);
|
|
129
|
-
|
|
130
|
-
CREATE INDEX IF NOT EXISTS idx_session_handoffs_session
|
|
131
|
-
ON session_handoffs(session_id, created_at DESC);
|
|
118
|
+
db.exec(`
|
|
119
|
+
CREATE TABLE IF NOT EXISTS session_handoffs (
|
|
120
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
121
|
+
session_id TEXT NOT NULL,
|
|
122
|
+
repo_root TEXT,
|
|
123
|
+
task_id TEXT,
|
|
124
|
+
summary TEXT NOT NULL,
|
|
125
|
+
next_action TEXT,
|
|
126
|
+
artifacts_json TEXT NOT NULL DEFAULT '[]',
|
|
127
|
+
created_at TEXT NOT NULL
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
CREATE INDEX IF NOT EXISTS idx_session_handoffs_session
|
|
131
|
+
ON session_handoffs(session_id, created_at DESC);
|
|
132
132
|
`);
|
|
133
133
|
},
|
|
134
134
|
},
|
|
135
135
|
{
|
|
136
136
|
version: 6,
|
|
137
137
|
up: (db) => {
|
|
138
|
-
db.exec(`
|
|
139
|
-
CREATE TABLE IF NOT EXISTS working_memory (
|
|
140
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
141
|
-
scope TEXT NOT NULL,
|
|
142
|
-
session_id TEXT,
|
|
143
|
-
task_id TEXT,
|
|
144
|
-
importance REAL NOT NULL DEFAULT 0,
|
|
145
|
-
content TEXT NOT NULL,
|
|
146
|
-
metadata_json TEXT NOT NULL DEFAULT '{}',
|
|
147
|
-
created_at TEXT NOT NULL,
|
|
148
|
-
updated_at TEXT NOT NULL
|
|
149
|
-
);
|
|
150
|
-
|
|
151
|
-
CREATE INDEX IF NOT EXISTS idx_working_memory_scope
|
|
152
|
-
ON working_memory(scope, importance DESC, created_at DESC);
|
|
153
|
-
|
|
154
|
-
CREATE INDEX IF NOT EXISTS idx_working_memory_session
|
|
155
|
-
ON working_memory(session_id, created_at DESC);
|
|
138
|
+
db.exec(`
|
|
139
|
+
CREATE TABLE IF NOT EXISTS working_memory (
|
|
140
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
141
|
+
scope TEXT NOT NULL,
|
|
142
|
+
session_id TEXT,
|
|
143
|
+
task_id TEXT,
|
|
144
|
+
importance REAL NOT NULL DEFAULT 0,
|
|
145
|
+
content TEXT NOT NULL,
|
|
146
|
+
metadata_json TEXT NOT NULL DEFAULT '{}',
|
|
147
|
+
created_at TEXT NOT NULL,
|
|
148
|
+
updated_at TEXT NOT NULL
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
CREATE INDEX IF NOT EXISTS idx_working_memory_scope
|
|
152
|
+
ON working_memory(scope, importance DESC, created_at DESC);
|
|
153
|
+
|
|
154
|
+
CREATE INDEX IF NOT EXISTS idx_working_memory_session
|
|
155
|
+
ON working_memory(session_id, created_at DESC);
|
|
156
156
|
`);
|
|
157
157
|
},
|
|
158
158
|
},
|
|
@@ -193,9 +193,9 @@ const MIGRATIONS = [
|
|
|
193
193
|
if (!tableHasColumn(db, 'memories', 'source_session_id')) {
|
|
194
194
|
db.exec(`ALTER TABLE memories ADD COLUMN source_session_id TEXT`);
|
|
195
195
|
}
|
|
196
|
-
db.exec(`
|
|
197
|
-
CREATE INDEX IF NOT EXISTS idx_memories_source_session_id
|
|
198
|
-
ON memories(source_session_id) WHERE source_session_id IS NOT NULL
|
|
196
|
+
db.exec(`
|
|
197
|
+
CREATE INDEX IF NOT EXISTS idx_memories_source_session_id
|
|
198
|
+
ON memories(source_session_id) WHERE source_session_id IS NOT NULL
|
|
199
199
|
`);
|
|
200
200
|
},
|
|
201
201
|
},
|
|
@@ -257,44 +257,44 @@ const MIGRATIONS = [
|
|
|
257
257
|
db.exec(`UPDATE memories SET kind = 'superseded' WHERE kind IS NULL AND superseded_by IS NOT NULL`);
|
|
258
258
|
db.exec(`UPDATE memories SET kind = 'distilled' WHERE kind IS NULL`);
|
|
259
259
|
// raw_archive: legitimate path for kind='raw' removal (used by archiveRawMemory).
|
|
260
|
-
db.exec(`
|
|
261
|
-
CREATE TABLE IF NOT EXISTS raw_archive (
|
|
262
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
263
|
-
memory_id TEXT NOT NULL,
|
|
264
|
-
archived_at TEXT NOT NULL,
|
|
265
|
-
reason TEXT NOT NULL,
|
|
266
|
-
archived_by TEXT,
|
|
267
|
-
payload_json TEXT NOT NULL
|
|
268
|
-
)
|
|
260
|
+
db.exec(`
|
|
261
|
+
CREATE TABLE IF NOT EXISTS raw_archive (
|
|
262
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
263
|
+
memory_id TEXT NOT NULL,
|
|
264
|
+
archived_at TEXT NOT NULL,
|
|
265
|
+
reason TEXT NOT NULL,
|
|
266
|
+
archived_by TEXT,
|
|
267
|
+
payload_json TEXT NOT NULL
|
|
268
|
+
)
|
|
269
269
|
`);
|
|
270
270
|
db.exec(`CREATE INDEX IF NOT EXISTS idx_raw_archive_memory_id ON raw_archive(memory_id)`);
|
|
271
271
|
// Append-only invariant: kind='raw' rows cannot be deleted directly.
|
|
272
272
|
// Use raw_archive flow: archive-then-update-then-delete (see src/raw-archive.ts).
|
|
273
|
-
db.exec(`
|
|
274
|
-
CREATE TRIGGER IF NOT EXISTS trg_memories_raw_append_only
|
|
275
|
-
BEFORE DELETE ON memories
|
|
276
|
-
WHEN OLD.kind = 'raw'
|
|
277
|
-
BEGIN
|
|
278
|
-
SELECT RAISE(ABORT, 'raw is append-only');
|
|
279
|
-
END
|
|
273
|
+
db.exec(`
|
|
274
|
+
CREATE TRIGGER IF NOT EXISTS trg_memories_raw_append_only
|
|
275
|
+
BEFORE DELETE ON memories
|
|
276
|
+
WHEN OLD.kind = 'raw'
|
|
277
|
+
BEGIN
|
|
278
|
+
SELECT RAISE(ABORT, 'raw is append-only');
|
|
279
|
+
END
|
|
280
280
|
`);
|
|
281
281
|
// CHECK substitute: ALTER TABLE cannot add CHECK, so enforce kind allowed-set
|
|
282
282
|
// via INSERT/UPDATE triggers.
|
|
283
|
-
db.exec(`
|
|
284
|
-
CREATE TRIGGER IF NOT EXISTS trg_memories_kind_check_insert
|
|
285
|
-
BEFORE INSERT ON memories
|
|
286
|
-
WHEN NEW.kind IS NOT NULL AND NEW.kind NOT IN ('raw','distilled','superseded','archived')
|
|
287
|
-
BEGIN
|
|
288
|
-
SELECT RAISE(ABORT, 'invalid kind: must be raw|distilled|superseded|archived');
|
|
289
|
-
END
|
|
283
|
+
db.exec(`
|
|
284
|
+
CREATE TRIGGER IF NOT EXISTS trg_memories_kind_check_insert
|
|
285
|
+
BEFORE INSERT ON memories
|
|
286
|
+
WHEN NEW.kind IS NOT NULL AND NEW.kind NOT IN ('raw','distilled','superseded','archived')
|
|
287
|
+
BEGIN
|
|
288
|
+
SELECT RAISE(ABORT, 'invalid kind: must be raw|distilled|superseded|archived');
|
|
289
|
+
END
|
|
290
290
|
`);
|
|
291
|
-
db.exec(`
|
|
292
|
-
CREATE TRIGGER IF NOT EXISTS trg_memories_kind_check_update
|
|
293
|
-
BEFORE UPDATE ON memories
|
|
294
|
-
WHEN NEW.kind IS NOT NULL AND NEW.kind NOT IN ('raw','distilled','superseded','archived')
|
|
295
|
-
BEGIN
|
|
296
|
-
SELECT RAISE(ABORT, 'invalid kind: must be raw|distilled|superseded|archived');
|
|
297
|
-
END
|
|
291
|
+
db.exec(`
|
|
292
|
+
CREATE TRIGGER IF NOT EXISTS trg_memories_kind_check_update
|
|
293
|
+
BEFORE UPDATE ON memories
|
|
294
|
+
WHEN NEW.kind IS NOT NULL AND NEW.kind NOT IN ('raw','distilled','superseded','archived')
|
|
295
|
+
BEGIN
|
|
296
|
+
SELECT RAISE(ABORT, 'invalid kind: must be raw|distilled|superseded|archived');
|
|
297
|
+
END
|
|
298
298
|
`);
|
|
299
299
|
},
|
|
300
300
|
},
|
|
@@ -312,30 +312,30 @@ const MIGRATIONS = [
|
|
|
312
312
|
// allowed (different timestamps).
|
|
313
313
|
db.exec(`DROP TRIGGER IF EXISTS trg_memories_kind_check_insert`);
|
|
314
314
|
db.exec(`DROP TRIGGER IF EXISTS trg_memories_kind_check_update`);
|
|
315
|
-
db.exec(`
|
|
316
|
-
CREATE TRIGGER trg_memories_kind_check_insert
|
|
317
|
-
BEFORE INSERT ON memories
|
|
318
|
-
WHEN NEW.kind IS NULL OR NEW.kind NOT IN ('raw','distilled','superseded','archived')
|
|
319
|
-
BEGIN
|
|
320
|
-
SELECT RAISE(ABORT, 'invalid kind: must be raw|distilled|superseded|archived (not null)');
|
|
321
|
-
END
|
|
315
|
+
db.exec(`
|
|
316
|
+
CREATE TRIGGER trg_memories_kind_check_insert
|
|
317
|
+
BEFORE INSERT ON memories
|
|
318
|
+
WHEN NEW.kind IS NULL OR NEW.kind NOT IN ('raw','distilled','superseded','archived')
|
|
319
|
+
BEGIN
|
|
320
|
+
SELECT RAISE(ABORT, 'invalid kind: must be raw|distilled|superseded|archived (not null)');
|
|
321
|
+
END
|
|
322
322
|
`);
|
|
323
|
-
db.exec(`
|
|
324
|
-
CREATE TRIGGER trg_memories_kind_check_update
|
|
325
|
-
BEFORE UPDATE ON memories
|
|
326
|
-
WHEN NEW.kind IS NULL OR NEW.kind NOT IN ('raw','distilled','superseded','archived')
|
|
327
|
-
BEGIN
|
|
328
|
-
SELECT RAISE(ABORT, 'invalid kind: must be raw|distilled|superseded|archived (not null)');
|
|
329
|
-
END
|
|
323
|
+
db.exec(`
|
|
324
|
+
CREATE TRIGGER trg_memories_kind_check_update
|
|
325
|
+
BEFORE UPDATE ON memories
|
|
326
|
+
WHEN NEW.kind IS NULL OR NEW.kind NOT IN ('raw','distilled','superseded','archived')
|
|
327
|
+
BEGIN
|
|
328
|
+
SELECT RAISE(ABORT, 'invalid kind: must be raw|distilled|superseded|archived (not null)');
|
|
329
|
+
END
|
|
330
330
|
`);
|
|
331
331
|
// Defensive: any rows that somehow have NULL kind get fixed (shouldn't exist post-v14
|
|
332
332
|
// backfill, but cheap insurance).
|
|
333
333
|
db.exec(`UPDATE memories SET kind = 'distilled' WHERE kind IS NULL`);
|
|
334
334
|
// raw_archive uniqueness. SQLite cannot ADD CONSTRAINT, but a partial unique index
|
|
335
335
|
// on (memory_id, archived_at) is equivalent for INSERT-time enforcement.
|
|
336
|
-
db.exec(`
|
|
337
|
-
CREATE UNIQUE INDEX IF NOT EXISTS idx_raw_archive_id_at
|
|
338
|
-
ON raw_archive(memory_id, archived_at)
|
|
336
|
+
db.exec(`
|
|
337
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_raw_archive_id_at
|
|
338
|
+
ON raw_archive(memory_id, archived_at)
|
|
339
339
|
`);
|
|
340
340
|
},
|
|
341
341
|
},
|
|
@@ -369,31 +369,31 @@ const MIGRATIONS = [
|
|
|
369
369
|
// A5 stub auth: api_keys (scrypt-hashed; plaintext returned to caller exactly once)
|
|
370
370
|
// and audit_log (append-only mutation trail). Both carry tenant_id from day 1 so
|
|
371
371
|
// future multi-tenant enforcement is a config flip, not a re-migration.
|
|
372
|
-
db.exec(`
|
|
373
|
-
CREATE TABLE IF NOT EXISTS api_keys (
|
|
374
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
375
|
-
key_id TEXT UNIQUE NOT NULL,
|
|
376
|
-
key_hash TEXT NOT NULL,
|
|
377
|
-
tenant_id TEXT NOT NULL DEFAULT 'default',
|
|
378
|
-
label TEXT,
|
|
379
|
-
created_at TEXT NOT NULL,
|
|
380
|
-
revoked_at TEXT
|
|
381
|
-
)
|
|
372
|
+
db.exec(`
|
|
373
|
+
CREATE TABLE IF NOT EXISTS api_keys (
|
|
374
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
375
|
+
key_id TEXT UNIQUE NOT NULL,
|
|
376
|
+
key_hash TEXT NOT NULL,
|
|
377
|
+
tenant_id TEXT NOT NULL DEFAULT 'default',
|
|
378
|
+
label TEXT,
|
|
379
|
+
created_at TEXT NOT NULL,
|
|
380
|
+
revoked_at TEXT
|
|
381
|
+
)
|
|
382
382
|
`);
|
|
383
|
-
db.exec(`
|
|
384
|
-
CREATE INDEX IF NOT EXISTS idx_api_keys_tenant_active
|
|
385
|
-
ON api_keys(tenant_id) WHERE revoked_at IS NULL
|
|
383
|
+
db.exec(`
|
|
384
|
+
CREATE INDEX IF NOT EXISTS idx_api_keys_tenant_active
|
|
385
|
+
ON api_keys(tenant_id) WHERE revoked_at IS NULL
|
|
386
386
|
`);
|
|
387
|
-
db.exec(`
|
|
388
|
-
CREATE TABLE IF NOT EXISTS audit_log (
|
|
389
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
390
|
-
ts TEXT NOT NULL,
|
|
391
|
-
tenant_id TEXT NOT NULL DEFAULT 'default',
|
|
392
|
-
actor TEXT NOT NULL,
|
|
393
|
-
op TEXT NOT NULL,
|
|
394
|
-
target_id TEXT,
|
|
395
|
-
metadata_json TEXT NOT NULL DEFAULT '{}'
|
|
396
|
-
)
|
|
387
|
+
db.exec(`
|
|
388
|
+
CREATE TABLE IF NOT EXISTS audit_log (
|
|
389
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
390
|
+
ts TEXT NOT NULL,
|
|
391
|
+
tenant_id TEXT NOT NULL DEFAULT 'default',
|
|
392
|
+
actor TEXT NOT NULL,
|
|
393
|
+
op TEXT NOT NULL,
|
|
394
|
+
target_id TEXT,
|
|
395
|
+
metadata_json TEXT NOT NULL DEFAULT '{}'
|
|
396
|
+
)
|
|
397
397
|
`);
|
|
398
398
|
db.exec(`CREATE INDEX IF NOT EXISTS idx_audit_log_tenant_ts ON audit_log(tenant_id, ts DESC)`);
|
|
399
399
|
},
|
|
@@ -403,102 +403,102 @@ const MIGRATIONS = [
|
|
|
403
403
|
up: (db) => {
|
|
404
404
|
// E1.3 Slack ingestion: idempotency log, per-channel backfill cursors, DLQ.
|
|
405
405
|
// See docs/plans/2026-04-29-e1.3-slack-ingestion.md.
|
|
406
|
-
db.exec(`
|
|
407
|
-
CREATE TABLE IF NOT EXISTS slack_event_log (
|
|
408
|
-
event_id TEXT PRIMARY KEY,
|
|
409
|
-
ingested_at TEXT NOT NULL,
|
|
410
|
-
memory_id TEXT
|
|
411
|
-
)
|
|
406
|
+
db.exec(`
|
|
407
|
+
CREATE TABLE IF NOT EXISTS slack_event_log (
|
|
408
|
+
event_id TEXT PRIMARY KEY,
|
|
409
|
+
ingested_at TEXT NOT NULL,
|
|
410
|
+
memory_id TEXT
|
|
411
|
+
)
|
|
412
412
|
`);
|
|
413
413
|
db.exec(`CREATE INDEX IF NOT EXISTS idx_slack_event_log_memory ON slack_event_log(memory_id) WHERE memory_id IS NOT NULL`);
|
|
414
|
-
db.exec(`
|
|
415
|
-
CREATE TABLE IF NOT EXISTS slack_cursors (
|
|
416
|
-
tenant_id TEXT NOT NULL,
|
|
417
|
-
channel_id TEXT NOT NULL,
|
|
418
|
-
latest_ts TEXT NOT NULL,
|
|
419
|
-
updated_at TEXT NOT NULL,
|
|
420
|
-
PRIMARY KEY (tenant_id, channel_id)
|
|
421
|
-
)
|
|
414
|
+
db.exec(`
|
|
415
|
+
CREATE TABLE IF NOT EXISTS slack_cursors (
|
|
416
|
+
tenant_id TEXT NOT NULL,
|
|
417
|
+
channel_id TEXT NOT NULL,
|
|
418
|
+
latest_ts TEXT NOT NULL,
|
|
419
|
+
updated_at TEXT NOT NULL,
|
|
420
|
+
PRIMARY KEY (tenant_id, channel_id)
|
|
421
|
+
)
|
|
422
422
|
`);
|
|
423
|
-
db.exec(`
|
|
424
|
-
CREATE TABLE IF NOT EXISTS slack_dlq (
|
|
425
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
426
|
-
tenant_id TEXT NOT NULL,
|
|
427
|
-
raw_payload TEXT NOT NULL,
|
|
428
|
-
error TEXT NOT NULL,
|
|
429
|
-
received_at TEXT NOT NULL,
|
|
430
|
-
retried_at TEXT
|
|
431
|
-
)
|
|
423
|
+
db.exec(`
|
|
424
|
+
CREATE TABLE IF NOT EXISTS slack_dlq (
|
|
425
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
426
|
+
tenant_id TEXT NOT NULL,
|
|
427
|
+
raw_payload TEXT NOT NULL,
|
|
428
|
+
error TEXT NOT NULL,
|
|
429
|
+
received_at TEXT NOT NULL,
|
|
430
|
+
retried_at TEXT
|
|
431
|
+
)
|
|
432
432
|
`);
|
|
433
433
|
db.exec(`CREATE INDEX IF NOT EXISTS idx_slack_dlq_tenant_received ON slack_dlq(tenant_id, received_at)`);
|
|
434
434
|
// Multi-tenant routing seam (review patch #6). Empty by default — single-
|
|
435
435
|
// tenant deployments resolve via HIPPO_TENANT fallback. Multi-workspace
|
|
436
436
|
// deployments populate this table to map team_id → tenant_id.
|
|
437
|
-
db.exec(`
|
|
438
|
-
CREATE TABLE IF NOT EXISTS slack_workspaces (
|
|
439
|
-
team_id TEXT PRIMARY KEY,
|
|
440
|
-
tenant_id TEXT NOT NULL,
|
|
441
|
-
added_at TEXT NOT NULL
|
|
442
|
-
)
|
|
437
|
+
db.exec(`
|
|
438
|
+
CREATE TABLE IF NOT EXISTS slack_workspaces (
|
|
439
|
+
team_id TEXT PRIMARY KEY,
|
|
440
|
+
tenant_id TEXT NOT NULL,
|
|
441
|
+
added_at TEXT NOT NULL
|
|
442
|
+
)
|
|
443
443
|
`);
|
|
444
444
|
},
|
|
445
445
|
},
|
|
446
446
|
{
|
|
447
447
|
version: 18,
|
|
448
448
|
up: (db) => {
|
|
449
|
-
db.exec(`
|
|
450
|
-
CREATE TABLE IF NOT EXISTS goal_stack (
|
|
451
|
-
id TEXT PRIMARY KEY,
|
|
452
|
-
session_id TEXT NOT NULL,
|
|
453
|
-
tenant_id TEXT NOT NULL DEFAULT 'default',
|
|
454
|
-
goal_name TEXT NOT NULL,
|
|
455
|
-
level INTEGER NOT NULL DEFAULT 0
|
|
456
|
-
CHECK (level BETWEEN 0 AND 2),
|
|
457
|
-
parent_goal_id TEXT REFERENCES goal_stack(id) ON DELETE SET NULL,
|
|
458
|
-
status TEXT NOT NULL CHECK (status IN ('active','suspended','completed')),
|
|
459
|
-
success_condition TEXT,
|
|
460
|
-
retrieval_policy_id TEXT,
|
|
461
|
-
created_at TEXT NOT NULL,
|
|
462
|
-
completed_at TEXT,
|
|
463
|
-
outcome_score REAL
|
|
464
|
-
CHECK (outcome_score IS NULL OR (outcome_score >= 0 AND outcome_score <= 1))
|
|
465
|
-
);
|
|
466
|
-
|
|
467
|
-
CREATE INDEX IF NOT EXISTS idx_goal_stack_tenant_session_status
|
|
468
|
-
ON goal_stack(tenant_id, session_id, status, created_at);
|
|
469
|
-
|
|
470
|
-
CREATE TABLE IF NOT EXISTS retrieval_policy (
|
|
471
|
-
id TEXT PRIMARY KEY,
|
|
472
|
-
goal_id TEXT NOT NULL REFERENCES goal_stack(id) ON DELETE CASCADE,
|
|
473
|
-
policy_type TEXT NOT NULL CHECK (policy_type IN
|
|
474
|
-
('schema-fit-biased','error-prioritized','recency-first','hybrid')),
|
|
475
|
-
weight_schema_fit REAL NOT NULL DEFAULT 1.0
|
|
476
|
-
CHECK (weight_schema_fit >= 0 AND weight_schema_fit <= 100),
|
|
477
|
-
weight_recency REAL NOT NULL DEFAULT 1.0
|
|
478
|
-
CHECK (weight_recency >= 0 AND weight_recency <= 100),
|
|
479
|
-
weight_outcome REAL NOT NULL DEFAULT 1.0
|
|
480
|
-
CHECK (weight_outcome >= 0 AND weight_outcome <= 100),
|
|
481
|
-
error_priority REAL NOT NULL DEFAULT 1.0
|
|
482
|
-
CHECK (error_priority >= 0 AND error_priority <= 100)
|
|
483
|
-
);
|
|
484
|
-
|
|
485
|
-
CREATE INDEX IF NOT EXISTS idx_retrieval_policy_goal
|
|
486
|
-
ON retrieval_policy(goal_id);
|
|
487
|
-
|
|
488
|
-
CREATE TABLE IF NOT EXISTS goal_recall_log (
|
|
489
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
490
|
-
goal_id TEXT NOT NULL REFERENCES goal_stack(id) ON DELETE CASCADE,
|
|
491
|
-
memory_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
|
|
492
|
-
tenant_id TEXT NOT NULL DEFAULT 'default',
|
|
493
|
-
session_id TEXT NOT NULL,
|
|
494
|
-
recalled_at TEXT NOT NULL,
|
|
495
|
-
score REAL NOT NULL
|
|
496
|
-
);
|
|
497
|
-
|
|
498
|
-
CREATE INDEX IF NOT EXISTS idx_goal_recall_log_goal
|
|
499
|
-
ON goal_recall_log(goal_id);
|
|
500
|
-
CREATE UNIQUE INDEX IF NOT EXISTS uniq_goal_recall_log_memory_goal
|
|
501
|
-
ON goal_recall_log(memory_id, goal_id);
|
|
449
|
+
db.exec(`
|
|
450
|
+
CREATE TABLE IF NOT EXISTS goal_stack (
|
|
451
|
+
id TEXT PRIMARY KEY,
|
|
452
|
+
session_id TEXT NOT NULL,
|
|
453
|
+
tenant_id TEXT NOT NULL DEFAULT 'default',
|
|
454
|
+
goal_name TEXT NOT NULL,
|
|
455
|
+
level INTEGER NOT NULL DEFAULT 0
|
|
456
|
+
CHECK (level BETWEEN 0 AND 2),
|
|
457
|
+
parent_goal_id TEXT REFERENCES goal_stack(id) ON DELETE SET NULL,
|
|
458
|
+
status TEXT NOT NULL CHECK (status IN ('active','suspended','completed')),
|
|
459
|
+
success_condition TEXT,
|
|
460
|
+
retrieval_policy_id TEXT,
|
|
461
|
+
created_at TEXT NOT NULL,
|
|
462
|
+
completed_at TEXT,
|
|
463
|
+
outcome_score REAL
|
|
464
|
+
CHECK (outcome_score IS NULL OR (outcome_score >= 0 AND outcome_score <= 1))
|
|
465
|
+
);
|
|
466
|
+
|
|
467
|
+
CREATE INDEX IF NOT EXISTS idx_goal_stack_tenant_session_status
|
|
468
|
+
ON goal_stack(tenant_id, session_id, status, created_at);
|
|
469
|
+
|
|
470
|
+
CREATE TABLE IF NOT EXISTS retrieval_policy (
|
|
471
|
+
id TEXT PRIMARY KEY,
|
|
472
|
+
goal_id TEXT NOT NULL REFERENCES goal_stack(id) ON DELETE CASCADE,
|
|
473
|
+
policy_type TEXT NOT NULL CHECK (policy_type IN
|
|
474
|
+
('schema-fit-biased','error-prioritized','recency-first','hybrid')),
|
|
475
|
+
weight_schema_fit REAL NOT NULL DEFAULT 1.0
|
|
476
|
+
CHECK (weight_schema_fit >= 0 AND weight_schema_fit <= 100),
|
|
477
|
+
weight_recency REAL NOT NULL DEFAULT 1.0
|
|
478
|
+
CHECK (weight_recency >= 0 AND weight_recency <= 100),
|
|
479
|
+
weight_outcome REAL NOT NULL DEFAULT 1.0
|
|
480
|
+
CHECK (weight_outcome >= 0 AND weight_outcome <= 100),
|
|
481
|
+
error_priority REAL NOT NULL DEFAULT 1.0
|
|
482
|
+
CHECK (error_priority >= 0 AND error_priority <= 100)
|
|
483
|
+
);
|
|
484
|
+
|
|
485
|
+
CREATE INDEX IF NOT EXISTS idx_retrieval_policy_goal
|
|
486
|
+
ON retrieval_policy(goal_id);
|
|
487
|
+
|
|
488
|
+
CREATE TABLE IF NOT EXISTS goal_recall_log (
|
|
489
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
490
|
+
goal_id TEXT NOT NULL REFERENCES goal_stack(id) ON DELETE CASCADE,
|
|
491
|
+
memory_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
|
|
492
|
+
tenant_id TEXT NOT NULL DEFAULT 'default',
|
|
493
|
+
session_id TEXT NOT NULL,
|
|
494
|
+
recalled_at TEXT NOT NULL,
|
|
495
|
+
score REAL NOT NULL
|
|
496
|
+
);
|
|
497
|
+
|
|
498
|
+
CREATE INDEX IF NOT EXISTS idx_goal_recall_log_goal
|
|
499
|
+
ON goal_recall_log(goal_id);
|
|
500
|
+
CREATE UNIQUE INDEX IF NOT EXISTS uniq_goal_recall_log_memory_goal
|
|
501
|
+
ON goal_recall_log(memory_id, goal_id);
|
|
502
502
|
`);
|
|
503
503
|
},
|
|
504
504
|
},
|
|
@@ -595,27 +595,27 @@ const MIGRATIONS = [
|
|
|
595
595
|
// EXISTS bodies before ALTERing. A silent skip would otherwise stamp
|
|
596
596
|
// schema_version=22 on a DB missing the underlying tables, leaving
|
|
597
597
|
// them permanently absent.
|
|
598
|
-
db.exec(`
|
|
599
|
-
CREATE TABLE IF NOT EXISTS session_events (
|
|
600
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
601
|
-
session_id TEXT NOT NULL,
|
|
602
|
-
task TEXT,
|
|
603
|
-
event_type TEXT NOT NULL,
|
|
604
|
-
content TEXT NOT NULL,
|
|
605
|
-
source TEXT NOT NULL,
|
|
606
|
-
metadata_json TEXT NOT NULL,
|
|
607
|
-
created_at TEXT NOT NULL
|
|
608
|
-
);
|
|
609
|
-
CREATE TABLE IF NOT EXISTS session_handoffs (
|
|
610
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
611
|
-
session_id TEXT NOT NULL,
|
|
612
|
-
repo_root TEXT,
|
|
613
|
-
task_id TEXT,
|
|
614
|
-
summary TEXT NOT NULL,
|
|
615
|
-
next_action TEXT,
|
|
616
|
-
artifacts_json TEXT NOT NULL DEFAULT '[]',
|
|
617
|
-
created_at TEXT NOT NULL
|
|
618
|
-
);
|
|
598
|
+
db.exec(`
|
|
599
|
+
CREATE TABLE IF NOT EXISTS session_events (
|
|
600
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
601
|
+
session_id TEXT NOT NULL,
|
|
602
|
+
task TEXT,
|
|
603
|
+
event_type TEXT NOT NULL,
|
|
604
|
+
content TEXT NOT NULL,
|
|
605
|
+
source TEXT NOT NULL,
|
|
606
|
+
metadata_json TEXT NOT NULL,
|
|
607
|
+
created_at TEXT NOT NULL
|
|
608
|
+
);
|
|
609
|
+
CREATE TABLE IF NOT EXISTS session_handoffs (
|
|
610
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
611
|
+
session_id TEXT NOT NULL,
|
|
612
|
+
repo_root TEXT,
|
|
613
|
+
task_id TEXT,
|
|
614
|
+
summary TEXT NOT NULL,
|
|
615
|
+
next_action TEXT,
|
|
616
|
+
artifacts_json TEXT NOT NULL DEFAULT '[]',
|
|
617
|
+
created_at TEXT NOT NULL
|
|
618
|
+
);
|
|
619
619
|
`);
|
|
620
620
|
if (!tableHasColumn(db, 'session_events', 'tenant_id')) {
|
|
621
621
|
db.exec(`ALTER TABLE session_events ADD COLUMN tenant_id TEXT NOT NULL DEFAULT 'default'`);
|
|
@@ -635,25 +635,25 @@ const MIGRATIONS = [
|
|
|
635
635
|
// tenant boundaries on guesses. The COUNT(DISTINCT) gate is the load-
|
|
636
636
|
// bearing check; without it, rows with multiple tenants under the same
|
|
637
637
|
// session_id would silently pick whichever group came first.
|
|
638
|
-
db.exec(`
|
|
639
|
-
UPDATE session_events
|
|
640
|
-
SET tenant_id = (
|
|
641
|
-
SELECT MAX(t.tenant_id) FROM task_snapshots t
|
|
642
|
-
WHERE t.session_id = session_events.session_id
|
|
643
|
-
)
|
|
644
|
-
WHERE tenant_id = 'default'
|
|
645
|
-
AND (SELECT COUNT(DISTINCT t.tenant_id) FROM task_snapshots t
|
|
646
|
-
WHERE t.session_id = session_events.session_id) = 1
|
|
638
|
+
db.exec(`
|
|
639
|
+
UPDATE session_events
|
|
640
|
+
SET tenant_id = (
|
|
641
|
+
SELECT MAX(t.tenant_id) FROM task_snapshots t
|
|
642
|
+
WHERE t.session_id = session_events.session_id
|
|
643
|
+
)
|
|
644
|
+
WHERE tenant_id = 'default'
|
|
645
|
+
AND (SELECT COUNT(DISTINCT t.tenant_id) FROM task_snapshots t
|
|
646
|
+
WHERE t.session_id = session_events.session_id) = 1
|
|
647
647
|
`);
|
|
648
|
-
db.exec(`
|
|
649
|
-
UPDATE session_handoffs
|
|
650
|
-
SET tenant_id = (
|
|
651
|
-
SELECT MAX(t.tenant_id) FROM task_snapshots t
|
|
652
|
-
WHERE t.session_id = session_handoffs.session_id
|
|
653
|
-
)
|
|
654
|
-
WHERE tenant_id = 'default'
|
|
655
|
-
AND (SELECT COUNT(DISTINCT t.tenant_id) FROM task_snapshots t
|
|
656
|
-
WHERE t.session_id = session_handoffs.session_id) = 1
|
|
648
|
+
db.exec(`
|
|
649
|
+
UPDATE session_handoffs
|
|
650
|
+
SET tenant_id = (
|
|
651
|
+
SELECT MAX(t.tenant_id) FROM task_snapshots t
|
|
652
|
+
WHERE t.session_id = session_handoffs.session_id
|
|
653
|
+
)
|
|
654
|
+
WHERE tenant_id = 'default'
|
|
655
|
+
AND (SELECT COUNT(DISTINCT t.tenant_id) FROM task_snapshots t
|
|
656
|
+
WHERE t.session_id = session_handoffs.session_id) = 1
|
|
657
657
|
`);
|
|
658
658
|
db.exec(`CREATE INDEX IF NOT EXISTS idx_session_events_tenant_session ON session_events(tenant_id, session_id, created_at DESC, id DESC)`);
|
|
659
659
|
db.exec(`CREATE INDEX IF NOT EXISTS idx_session_handoffs_tenant_session ON session_handoffs(tenant_id, session_id, created_at DESC)`);
|
|
@@ -665,19 +665,19 @@ const MIGRATIONS = [
|
|
|
665
665
|
// task_snapshots: add scope so all three continuity tables carry it.
|
|
666
666
|
// Self-heal partial-init stores via CREATE TABLE IF NOT EXISTS (the v22
|
|
667
667
|
// session_events / session_handoffs healing is upstream).
|
|
668
|
-
db.exec(`
|
|
669
|
-
CREATE TABLE IF NOT EXISTS task_snapshots (
|
|
670
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
671
|
-
task TEXT NOT NULL,
|
|
672
|
-
summary TEXT NOT NULL,
|
|
673
|
-
next_step TEXT NOT NULL,
|
|
674
|
-
status TEXT NOT NULL,
|
|
675
|
-
source TEXT NOT NULL,
|
|
676
|
-
session_id TEXT,
|
|
677
|
-
tenant_id TEXT NOT NULL DEFAULT 'default',
|
|
678
|
-
created_at TEXT NOT NULL,
|
|
679
|
-
updated_at TEXT NOT NULL
|
|
680
|
-
)
|
|
668
|
+
db.exec(`
|
|
669
|
+
CREATE TABLE IF NOT EXISTS task_snapshots (
|
|
670
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
671
|
+
task TEXT NOT NULL,
|
|
672
|
+
summary TEXT NOT NULL,
|
|
673
|
+
next_step TEXT NOT NULL,
|
|
674
|
+
status TEXT NOT NULL,
|
|
675
|
+
source TEXT NOT NULL,
|
|
676
|
+
session_id TEXT,
|
|
677
|
+
tenant_id TEXT NOT NULL DEFAULT 'default',
|
|
678
|
+
created_at TEXT NOT NULL,
|
|
679
|
+
updated_at TEXT NOT NULL
|
|
680
|
+
)
|
|
681
681
|
`);
|
|
682
682
|
if (!tableHasColumn(db, 'task_snapshots', 'scope')) {
|
|
683
683
|
db.exec(`ALTER TABLE task_snapshots ADD COLUMN scope TEXT`);
|
|
@@ -710,64 +710,64 @@ const MIGRATIONS = [
|
|
|
710
710
|
up: (db) => {
|
|
711
711
|
// v1.3.0 GitHub connector schema (codex round 1, 2026-05-04).
|
|
712
712
|
// Six tables + a min_compatible_binary meta row for rollback safety.
|
|
713
|
-
db.exec(`
|
|
714
|
-
CREATE TABLE IF NOT EXISTS github_event_log (
|
|
715
|
-
idempotency_key TEXT PRIMARY KEY,
|
|
716
|
-
delivery_id TEXT NOT NULL,
|
|
717
|
-
event_name TEXT NOT NULL,
|
|
718
|
-
ingested_at TEXT NOT NULL,
|
|
719
|
-
memory_id TEXT
|
|
720
|
-
)
|
|
713
|
+
db.exec(`
|
|
714
|
+
CREATE TABLE IF NOT EXISTS github_event_log (
|
|
715
|
+
idempotency_key TEXT PRIMARY KEY,
|
|
716
|
+
delivery_id TEXT NOT NULL,
|
|
717
|
+
event_name TEXT NOT NULL,
|
|
718
|
+
ingested_at TEXT NOT NULL,
|
|
719
|
+
memory_id TEXT
|
|
720
|
+
)
|
|
721
721
|
`);
|
|
722
722
|
db.exec(`CREATE INDEX IF NOT EXISTS idx_github_event_log_memory ON github_event_log(memory_id) WHERE memory_id IS NOT NULL`);
|
|
723
723
|
db.exec(`CREATE INDEX IF NOT EXISTS idx_github_event_log_delivery ON github_event_log(delivery_id)`);
|
|
724
|
-
db.exec(`
|
|
725
|
-
CREATE TABLE IF NOT EXISTS github_cursors (
|
|
726
|
-
tenant_id TEXT NOT NULL,
|
|
727
|
-
repo_full_name TEXT NOT NULL,
|
|
728
|
-
issues_hwm TEXT,
|
|
729
|
-
issue_comments_hwm TEXT,
|
|
730
|
-
pr_review_comments_hwm TEXT,
|
|
731
|
-
updated_at TEXT NOT NULL,
|
|
732
|
-
PRIMARY KEY (tenant_id, repo_full_name)
|
|
733
|
-
)
|
|
724
|
+
db.exec(`
|
|
725
|
+
CREATE TABLE IF NOT EXISTS github_cursors (
|
|
726
|
+
tenant_id TEXT NOT NULL,
|
|
727
|
+
repo_full_name TEXT NOT NULL,
|
|
728
|
+
issues_hwm TEXT,
|
|
729
|
+
issue_comments_hwm TEXT,
|
|
730
|
+
pr_review_comments_hwm TEXT,
|
|
731
|
+
updated_at TEXT NOT NULL,
|
|
732
|
+
PRIMARY KEY (tenant_id, repo_full_name)
|
|
733
|
+
)
|
|
734
734
|
`);
|
|
735
|
-
db.exec(`
|
|
736
|
-
CREATE TABLE IF NOT EXISTS github_dlq (
|
|
737
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
738
|
-
tenant_id TEXT NOT NULL,
|
|
739
|
-
raw_payload TEXT NOT NULL,
|
|
740
|
-
error TEXT NOT NULL,
|
|
741
|
-
event_name TEXT,
|
|
742
|
-
delivery_id TEXT,
|
|
743
|
-
signature TEXT,
|
|
744
|
-
installation_id TEXT,
|
|
745
|
-
repo_full_name TEXT,
|
|
746
|
-
retry_count INTEGER NOT NULL DEFAULT 0,
|
|
747
|
-
received_at TEXT NOT NULL,
|
|
748
|
-
retried_at TEXT,
|
|
749
|
-
bucket TEXT NOT NULL DEFAULT 'parse_error'
|
|
750
|
-
)
|
|
735
|
+
db.exec(`
|
|
736
|
+
CREATE TABLE IF NOT EXISTS github_dlq (
|
|
737
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
738
|
+
tenant_id TEXT NOT NULL,
|
|
739
|
+
raw_payload TEXT NOT NULL,
|
|
740
|
+
error TEXT NOT NULL,
|
|
741
|
+
event_name TEXT,
|
|
742
|
+
delivery_id TEXT,
|
|
743
|
+
signature TEXT,
|
|
744
|
+
installation_id TEXT,
|
|
745
|
+
repo_full_name TEXT,
|
|
746
|
+
retry_count INTEGER NOT NULL DEFAULT 0,
|
|
747
|
+
received_at TEXT NOT NULL,
|
|
748
|
+
retried_at TEXT,
|
|
749
|
+
bucket TEXT NOT NULL DEFAULT 'parse_error'
|
|
750
|
+
)
|
|
751
751
|
`);
|
|
752
752
|
db.exec(`CREATE INDEX IF NOT EXISTS idx_github_dlq_tenant_received ON github_dlq(tenant_id, received_at)`);
|
|
753
|
-
db.exec(`
|
|
754
|
-
CREATE TABLE IF NOT EXISTS github_installations (
|
|
755
|
-
installation_id TEXT PRIMARY KEY,
|
|
756
|
-
tenant_id TEXT NOT NULL,
|
|
757
|
-
added_at TEXT NOT NULL
|
|
758
|
-
)
|
|
753
|
+
db.exec(`
|
|
754
|
+
CREATE TABLE IF NOT EXISTS github_installations (
|
|
755
|
+
installation_id TEXT PRIMARY KEY,
|
|
756
|
+
tenant_id TEXT NOT NULL,
|
|
757
|
+
added_at TEXT NOT NULL
|
|
758
|
+
)
|
|
759
759
|
`);
|
|
760
760
|
// PAT-mode multi-tenant routing (codex P0 #4). Maps repo_full_name to
|
|
761
761
|
// tenant when the webhook envelope has no `installation` field. Composite
|
|
762
762
|
// PK so the same repo can intentionally be visible to multiple tenants
|
|
763
763
|
// (e.g., shared tooling accounts) — collision is on (repo, tenant) pair.
|
|
764
|
-
db.exec(`
|
|
765
|
-
CREATE TABLE IF NOT EXISTS github_repositories (
|
|
766
|
-
repo_full_name TEXT NOT NULL,
|
|
767
|
-
tenant_id TEXT NOT NULL,
|
|
768
|
-
added_at TEXT NOT NULL,
|
|
769
|
-
PRIMARY KEY (repo_full_name, tenant_id)
|
|
770
|
-
)
|
|
764
|
+
db.exec(`
|
|
765
|
+
CREATE TABLE IF NOT EXISTS github_repositories (
|
|
766
|
+
repo_full_name TEXT NOT NULL,
|
|
767
|
+
tenant_id TEXT NOT NULL,
|
|
768
|
+
added_at TEXT NOT NULL,
|
|
769
|
+
PRIMARY KEY (repo_full_name, tenant_id)
|
|
770
|
+
)
|
|
771
771
|
`);
|
|
772
772
|
// Rollback-safety guard (codex P0 #2). Any binary < 1.2.1 lacks the
|
|
773
773
|
// generic *:private:* default-deny and would leak github:private:* rows
|
|
@@ -797,26 +797,26 @@ const MIGRATIONS = [
|
|
|
797
797
|
// Backfill descendant_count for existing level-2 summary rows. Use
|
|
798
798
|
// dag_parent_id pointing at the summary id. Level-3 (entity profiles)
|
|
799
799
|
// not built today; their descendant_count stays at default 0.
|
|
800
|
-
db.exec(`
|
|
801
|
-
UPDATE memories
|
|
802
|
-
SET descendant_count = (
|
|
803
|
-
SELECT COUNT(*) FROM memories AS c WHERE c.dag_parent_id = memories.id
|
|
804
|
-
)
|
|
805
|
-
WHERE dag_level >= 2
|
|
806
|
-
AND descendant_count = 0
|
|
800
|
+
db.exec(`
|
|
801
|
+
UPDATE memories
|
|
802
|
+
SET descendant_count = (
|
|
803
|
+
SELECT COUNT(*) FROM memories AS c WHERE c.dag_parent_id = memories.id
|
|
804
|
+
)
|
|
805
|
+
WHERE dag_level >= 2
|
|
806
|
+
AND descendant_count = 0
|
|
807
807
|
`);
|
|
808
808
|
// Backfill earliest_at / latest_at from child created timestamps for
|
|
809
809
|
// existing summaries. Children of a level-2 summary are level-1 facts.
|
|
810
|
-
db.exec(`
|
|
811
|
-
UPDATE memories
|
|
812
|
-
SET earliest_at = (
|
|
813
|
-
SELECT MIN(c.created) FROM memories AS c WHERE c.dag_parent_id = memories.id
|
|
814
|
-
),
|
|
815
|
-
latest_at = (
|
|
816
|
-
SELECT MAX(c.created) FROM memories AS c WHERE c.dag_parent_id = memories.id
|
|
817
|
-
)
|
|
818
|
-
WHERE dag_level >= 2
|
|
819
|
-
AND earliest_at IS NULL
|
|
810
|
+
db.exec(`
|
|
811
|
+
UPDATE memories
|
|
812
|
+
SET earliest_at = (
|
|
813
|
+
SELECT MIN(c.created) FROM memories AS c WHERE c.dag_parent_id = memories.id
|
|
814
|
+
),
|
|
815
|
+
latest_at = (
|
|
816
|
+
SELECT MAX(c.created) FROM memories AS c WHERE c.dag_parent_id = memories.id
|
|
817
|
+
)
|
|
818
|
+
WHERE dag_level >= 2
|
|
819
|
+
AND earliest_at IS NULL
|
|
820
820
|
`);
|
|
821
821
|
},
|
|
822
822
|
},
|
|
@@ -859,32 +859,32 @@ const MIGRATIONS = [
|
|
|
859
859
|
// bug, fixes anyone who has it. Includes the role column from the
|
|
860
860
|
// start so it matches v26's intent without needing v26 to ALTER on
|
|
861
861
|
// this heal path.
|
|
862
|
-
db.exec(`
|
|
863
|
-
CREATE TABLE IF NOT EXISTS api_keys (
|
|
864
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
865
|
-
key_id TEXT UNIQUE NOT NULL,
|
|
866
|
-
key_hash TEXT NOT NULL,
|
|
867
|
-
tenant_id TEXT NOT NULL DEFAULT 'default',
|
|
868
|
-
label TEXT,
|
|
869
|
-
created_at TEXT NOT NULL,
|
|
870
|
-
revoked_at TEXT,
|
|
871
|
-
role TEXT NOT NULL DEFAULT 'admin'
|
|
872
|
-
)
|
|
862
|
+
db.exec(`
|
|
863
|
+
CREATE TABLE IF NOT EXISTS api_keys (
|
|
864
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
865
|
+
key_id TEXT UNIQUE NOT NULL,
|
|
866
|
+
key_hash TEXT NOT NULL,
|
|
867
|
+
tenant_id TEXT NOT NULL DEFAULT 'default',
|
|
868
|
+
label TEXT,
|
|
869
|
+
created_at TEXT NOT NULL,
|
|
870
|
+
revoked_at TEXT,
|
|
871
|
+
role TEXT NOT NULL DEFAULT 'admin'
|
|
872
|
+
)
|
|
873
873
|
`);
|
|
874
|
-
db.exec(`
|
|
875
|
-
CREATE INDEX IF NOT EXISTS idx_api_keys_tenant_active
|
|
876
|
-
ON api_keys(tenant_id) WHERE revoked_at IS NULL
|
|
874
|
+
db.exec(`
|
|
875
|
+
CREATE INDEX IF NOT EXISTS idx_api_keys_tenant_active
|
|
876
|
+
ON api_keys(tenant_id) WHERE revoked_at IS NULL
|
|
877
877
|
`);
|
|
878
|
-
db.exec(`
|
|
879
|
-
CREATE TABLE IF NOT EXISTS audit_log (
|
|
880
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
881
|
-
ts TEXT NOT NULL,
|
|
882
|
-
tenant_id TEXT NOT NULL DEFAULT 'default',
|
|
883
|
-
actor TEXT NOT NULL,
|
|
884
|
-
op TEXT NOT NULL,
|
|
885
|
-
target_id TEXT,
|
|
886
|
-
metadata_json TEXT NOT NULL DEFAULT '{}'
|
|
887
|
-
)
|
|
878
|
+
db.exec(`
|
|
879
|
+
CREATE TABLE IF NOT EXISTS audit_log (
|
|
880
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
881
|
+
ts TEXT NOT NULL,
|
|
882
|
+
tenant_id TEXT NOT NULL DEFAULT 'default',
|
|
883
|
+
actor TEXT NOT NULL,
|
|
884
|
+
op TEXT NOT NULL,
|
|
885
|
+
target_id TEXT,
|
|
886
|
+
metadata_json TEXT NOT NULL DEFAULT '{}'
|
|
887
|
+
)
|
|
888
888
|
`);
|
|
889
889
|
db.exec(`CREATE INDEX IF NOT EXISTS idx_audit_log_tenant_ts ON audit_log(tenant_id, ts DESC)`);
|
|
890
890
|
// Belt-and-braces: if api_keys existed before this migration WITHOUT
|
|
@@ -952,58 +952,872 @@ const MIGRATIONS = [
|
|
|
952
952
|
// J3 computes accuracy (clean vs regressed) from (estimate_value,
|
|
953
953
|
// actual_value) at query time.
|
|
954
954
|
if (!tableExists(db, 'predictions')) {
|
|
955
|
-
db.exec(`
|
|
956
|
-
CREATE TABLE predictions (
|
|
957
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
958
|
-
memory_id TEXT,
|
|
959
|
-
tenant_id TEXT NOT NULL,
|
|
960
|
-
class_tag TEXT NOT NULL,
|
|
961
|
-
claim_text TEXT NOT NULL,
|
|
962
|
-
estimate_value REAL,
|
|
963
|
-
estimate_unit TEXT,
|
|
964
|
-
target_date TEXT,
|
|
965
|
-
actual_value REAL,
|
|
966
|
-
closure_state TEXT NOT NULL DEFAULT 'open'
|
|
967
|
-
CHECK (closure_state IN ('open', 'closed', 'closed-unknown')),
|
|
968
|
-
closed_at TEXT,
|
|
969
|
-
closure_note TEXT,
|
|
970
|
-
created_at TEXT NOT NULL,
|
|
971
|
-
FOREIGN KEY (memory_id) REFERENCES memories(id) ON DELETE SET NULL
|
|
972
|
-
)
|
|
955
|
+
db.exec(`
|
|
956
|
+
CREATE TABLE predictions (
|
|
957
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
958
|
+
memory_id TEXT,
|
|
959
|
+
tenant_id TEXT NOT NULL,
|
|
960
|
+
class_tag TEXT NOT NULL,
|
|
961
|
+
claim_text TEXT NOT NULL,
|
|
962
|
+
estimate_value REAL,
|
|
963
|
+
estimate_unit TEXT,
|
|
964
|
+
target_date TEXT,
|
|
965
|
+
actual_value REAL,
|
|
966
|
+
closure_state TEXT NOT NULL DEFAULT 'open'
|
|
967
|
+
CHECK (closure_state IN ('open', 'closed', 'closed-unknown')),
|
|
968
|
+
closed_at TEXT,
|
|
969
|
+
closure_note TEXT,
|
|
970
|
+
created_at TEXT NOT NULL,
|
|
971
|
+
FOREIGN KEY (memory_id) REFERENCES memories(id) ON DELETE SET NULL
|
|
972
|
+
)
|
|
973
973
|
`);
|
|
974
|
-
db.exec(`
|
|
975
|
-
CREATE INDEX IF NOT EXISTS idx_predictions_tenant_class
|
|
976
|
-
ON predictions(tenant_id, class_tag, closure_state)
|
|
974
|
+
db.exec(`
|
|
975
|
+
CREATE INDEX IF NOT EXISTS idx_predictions_tenant_class
|
|
976
|
+
ON predictions(tenant_id, class_tag, closure_state)
|
|
977
977
|
`);
|
|
978
|
-
db.exec(`
|
|
979
|
-
CREATE INDEX IF NOT EXISTS idx_predictions_memory
|
|
980
|
-
ON predictions(memory_id) WHERE memory_id IS NOT NULL
|
|
978
|
+
db.exec(`
|
|
979
|
+
CREATE INDEX IF NOT EXISTS idx_predictions_memory
|
|
980
|
+
ON predictions(memory_id) WHERE memory_id IS NOT NULL
|
|
981
981
|
`);
|
|
982
982
|
// Cross-tenant safety: tenant_id must match the referenced memory's
|
|
983
983
|
// tenant_id when memory_id IS NOT NULL. INSERT + UPDATE pair, mirroring
|
|
984
984
|
// v14 memories.kind enforcement (db.ts:298-322).
|
|
985
|
-
db.exec(`
|
|
986
|
-
CREATE TRIGGER IF NOT EXISTS trg_predictions_tenant_match_insert
|
|
987
|
-
BEFORE INSERT ON predictions
|
|
988
|
-
WHEN NEW.memory_id IS NOT NULL
|
|
989
|
-
BEGIN
|
|
990
|
-
SELECT CASE
|
|
991
|
-
WHEN NEW.tenant_id != (SELECT tenant_id FROM memories WHERE id = NEW.memory_id)
|
|
992
|
-
THEN RAISE(ABORT, 'predictions.tenant_id must match memories.tenant_id for the referenced memory')
|
|
993
|
-
END;
|
|
994
|
-
END
|
|
985
|
+
db.exec(`
|
|
986
|
+
CREATE TRIGGER IF NOT EXISTS trg_predictions_tenant_match_insert
|
|
987
|
+
BEFORE INSERT ON predictions
|
|
988
|
+
WHEN NEW.memory_id IS NOT NULL
|
|
989
|
+
BEGIN
|
|
990
|
+
SELECT CASE
|
|
991
|
+
WHEN NEW.tenant_id != (SELECT tenant_id FROM memories WHERE id = NEW.memory_id)
|
|
992
|
+
THEN RAISE(ABORT, 'predictions.tenant_id must match memories.tenant_id for the referenced memory')
|
|
993
|
+
END;
|
|
994
|
+
END
|
|
995
995
|
`);
|
|
996
|
-
db.exec(`
|
|
997
|
-
CREATE TRIGGER IF NOT EXISTS trg_predictions_tenant_match_update
|
|
998
|
-
BEFORE UPDATE ON predictions
|
|
999
|
-
WHEN NEW.memory_id IS NOT NULL
|
|
1000
|
-
AND (NEW.memory_id IS NOT OLD.memory_id OR NEW.tenant_id IS NOT OLD.tenant_id)
|
|
1001
|
-
BEGIN
|
|
1002
|
-
SELECT CASE
|
|
1003
|
-
WHEN NEW.tenant_id != (SELECT tenant_id FROM memories WHERE id = NEW.memory_id)
|
|
1004
|
-
THEN RAISE(ABORT, 'predictions.tenant_id must match memories.tenant_id for the referenced memory')
|
|
1005
|
-
END;
|
|
1006
|
-
END
|
|
996
|
+
db.exec(`
|
|
997
|
+
CREATE TRIGGER IF NOT EXISTS trg_predictions_tenant_match_update
|
|
998
|
+
BEFORE UPDATE ON predictions
|
|
999
|
+
WHEN NEW.memory_id IS NOT NULL
|
|
1000
|
+
AND (NEW.memory_id IS NOT OLD.memory_id OR NEW.tenant_id IS NOT OLD.tenant_id)
|
|
1001
|
+
BEGIN
|
|
1002
|
+
SELECT CASE
|
|
1003
|
+
WHEN NEW.tenant_id != (SELECT tenant_id FROM memories WHERE id = NEW.memory_id)
|
|
1004
|
+
THEN RAISE(ABORT, 'predictions.tenant_id must match memories.tenant_id for the referenced memory')
|
|
1005
|
+
END;
|
|
1006
|
+
END
|
|
1007
|
+
`);
|
|
1008
|
+
}
|
|
1009
|
+
},
|
|
1010
|
+
},
|
|
1011
|
+
{
|
|
1012
|
+
version: 30,
|
|
1013
|
+
up: (db) => {
|
|
1014
|
+
// E2 decision first-class object (docs/plans/2026-05-28-e2-decision-object.md).
|
|
1015
|
+
// Promotes `hippo decide` from a tagged memory (which decayed on a 90-day
|
|
1016
|
+
// half-life even while the decision was still in force) to a canonical
|
|
1017
|
+
// decisions table that is the source of truth. The memory mirror is kept
|
|
1018
|
+
// for recall but is no longer authoritative; memory_id is NULLABLE with
|
|
1019
|
+
// ON DELETE SET NULL so forget/consolidate/archive does not lose a
|
|
1020
|
+
// decision. Mirrors the v29 predictions tenant-match trigger pattern.
|
|
1021
|
+
//
|
|
1022
|
+
// status (active|superseded|closed): superseded carries a self-FK
|
|
1023
|
+
// superseded_by to the successor decision; closed is a terminal
|
|
1024
|
+
// retire-without-successor. A superseded_by same-tenant trigger makes
|
|
1025
|
+
// cross-tenant supersession unrepresentable at the schema level.
|
|
1026
|
+
if (!tableExists(db, 'decisions')) {
|
|
1027
|
+
db.exec(`
|
|
1028
|
+
CREATE TABLE decisions (
|
|
1029
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1030
|
+
memory_id TEXT,
|
|
1031
|
+
tenant_id TEXT NOT NULL,
|
|
1032
|
+
decision_text TEXT NOT NULL,
|
|
1033
|
+
context TEXT,
|
|
1034
|
+
status TEXT NOT NULL DEFAULT 'active'
|
|
1035
|
+
CHECK (status IN ('active', 'superseded', 'closed')),
|
|
1036
|
+
superseded_by INTEGER,
|
|
1037
|
+
superseded_at TEXT,
|
|
1038
|
+
closed_at TEXT,
|
|
1039
|
+
created_at TEXT NOT NULL,
|
|
1040
|
+
FOREIGN KEY (memory_id) REFERENCES memories(id) ON DELETE SET NULL,
|
|
1041
|
+
FOREIGN KEY (superseded_by) REFERENCES decisions(id) ON DELETE SET NULL
|
|
1042
|
+
)
|
|
1043
|
+
`);
|
|
1044
|
+
db.exec(`
|
|
1045
|
+
CREATE INDEX IF NOT EXISTS idx_decisions_tenant_status
|
|
1046
|
+
ON decisions(tenant_id, status)
|
|
1047
|
+
`);
|
|
1048
|
+
db.exec(`
|
|
1049
|
+
CREATE INDEX IF NOT EXISTS idx_decisions_memory
|
|
1050
|
+
ON decisions(memory_id) WHERE memory_id IS NOT NULL
|
|
1051
|
+
`);
|
|
1052
|
+
// Cross-tenant safety vs the referenced memory (mirrors predictions v29).
|
|
1053
|
+
db.exec(`
|
|
1054
|
+
CREATE TRIGGER IF NOT EXISTS trg_decisions_tenant_match_insert
|
|
1055
|
+
BEFORE INSERT ON decisions
|
|
1056
|
+
WHEN NEW.memory_id IS NOT NULL
|
|
1057
|
+
BEGIN
|
|
1058
|
+
SELECT CASE
|
|
1059
|
+
WHEN NEW.tenant_id != (SELECT tenant_id FROM memories WHERE id = NEW.memory_id)
|
|
1060
|
+
THEN RAISE(ABORT, 'decisions.tenant_id must match memories.tenant_id for the referenced memory')
|
|
1061
|
+
END;
|
|
1062
|
+
END
|
|
1063
|
+
`);
|
|
1064
|
+
db.exec(`
|
|
1065
|
+
CREATE TRIGGER IF NOT EXISTS trg_decisions_tenant_match_update
|
|
1066
|
+
BEFORE UPDATE ON decisions
|
|
1067
|
+
WHEN NEW.memory_id IS NOT NULL
|
|
1068
|
+
AND (NEW.memory_id IS NOT OLD.memory_id OR NEW.tenant_id IS NOT OLD.tenant_id)
|
|
1069
|
+
BEGIN
|
|
1070
|
+
SELECT CASE
|
|
1071
|
+
WHEN NEW.tenant_id != (SELECT tenant_id FROM memories WHERE id = NEW.memory_id)
|
|
1072
|
+
THEN RAISE(ABORT, 'decisions.tenant_id must match memories.tenant_id for the referenced memory')
|
|
1073
|
+
END;
|
|
1074
|
+
END
|
|
1075
|
+
`);
|
|
1076
|
+
// Cross-tenant safety vs the successor decision (self-FK). superseded_by
|
|
1077
|
+
// is set only via UPDATE (the supersede path); the successor must share
|
|
1078
|
+
// the tenant. The successor row already exists in the same transaction
|
|
1079
|
+
// when this fires, so the subquery resolves.
|
|
1080
|
+
db.exec(`
|
|
1081
|
+
CREATE TRIGGER IF NOT EXISTS trg_decisions_supersede_tenant_match_update
|
|
1082
|
+
BEFORE UPDATE ON decisions
|
|
1083
|
+
WHEN NEW.superseded_by IS NOT NULL
|
|
1084
|
+
AND NEW.superseded_by IS NOT OLD.superseded_by
|
|
1085
|
+
BEGIN
|
|
1086
|
+
SELECT CASE
|
|
1087
|
+
WHEN NEW.tenant_id != (SELECT tenant_id FROM decisions WHERE id = NEW.superseded_by)
|
|
1088
|
+
THEN RAISE(ABORT, 'decisions.superseded_by must reference a decision in the same tenant')
|
|
1089
|
+
END;
|
|
1090
|
+
END
|
|
1091
|
+
`);
|
|
1092
|
+
}
|
|
1093
|
+
},
|
|
1094
|
+
},
|
|
1095
|
+
{
|
|
1096
|
+
version: 31,
|
|
1097
|
+
up: (db) => {
|
|
1098
|
+
// E2 incident first-class object (docs/plans/2026-05-29-e2-incident-object.md).
|
|
1099
|
+
// Mirrors the v30 decisions block but for an open -> resolved -> closed
|
|
1100
|
+
// lifecycle (NOT supersede): there is no superseded_by self-FK and no
|
|
1101
|
+
// supersede trigger. An incident is a postmortem capsule: a recorded
|
|
1102
|
+
// operational event with a lifecycle and optional linked receipts (the
|
|
1103
|
+
// memories that are its evidence, stored as a JSON array of ids in
|
|
1104
|
+
// linked_memory_ids). The memory mirror is kept for recall but is not
|
|
1105
|
+
// authoritative; memory_id is NULLABLE with ON DELETE SET NULL so
|
|
1106
|
+
// forget/consolidate/archive does not lose an incident.
|
|
1107
|
+
//
|
|
1108
|
+
// status (open|resolved|closed): resolved records a resolution_text +
|
|
1109
|
+
// resolved_at and stays on record; closed is a terminal retire reachable
|
|
1110
|
+
// from open or resolved. Cross-tenant safety: BEFORE INSERT + BEFORE
|
|
1111
|
+
// UPDATE triggers enforce incidents.tenant_id == the referenced memory's
|
|
1112
|
+
// tenant_id (verbatim mirror of the v30 decisions tenant-match triggers).
|
|
1113
|
+
if (!tableExists(db, 'incidents')) {
|
|
1114
|
+
db.exec(`
|
|
1115
|
+
CREATE TABLE incidents (
|
|
1116
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1117
|
+
memory_id TEXT,
|
|
1118
|
+
tenant_id TEXT NOT NULL,
|
|
1119
|
+
incident_text TEXT NOT NULL,
|
|
1120
|
+
context TEXT,
|
|
1121
|
+
status TEXT NOT NULL DEFAULT 'open'
|
|
1122
|
+
CHECK (status IN ('open', 'resolved', 'closed')),
|
|
1123
|
+
resolution_text TEXT,
|
|
1124
|
+
resolved_at TEXT,
|
|
1125
|
+
closed_at TEXT,
|
|
1126
|
+
linked_memory_ids TEXT NOT NULL DEFAULT '[]',
|
|
1127
|
+
created_at TEXT NOT NULL,
|
|
1128
|
+
FOREIGN KEY (memory_id) REFERENCES memories(id) ON DELETE SET NULL
|
|
1129
|
+
)
|
|
1130
|
+
`);
|
|
1131
|
+
db.exec(`
|
|
1132
|
+
CREATE INDEX IF NOT EXISTS idx_incidents_tenant_status
|
|
1133
|
+
ON incidents(tenant_id, status)
|
|
1134
|
+
`);
|
|
1135
|
+
db.exec(`
|
|
1136
|
+
CREATE INDEX IF NOT EXISTS idx_incidents_memory
|
|
1137
|
+
ON incidents(memory_id) WHERE memory_id IS NOT NULL
|
|
1138
|
+
`);
|
|
1139
|
+
// Cross-tenant safety vs the referenced memory (verbatim mirror of the
|
|
1140
|
+
// v30 decisions tenant-match triggers; no supersede trigger).
|
|
1141
|
+
db.exec(`
|
|
1142
|
+
CREATE TRIGGER IF NOT EXISTS trg_incidents_tenant_match_insert
|
|
1143
|
+
BEFORE INSERT ON incidents
|
|
1144
|
+
WHEN NEW.memory_id IS NOT NULL
|
|
1145
|
+
BEGIN
|
|
1146
|
+
SELECT CASE
|
|
1147
|
+
WHEN NEW.tenant_id != (SELECT tenant_id FROM memories WHERE id = NEW.memory_id)
|
|
1148
|
+
THEN RAISE(ABORT, 'incidents.tenant_id must match memories.tenant_id for the referenced memory')
|
|
1149
|
+
END;
|
|
1150
|
+
END
|
|
1151
|
+
`);
|
|
1152
|
+
db.exec(`
|
|
1153
|
+
CREATE TRIGGER IF NOT EXISTS trg_incidents_tenant_match_update
|
|
1154
|
+
BEFORE UPDATE ON incidents
|
|
1155
|
+
WHEN NEW.memory_id IS NOT NULL
|
|
1156
|
+
AND (NEW.memory_id IS NOT OLD.memory_id OR NEW.tenant_id IS NOT OLD.tenant_id)
|
|
1157
|
+
BEGIN
|
|
1158
|
+
SELECT CASE
|
|
1159
|
+
WHEN NEW.tenant_id != (SELECT tenant_id FROM memories WHERE id = NEW.memory_id)
|
|
1160
|
+
THEN RAISE(ABORT, 'incidents.tenant_id must match memories.tenant_id for the referenced memory')
|
|
1161
|
+
END;
|
|
1162
|
+
END
|
|
1163
|
+
`);
|
|
1164
|
+
}
|
|
1165
|
+
},
|
|
1166
|
+
},
|
|
1167
|
+
{
|
|
1168
|
+
version: 32,
|
|
1169
|
+
up: (db) => {
|
|
1170
|
+
// E2 process first-class object (docs/plans/2026-05-29-e2-process-object.md).
|
|
1171
|
+
// A process is a "living process map": a named, ordered list of steps that
|
|
1172
|
+
// evolves. Unlike incident (open->resolved->closed, no supersede), process
|
|
1173
|
+
// REUSES the v30 decisions supersede path as its delta mechanism: a process
|
|
1174
|
+
// evolves by being superseded by a NEW VERSION that records what changed
|
|
1175
|
+
// (change_summary) and the full new state (steps), carrying a derived
|
|
1176
|
+
// version counter. So this table combines the v31 incidents tenant-match
|
|
1177
|
+
// trigger pair (vs the referenced memory) WITH the v30 decisions
|
|
1178
|
+
// superseded_by self-FK + supersede tenant-match trigger.
|
|
1179
|
+
//
|
|
1180
|
+
// status (active|superseded|closed): superseded carries a self-FK
|
|
1181
|
+
// superseded_by to the successor version; closed is a terminal
|
|
1182
|
+
// retire-without-successor (only an active head closes). The memory mirror
|
|
1183
|
+
// is kept for recall but is not authoritative; memory_id is NULLABLE with
|
|
1184
|
+
// ON DELETE SET NULL so forget/consolidate/archive does not lose a process.
|
|
1185
|
+
// steps is a JSON-encoded array of step strings (scoped v1; a normalized
|
|
1186
|
+
// process_steps table is deferred). version is server-derived
|
|
1187
|
+
// (predecessor.version + 1); change_summary is set on a successor row only.
|
|
1188
|
+
if (!tableExists(db, 'processes')) {
|
|
1189
|
+
db.exec(`
|
|
1190
|
+
CREATE TABLE processes (
|
|
1191
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1192
|
+
memory_id TEXT,
|
|
1193
|
+
tenant_id TEXT NOT NULL,
|
|
1194
|
+
process_name TEXT NOT NULL,
|
|
1195
|
+
description TEXT,
|
|
1196
|
+
steps TEXT NOT NULL DEFAULT '[]',
|
|
1197
|
+
version INTEGER NOT NULL DEFAULT 1,
|
|
1198
|
+
status TEXT NOT NULL DEFAULT 'active'
|
|
1199
|
+
CHECK (status IN ('active', 'superseded', 'closed')),
|
|
1200
|
+
superseded_by INTEGER,
|
|
1201
|
+
superseded_at TEXT,
|
|
1202
|
+
change_summary TEXT,
|
|
1203
|
+
closed_at TEXT,
|
|
1204
|
+
created_at TEXT NOT NULL,
|
|
1205
|
+
FOREIGN KEY (memory_id) REFERENCES memories(id) ON DELETE SET NULL,
|
|
1206
|
+
FOREIGN KEY (superseded_by) REFERENCES processes(id) ON DELETE SET NULL
|
|
1207
|
+
)
|
|
1208
|
+
`);
|
|
1209
|
+
db.exec(`
|
|
1210
|
+
CREATE INDEX IF NOT EXISTS idx_processes_tenant_status
|
|
1211
|
+
ON processes(tenant_id, status)
|
|
1212
|
+
`);
|
|
1213
|
+
db.exec(`
|
|
1214
|
+
CREATE INDEX IF NOT EXISTS idx_processes_memory
|
|
1215
|
+
ON processes(memory_id) WHERE memory_id IS NOT NULL
|
|
1216
|
+
`);
|
|
1217
|
+
// Cross-tenant safety vs the referenced memory (verbatim mirror of the
|
|
1218
|
+
// v31 incidents / v30 decisions tenant-match triggers).
|
|
1219
|
+
db.exec(`
|
|
1220
|
+
CREATE TRIGGER IF NOT EXISTS trg_processes_tenant_match_insert
|
|
1221
|
+
BEFORE INSERT ON processes
|
|
1222
|
+
WHEN NEW.memory_id IS NOT NULL
|
|
1223
|
+
BEGIN
|
|
1224
|
+
SELECT CASE
|
|
1225
|
+
WHEN NEW.tenant_id != (SELECT tenant_id FROM memories WHERE id = NEW.memory_id)
|
|
1226
|
+
THEN RAISE(ABORT, 'processes.tenant_id must match memories.tenant_id for the referenced memory')
|
|
1227
|
+
END;
|
|
1228
|
+
END
|
|
1229
|
+
`);
|
|
1230
|
+
db.exec(`
|
|
1231
|
+
CREATE TRIGGER IF NOT EXISTS trg_processes_tenant_match_update
|
|
1232
|
+
BEFORE UPDATE ON processes
|
|
1233
|
+
WHEN NEW.memory_id IS NOT NULL
|
|
1234
|
+
AND (NEW.memory_id IS NOT OLD.memory_id OR NEW.tenant_id IS NOT OLD.tenant_id)
|
|
1235
|
+
BEGIN
|
|
1236
|
+
SELECT CASE
|
|
1237
|
+
WHEN NEW.tenant_id != (SELECT tenant_id FROM memories WHERE id = NEW.memory_id)
|
|
1238
|
+
THEN RAISE(ABORT, 'processes.tenant_id must match memories.tenant_id for the referenced memory')
|
|
1239
|
+
END;
|
|
1240
|
+
END
|
|
1241
|
+
`);
|
|
1242
|
+
// Cross-tenant safety vs the successor process (self-FK; verbatim mirror
|
|
1243
|
+
// of the v30 decisions supersede trigger). superseded_by is set only via
|
|
1244
|
+
// the supersede UPDATE; the successor must share the tenant. The successor
|
|
1245
|
+
// row already exists in the same transaction when this fires.
|
|
1246
|
+
db.exec(`
|
|
1247
|
+
CREATE TRIGGER IF NOT EXISTS trg_processes_supersede_tenant_match_update
|
|
1248
|
+
BEFORE UPDATE ON processes
|
|
1249
|
+
WHEN NEW.superseded_by IS NOT NULL
|
|
1250
|
+
AND NEW.superseded_by IS NOT OLD.superseded_by
|
|
1251
|
+
BEGIN
|
|
1252
|
+
SELECT CASE
|
|
1253
|
+
WHEN NEW.tenant_id != (SELECT tenant_id FROM processes WHERE id = NEW.superseded_by)
|
|
1254
|
+
THEN RAISE(ABORT, 'processes.superseded_by must reference a process in the same tenant')
|
|
1255
|
+
END;
|
|
1256
|
+
END
|
|
1257
|
+
`);
|
|
1258
|
+
}
|
|
1259
|
+
},
|
|
1260
|
+
},
|
|
1261
|
+
{
|
|
1262
|
+
version: 33,
|
|
1263
|
+
up: (db) => {
|
|
1264
|
+
// E2 policy first-class object (docs/plans/2026-05-30-e2-policy-object.md).
|
|
1265
|
+
// The "bi-temporal-first" object type: a named rule/statement that is in
|
|
1266
|
+
// force over an EFFECTIVE-TIME range (valid_from required, valid_to nullable
|
|
1267
|
+
// = open-ended) and evolves via the v32 processes supersede machinery
|
|
1268
|
+
// (superseded_by self-FK + supersede tenant-match trigger + version +
|
|
1269
|
+
// change_summary). This table = the v32 processes table MINUS `steps`
|
|
1270
|
+
// (a policy has policy_text, not an ordered step list) PLUS the first-class
|
|
1271
|
+
// effective-time columns valid_from/valid_to. Valid-time is the queryable
|
|
1272
|
+
// axis (the as-of query loadPoliciesAsOf); transaction-time is present via
|
|
1273
|
+
// created_at + the supersede chain's superseded_at (time-travel deferred).
|
|
1274
|
+
//
|
|
1275
|
+
// All date inputs are normalized to canonical ISO-8601 datetime
|
|
1276
|
+
// (toISOString) at the store boundary before persist/compare, so the
|
|
1277
|
+
// fixed-width values sort lexically and the half-open [valid_from, valid_to)
|
|
1278
|
+
// as-of comparison is correct (plan-eng-critic round-1 CRIT fix).
|
|
1279
|
+
if (!tableExists(db, 'policies')) {
|
|
1280
|
+
db.exec(`
|
|
1281
|
+
CREATE TABLE policies (
|
|
1282
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1283
|
+
memory_id TEXT,
|
|
1284
|
+
tenant_id TEXT NOT NULL,
|
|
1285
|
+
policy_name TEXT NOT NULL,
|
|
1286
|
+
policy_text TEXT NOT NULL,
|
|
1287
|
+
valid_from TEXT NOT NULL,
|
|
1288
|
+
valid_to TEXT,
|
|
1289
|
+
version INTEGER NOT NULL DEFAULT 1,
|
|
1290
|
+
status TEXT NOT NULL DEFAULT 'active'
|
|
1291
|
+
CHECK (status IN ('active', 'superseded', 'closed')),
|
|
1292
|
+
superseded_by INTEGER,
|
|
1293
|
+
superseded_at TEXT,
|
|
1294
|
+
change_summary TEXT,
|
|
1295
|
+
closed_at TEXT,
|
|
1296
|
+
created_at TEXT NOT NULL,
|
|
1297
|
+
FOREIGN KEY (memory_id) REFERENCES memories(id) ON DELETE SET NULL,
|
|
1298
|
+
FOREIGN KEY (superseded_by) REFERENCES policies(id) ON DELETE SET NULL
|
|
1299
|
+
)
|
|
1300
|
+
`);
|
|
1301
|
+
db.exec(`
|
|
1302
|
+
CREATE INDEX IF NOT EXISTS idx_policies_tenant_status
|
|
1303
|
+
ON policies(tenant_id, status)
|
|
1304
|
+
`);
|
|
1305
|
+
db.exec(`
|
|
1306
|
+
CREATE INDEX IF NOT EXISTS idx_policies_memory
|
|
1307
|
+
ON policies(memory_id) WHERE memory_id IS NOT NULL
|
|
1308
|
+
`);
|
|
1309
|
+
// Supports the as-of query (active policies in force at a valid-time).
|
|
1310
|
+
db.exec(`
|
|
1311
|
+
CREATE INDEX IF NOT EXISTS idx_policies_asof
|
|
1312
|
+
ON policies(tenant_id, valid_from)
|
|
1313
|
+
`);
|
|
1314
|
+
// Cross-tenant safety vs the referenced memory (verbatim mirror of the
|
|
1315
|
+
// v32 processes tenant-match triggers).
|
|
1316
|
+
db.exec(`
|
|
1317
|
+
CREATE TRIGGER IF NOT EXISTS trg_policies_tenant_match_insert
|
|
1318
|
+
BEFORE INSERT ON policies
|
|
1319
|
+
WHEN NEW.memory_id IS NOT NULL
|
|
1320
|
+
BEGIN
|
|
1321
|
+
SELECT CASE
|
|
1322
|
+
WHEN NEW.tenant_id != (SELECT tenant_id FROM memories WHERE id = NEW.memory_id)
|
|
1323
|
+
THEN RAISE(ABORT, 'policies.tenant_id must match memories.tenant_id for the referenced memory')
|
|
1324
|
+
END;
|
|
1325
|
+
END
|
|
1326
|
+
`);
|
|
1327
|
+
db.exec(`
|
|
1328
|
+
CREATE TRIGGER IF NOT EXISTS trg_policies_tenant_match_update
|
|
1329
|
+
BEFORE UPDATE ON policies
|
|
1330
|
+
WHEN NEW.memory_id IS NOT NULL
|
|
1331
|
+
AND (NEW.memory_id IS NOT OLD.memory_id OR NEW.tenant_id IS NOT OLD.tenant_id)
|
|
1332
|
+
BEGIN
|
|
1333
|
+
SELECT CASE
|
|
1334
|
+
WHEN NEW.tenant_id != (SELECT tenant_id FROM memories WHERE id = NEW.memory_id)
|
|
1335
|
+
THEN RAISE(ABORT, 'policies.tenant_id must match memories.tenant_id for the referenced memory')
|
|
1336
|
+
END;
|
|
1337
|
+
END
|
|
1338
|
+
`);
|
|
1339
|
+
// Cross-tenant safety vs the successor policy (self-FK; verbatim mirror of
|
|
1340
|
+
// the v32 processes supersede trigger).
|
|
1341
|
+
db.exec(`
|
|
1342
|
+
CREATE TRIGGER IF NOT EXISTS trg_policies_supersede_tenant_match_update
|
|
1343
|
+
BEFORE UPDATE ON policies
|
|
1344
|
+
WHEN NEW.superseded_by IS NOT NULL
|
|
1345
|
+
AND NEW.superseded_by IS NOT OLD.superseded_by
|
|
1346
|
+
BEGIN
|
|
1347
|
+
SELECT CASE
|
|
1348
|
+
WHEN NEW.tenant_id != (SELECT tenant_id FROM policies WHERE id = NEW.superseded_by)
|
|
1349
|
+
THEN RAISE(ABORT, 'policies.superseded_by must reference a policy in the same tenant')
|
|
1350
|
+
END;
|
|
1351
|
+
END
|
|
1352
|
+
`);
|
|
1353
|
+
}
|
|
1354
|
+
},
|
|
1355
|
+
},
|
|
1356
|
+
{
|
|
1357
|
+
version: 34,
|
|
1358
|
+
up: (db) => {
|
|
1359
|
+
// E2 skill first-class object (docs/plans/2026-05-30-e2-skill-object.md).
|
|
1360
|
+
// A skill is a reusable, agent-followable capability: an `instructions` body
|
|
1361
|
+
// + an optional `trigger_text` (when to apply), evolving via the v32
|
|
1362
|
+
// processes supersede machinery (superseded_by self-FK + supersede
|
|
1363
|
+
// tenant-match trigger + version + change_summary). This table = the v32
|
|
1364
|
+
// processes table MINUS `steps` (a skill's content is a single instructions
|
|
1365
|
+
// body) PLUS `instructions` (NOT NULL) and `trigger_text`. "Executable" is
|
|
1366
|
+
// scoped to an agent-followable instruction that EXPORTS into the agent's
|
|
1367
|
+
// in-force rules (AGENTS.md / CLAUDE.md) via exportSkills; literal code
|
|
1368
|
+
// execution is deferred. NOTE: the column is `trigger_text`, NOT `trigger`,
|
|
1369
|
+
// because TRIGGER is a SQLite reserved keyword.
|
|
1370
|
+
if (!tableExists(db, 'skills')) {
|
|
1371
|
+
db.exec(`
|
|
1372
|
+
CREATE TABLE skills (
|
|
1373
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1374
|
+
memory_id TEXT,
|
|
1375
|
+
tenant_id TEXT NOT NULL,
|
|
1376
|
+
skill_name TEXT NOT NULL,
|
|
1377
|
+
instructions TEXT NOT NULL,
|
|
1378
|
+
trigger_text TEXT,
|
|
1379
|
+
version INTEGER NOT NULL DEFAULT 1,
|
|
1380
|
+
status TEXT NOT NULL DEFAULT 'active'
|
|
1381
|
+
CHECK (status IN ('active', 'superseded', 'closed')),
|
|
1382
|
+
superseded_by INTEGER,
|
|
1383
|
+
superseded_at TEXT,
|
|
1384
|
+
change_summary TEXT,
|
|
1385
|
+
closed_at TEXT,
|
|
1386
|
+
created_at TEXT NOT NULL,
|
|
1387
|
+
FOREIGN KEY (memory_id) REFERENCES memories(id) ON DELETE SET NULL,
|
|
1388
|
+
FOREIGN KEY (superseded_by) REFERENCES skills(id) ON DELETE SET NULL
|
|
1389
|
+
)
|
|
1390
|
+
`);
|
|
1391
|
+
db.exec(`
|
|
1392
|
+
CREATE INDEX IF NOT EXISTS idx_skills_tenant_status
|
|
1393
|
+
ON skills(tenant_id, status)
|
|
1394
|
+
`);
|
|
1395
|
+
db.exec(`
|
|
1396
|
+
CREATE INDEX IF NOT EXISTS idx_skills_memory
|
|
1397
|
+
ON skills(memory_id) WHERE memory_id IS NOT NULL
|
|
1398
|
+
`);
|
|
1399
|
+
// Cross-tenant safety vs the referenced memory (verbatim mirror of the
|
|
1400
|
+
// v32 processes tenant-match triggers).
|
|
1401
|
+
db.exec(`
|
|
1402
|
+
CREATE TRIGGER IF NOT EXISTS trg_skills_tenant_match_insert
|
|
1403
|
+
BEFORE INSERT ON skills
|
|
1404
|
+
WHEN NEW.memory_id IS NOT NULL
|
|
1405
|
+
BEGIN
|
|
1406
|
+
SELECT CASE
|
|
1407
|
+
WHEN NEW.tenant_id != (SELECT tenant_id FROM memories WHERE id = NEW.memory_id)
|
|
1408
|
+
THEN RAISE(ABORT, 'skills.tenant_id must match memories.tenant_id for the referenced memory')
|
|
1409
|
+
END;
|
|
1410
|
+
END
|
|
1411
|
+
`);
|
|
1412
|
+
db.exec(`
|
|
1413
|
+
CREATE TRIGGER IF NOT EXISTS trg_skills_tenant_match_update
|
|
1414
|
+
BEFORE UPDATE ON skills
|
|
1415
|
+
WHEN NEW.memory_id IS NOT NULL
|
|
1416
|
+
AND (NEW.memory_id IS NOT OLD.memory_id OR NEW.tenant_id IS NOT OLD.tenant_id)
|
|
1417
|
+
BEGIN
|
|
1418
|
+
SELECT CASE
|
|
1419
|
+
WHEN NEW.tenant_id != (SELECT tenant_id FROM memories WHERE id = NEW.memory_id)
|
|
1420
|
+
THEN RAISE(ABORT, 'skills.tenant_id must match memories.tenant_id for the referenced memory')
|
|
1421
|
+
END;
|
|
1422
|
+
END
|
|
1423
|
+
`);
|
|
1424
|
+
// Cross-tenant safety vs the successor skill (self-FK; verbatim mirror of
|
|
1425
|
+
// the v32 processes supersede trigger).
|
|
1426
|
+
db.exec(`
|
|
1427
|
+
CREATE TRIGGER IF NOT EXISTS trg_skills_supersede_tenant_match_update
|
|
1428
|
+
BEFORE UPDATE ON skills
|
|
1429
|
+
WHEN NEW.superseded_by IS NOT NULL
|
|
1430
|
+
AND NEW.superseded_by IS NOT OLD.superseded_by
|
|
1431
|
+
BEGIN
|
|
1432
|
+
SELECT CASE
|
|
1433
|
+
WHEN NEW.tenant_id != (SELECT tenant_id FROM skills WHERE id = NEW.superseded_by)
|
|
1434
|
+
THEN RAISE(ABORT, 'skills.superseded_by must reference a skill in the same tenant')
|
|
1435
|
+
END;
|
|
1436
|
+
END
|
|
1437
|
+
`);
|
|
1438
|
+
}
|
|
1439
|
+
},
|
|
1440
|
+
},
|
|
1441
|
+
{
|
|
1442
|
+
version: 35,
|
|
1443
|
+
up: (db) => {
|
|
1444
|
+
// E2 project_brief first-class object
|
|
1445
|
+
// (docs/plans/2026-05-30-e2-project-brief-object.md). A project_brief is the
|
|
1446
|
+
// living, repo-scoped summary of a repository's state: a `summary` body
|
|
1447
|
+
// scoped to a `repo`, evolving via the v34 skills supersede machinery
|
|
1448
|
+
// (superseded_by self-FK + supersede tenant-match trigger + version +
|
|
1449
|
+
// change_summary). This table = the v34 skills table with
|
|
1450
|
+
// skill_name/trigger_text replaced by `repo` (the repo-scoping dimension)
|
|
1451
|
+
// PLUS `summary` (the brief body). The distinguishing op (refreshBrief, in
|
|
1452
|
+
// src/project-briefs.ts) auto-assembles the summary from the repo's receipts
|
|
1453
|
+
// (memory rows tagged path:<repo>); it needs no schema support beyond `repo`.
|
|
1454
|
+
// All column names were checked against SQLite reserved words (skill-episode
|
|
1455
|
+
// lesson re: `trigger`): repo/summary/version/status/etc. are non-reserved.
|
|
1456
|
+
if (!tableExists(db, 'project_briefs')) {
|
|
1457
|
+
db.exec(`
|
|
1458
|
+
CREATE TABLE project_briefs (
|
|
1459
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1460
|
+
memory_id TEXT,
|
|
1461
|
+
tenant_id TEXT NOT NULL,
|
|
1462
|
+
repo TEXT NOT NULL,
|
|
1463
|
+
summary TEXT NOT NULL,
|
|
1464
|
+
version INTEGER NOT NULL DEFAULT 1,
|
|
1465
|
+
status TEXT NOT NULL DEFAULT 'active'
|
|
1466
|
+
CHECK (status IN ('active', 'superseded', 'closed')),
|
|
1467
|
+
superseded_by INTEGER,
|
|
1468
|
+
superseded_at TEXT,
|
|
1469
|
+
change_summary TEXT,
|
|
1470
|
+
closed_at TEXT,
|
|
1471
|
+
created_at TEXT NOT NULL,
|
|
1472
|
+
FOREIGN KEY (memory_id) REFERENCES memories(id) ON DELETE SET NULL,
|
|
1473
|
+
FOREIGN KEY (superseded_by) REFERENCES project_briefs(id) ON DELETE SET NULL
|
|
1474
|
+
)
|
|
1475
|
+
`);
|
|
1476
|
+
db.exec(`
|
|
1477
|
+
CREATE INDEX IF NOT EXISTS idx_project_briefs_tenant_status
|
|
1478
|
+
ON project_briefs(tenant_id, status)
|
|
1479
|
+
`);
|
|
1480
|
+
db.exec(`
|
|
1481
|
+
CREATE INDEX IF NOT EXISTS idx_project_briefs_memory
|
|
1482
|
+
ON project_briefs(memory_id) WHERE memory_id IS NOT NULL
|
|
1483
|
+
`);
|
|
1484
|
+
db.exec(`
|
|
1485
|
+
CREATE INDEX IF NOT EXISTS idx_project_briefs_repo
|
|
1486
|
+
ON project_briefs(tenant_id, repo, status)
|
|
1487
|
+
`);
|
|
1488
|
+
// Cross-tenant safety vs the referenced memory (verbatim mirror of the
|
|
1489
|
+
// v34 skills tenant-match triggers).
|
|
1490
|
+
db.exec(`
|
|
1491
|
+
CREATE TRIGGER IF NOT EXISTS trg_project_briefs_tenant_match_insert
|
|
1492
|
+
BEFORE INSERT ON project_briefs
|
|
1493
|
+
WHEN NEW.memory_id IS NOT NULL
|
|
1494
|
+
BEGIN
|
|
1495
|
+
SELECT CASE
|
|
1496
|
+
WHEN NEW.tenant_id != (SELECT tenant_id FROM memories WHERE id = NEW.memory_id)
|
|
1497
|
+
THEN RAISE(ABORT, 'project_briefs.tenant_id must match memories.tenant_id for the referenced memory')
|
|
1498
|
+
END;
|
|
1499
|
+
END
|
|
1500
|
+
`);
|
|
1501
|
+
db.exec(`
|
|
1502
|
+
CREATE TRIGGER IF NOT EXISTS trg_project_briefs_tenant_match_update
|
|
1503
|
+
BEFORE UPDATE ON project_briefs
|
|
1504
|
+
WHEN NEW.memory_id IS NOT NULL
|
|
1505
|
+
AND (NEW.memory_id IS NOT OLD.memory_id OR NEW.tenant_id IS NOT OLD.tenant_id)
|
|
1506
|
+
BEGIN
|
|
1507
|
+
SELECT CASE
|
|
1508
|
+
WHEN NEW.tenant_id != (SELECT tenant_id FROM memories WHERE id = NEW.memory_id)
|
|
1509
|
+
THEN RAISE(ABORT, 'project_briefs.tenant_id must match memories.tenant_id for the referenced memory')
|
|
1510
|
+
END;
|
|
1511
|
+
END
|
|
1512
|
+
`);
|
|
1513
|
+
// Cross-tenant safety vs the successor brief (self-FK; verbatim mirror of
|
|
1514
|
+
// the v34 skills supersede trigger).
|
|
1515
|
+
db.exec(`
|
|
1516
|
+
CREATE TRIGGER IF NOT EXISTS trg_project_briefs_supersede_tenant_match_update
|
|
1517
|
+
BEFORE UPDATE ON project_briefs
|
|
1518
|
+
WHEN NEW.superseded_by IS NOT NULL
|
|
1519
|
+
AND NEW.superseded_by IS NOT OLD.superseded_by
|
|
1520
|
+
BEGIN
|
|
1521
|
+
SELECT CASE
|
|
1522
|
+
WHEN NEW.tenant_id != (SELECT tenant_id FROM project_briefs WHERE id = NEW.superseded_by)
|
|
1523
|
+
THEN RAISE(ABORT, 'project_briefs.superseded_by must reference a project_brief in the same tenant')
|
|
1524
|
+
END;
|
|
1525
|
+
END
|
|
1526
|
+
`);
|
|
1527
|
+
}
|
|
1528
|
+
},
|
|
1529
|
+
},
|
|
1530
|
+
{
|
|
1531
|
+
version: 36,
|
|
1532
|
+
up: (db) => {
|
|
1533
|
+
// E2 customer_note first-class object (the LAST E2 object)
|
|
1534
|
+
// (docs/plans/2026-06-01-e2-customer-note-object.md). A customer_note is a
|
|
1535
|
+
// discrete note recorded against an account/customer entity, evolving via the
|
|
1536
|
+
// v35 project_briefs supersede machinery (superseded_by self-FK + supersede
|
|
1537
|
+
// tenant-match trigger + version + change_summary). This table = the v35
|
|
1538
|
+
// project_briefs table with repo/summary replaced by `customer` (the
|
|
1539
|
+
// entity-scoping dimension; a free-form account/customer id - the entities
|
|
1540
|
+
// table is unbuilt E3.1, so a FK is deferred) PLUS `note` (the note body).
|
|
1541
|
+
// MANY notes per customer (each its own supersede chain), unlike the
|
|
1542
|
+
// one-summary-per-repo project_brief. All column names checked against SQLite
|
|
1543
|
+
// reserved words (skill-episode lesson, codebase-audit rule 10): customer/note/
|
|
1544
|
+
// version/status/etc. are non-reserved.
|
|
1545
|
+
if (!tableExists(db, 'customer_notes')) {
|
|
1546
|
+
db.exec(`
|
|
1547
|
+
CREATE TABLE customer_notes (
|
|
1548
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1549
|
+
memory_id TEXT,
|
|
1550
|
+
tenant_id TEXT NOT NULL,
|
|
1551
|
+
customer TEXT NOT NULL,
|
|
1552
|
+
note TEXT NOT NULL,
|
|
1553
|
+
version INTEGER NOT NULL DEFAULT 1,
|
|
1554
|
+
status TEXT NOT NULL DEFAULT 'active'
|
|
1555
|
+
CHECK (status IN ('active', 'superseded', 'closed')),
|
|
1556
|
+
superseded_by INTEGER,
|
|
1557
|
+
superseded_at TEXT,
|
|
1558
|
+
change_summary TEXT,
|
|
1559
|
+
closed_at TEXT,
|
|
1560
|
+
created_at TEXT NOT NULL,
|
|
1561
|
+
FOREIGN KEY (memory_id) REFERENCES memories(id) ON DELETE SET NULL,
|
|
1562
|
+
FOREIGN KEY (superseded_by) REFERENCES customer_notes(id) ON DELETE SET NULL
|
|
1563
|
+
)
|
|
1564
|
+
`);
|
|
1565
|
+
db.exec(`
|
|
1566
|
+
CREATE INDEX IF NOT EXISTS idx_customer_notes_tenant_status
|
|
1567
|
+
ON customer_notes(tenant_id, status)
|
|
1568
|
+
`);
|
|
1569
|
+
db.exec(`
|
|
1570
|
+
CREATE INDEX IF NOT EXISTS idx_customer_notes_memory
|
|
1571
|
+
ON customer_notes(memory_id) WHERE memory_id IS NOT NULL
|
|
1572
|
+
`);
|
|
1573
|
+
db.exec(`
|
|
1574
|
+
CREATE INDEX IF NOT EXISTS idx_customer_notes_customer
|
|
1575
|
+
ON customer_notes(tenant_id, customer, status)
|
|
1576
|
+
`);
|
|
1577
|
+
// Cross-tenant safety vs the referenced memory (verbatim mirror of the
|
|
1578
|
+
// v35 project_briefs tenant-match triggers).
|
|
1579
|
+
db.exec(`
|
|
1580
|
+
CREATE TRIGGER IF NOT EXISTS trg_customer_notes_tenant_match_insert
|
|
1581
|
+
BEFORE INSERT ON customer_notes
|
|
1582
|
+
WHEN NEW.memory_id IS NOT NULL
|
|
1583
|
+
BEGIN
|
|
1584
|
+
SELECT CASE
|
|
1585
|
+
WHEN NEW.tenant_id != (SELECT tenant_id FROM memories WHERE id = NEW.memory_id)
|
|
1586
|
+
THEN RAISE(ABORT, 'customer_notes.tenant_id must match memories.tenant_id for the referenced memory')
|
|
1587
|
+
END;
|
|
1588
|
+
END
|
|
1589
|
+
`);
|
|
1590
|
+
db.exec(`
|
|
1591
|
+
CREATE TRIGGER IF NOT EXISTS trg_customer_notes_tenant_match_update
|
|
1592
|
+
BEFORE UPDATE ON customer_notes
|
|
1593
|
+
WHEN NEW.memory_id IS NOT NULL
|
|
1594
|
+
AND (NEW.memory_id IS NOT OLD.memory_id OR NEW.tenant_id IS NOT OLD.tenant_id)
|
|
1595
|
+
BEGIN
|
|
1596
|
+
SELECT CASE
|
|
1597
|
+
WHEN NEW.tenant_id != (SELECT tenant_id FROM memories WHERE id = NEW.memory_id)
|
|
1598
|
+
THEN RAISE(ABORT, 'customer_notes.tenant_id must match memories.tenant_id for the referenced memory')
|
|
1599
|
+
END;
|
|
1600
|
+
END
|
|
1601
|
+
`);
|
|
1602
|
+
// Cross-tenant safety vs the successor note (self-FK; verbatim mirror of the
|
|
1603
|
+
// v35 project_briefs supersede trigger).
|
|
1604
|
+
db.exec(`
|
|
1605
|
+
CREATE TRIGGER IF NOT EXISTS trg_customer_notes_supersede_tenant_match_update
|
|
1606
|
+
BEFORE UPDATE ON customer_notes
|
|
1607
|
+
WHEN NEW.superseded_by IS NOT NULL
|
|
1608
|
+
AND NEW.superseded_by IS NOT OLD.superseded_by
|
|
1609
|
+
BEGIN
|
|
1610
|
+
SELECT CASE
|
|
1611
|
+
WHEN NEW.tenant_id != (SELECT tenant_id FROM customer_notes WHERE id = NEW.superseded_by)
|
|
1612
|
+
THEN RAISE(ABORT, 'customer_notes.superseded_by must reference a customer_note in the same tenant')
|
|
1613
|
+
END;
|
|
1614
|
+
END
|
|
1615
|
+
`);
|
|
1616
|
+
}
|
|
1617
|
+
},
|
|
1618
|
+
},
|
|
1619
|
+
{
|
|
1620
|
+
version: 37,
|
|
1621
|
+
up: (db) => {
|
|
1622
|
+
// E3.3 graph-on-consolidated guard (docs/plans/2026-06-01-e3-graph-guard.md).
|
|
1623
|
+
// The graph layer (entities + relations) sits ON TOP OF consolidated state and
|
|
1624
|
+
// must NEVER index the raw layer. The substrate: entities + relations +
|
|
1625
|
+
// graph_extraction_queue, each FK-ing to memories and guarded so they can only
|
|
1626
|
+
// reference CONSOLIDATED memories (kind IN ('distilled','superseded')), never
|
|
1627
|
+
// kind='raw'. New tables -> real CHECK constraints (unlike the ALTER'd memories,
|
|
1628
|
+
// whose kind CHECK lives in triggers). The kind/source MATCH (source_kind ==
|
|
1629
|
+
// the FK'd memory's actual kind) cannot be a CHECK (CHECK can't subquery), so it
|
|
1630
|
+
// is a BEFORE INSERT *and* BEFORE UPDATE trigger (the subquery-capable pattern
|
|
1631
|
+
// from the v30 decisions / predictions tenant-match triggers). Both INSERT and
|
|
1632
|
+
// UPDATE are guarded: an INSERT-only guard is bypassable via a raw SQL UPDATE
|
|
1633
|
+
// that moves a row onto a raw memory (plan-eng-critic 2026-06-01). All column
|
|
1634
|
+
// names checked vs SQL reserved words (rule 10): rel_type avoids REFERENCES.
|
|
1635
|
+
if (!tableExists(db, 'entities')) {
|
|
1636
|
+
db.exec(`
|
|
1637
|
+
CREATE TABLE entities (
|
|
1638
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1639
|
+
tenant_id TEXT NOT NULL,
|
|
1640
|
+
entity_type TEXT NOT NULL
|
|
1641
|
+
CHECK (entity_type IN ('person', 'project', 'customer', 'system', 'policy', 'decision')),
|
|
1642
|
+
name TEXT NOT NULL,
|
|
1643
|
+
memory_id TEXT NOT NULL,
|
|
1644
|
+
source_kind TEXT NOT NULL CHECK (source_kind IN ('distilled', 'superseded')),
|
|
1645
|
+
created_at TEXT NOT NULL,
|
|
1646
|
+
FOREIGN KEY (memory_id) REFERENCES memories(id) ON DELETE CASCADE
|
|
1647
|
+
)
|
|
1648
|
+
`);
|
|
1649
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_entities_tenant ON entities(tenant_id)`);
|
|
1650
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_entities_memory ON entities(memory_id)`);
|
|
1651
|
+
db.exec(`
|
|
1652
|
+
CREATE TABLE relations (
|
|
1653
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1654
|
+
tenant_id TEXT NOT NULL,
|
|
1655
|
+
from_entity_id INTEGER NOT NULL,
|
|
1656
|
+
to_entity_id INTEGER NOT NULL,
|
|
1657
|
+
rel_type TEXT NOT NULL
|
|
1658
|
+
CHECK (rel_type IN ('owns', 'supersedes', 'depends-on', 'blocked-by', 'references')),
|
|
1659
|
+
memory_id TEXT NOT NULL,
|
|
1660
|
+
source_kind TEXT NOT NULL CHECK (source_kind IN ('distilled', 'superseded')),
|
|
1661
|
+
created_at TEXT NOT NULL,
|
|
1662
|
+
FOREIGN KEY (from_entity_id) REFERENCES entities(id) ON DELETE CASCADE,
|
|
1663
|
+
FOREIGN KEY (to_entity_id) REFERENCES entities(id) ON DELETE CASCADE,
|
|
1664
|
+
FOREIGN KEY (memory_id) REFERENCES memories(id) ON DELETE CASCADE
|
|
1665
|
+
)
|
|
1666
|
+
`);
|
|
1667
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_relations_tenant ON relations(tenant_id)`);
|
|
1668
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_relations_from ON relations(from_entity_id)`);
|
|
1669
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_relations_to ON relations(to_entity_id)`);
|
|
1670
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_relations_memory ON relations(memory_id)`);
|
|
1671
|
+
db.exec(`
|
|
1672
|
+
CREATE TABLE graph_extraction_queue (
|
|
1673
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1674
|
+
tenant_id TEXT NOT NULL,
|
|
1675
|
+
memory_id TEXT NOT NULL,
|
|
1676
|
+
kind TEXT NOT NULL CHECK (kind IN ('distilled', 'superseded')),
|
|
1677
|
+
status TEXT NOT NULL DEFAULT 'pending'
|
|
1678
|
+
CHECK (status IN ('pending', 'processed', 'skipped')),
|
|
1679
|
+
enqueued_at TEXT NOT NULL,
|
|
1680
|
+
processed_at TEXT,
|
|
1681
|
+
FOREIGN KEY (memory_id) REFERENCES memories(id) ON DELETE CASCADE
|
|
1682
|
+
)
|
|
1683
|
+
`);
|
|
1684
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_graph_queue_status ON graph_extraction_queue(tenant_id, status)`);
|
|
1685
|
+
// entities guard: source_kind must equal the FK'd memory's actual kind (so a
|
|
1686
|
+
// raw memory or a lying source_kind both ABORT), and tenant must match. Both
|
|
1687
|
+
// INSERT and UPDATE (UPDATE fires when memory_id/source_kind/tenant_id change).
|
|
1688
|
+
db.exec(`
|
|
1689
|
+
CREATE TRIGGER IF NOT EXISTS trg_entities_consolidated_only_insert
|
|
1690
|
+
BEFORE INSERT ON entities
|
|
1691
|
+
BEGIN
|
|
1692
|
+
SELECT CASE
|
|
1693
|
+
WHEN NEW.source_kind != (SELECT kind FROM memories WHERE id = NEW.memory_id)
|
|
1694
|
+
THEN RAISE(ABORT, 'entities.source_kind must equal the referenced memory kind; the graph indexes consolidated state only (no raw)')
|
|
1695
|
+
WHEN NEW.tenant_id != (SELECT tenant_id FROM memories WHERE id = NEW.memory_id)
|
|
1696
|
+
THEN RAISE(ABORT, 'entities.tenant_id must match memories.tenant_id for the referenced memory')
|
|
1697
|
+
END;
|
|
1698
|
+
END
|
|
1699
|
+
`);
|
|
1700
|
+
db.exec(`
|
|
1701
|
+
CREATE TRIGGER IF NOT EXISTS trg_entities_consolidated_only_update
|
|
1702
|
+
BEFORE UPDATE ON entities
|
|
1703
|
+
WHEN NEW.memory_id IS NOT OLD.memory_id
|
|
1704
|
+
OR NEW.source_kind IS NOT OLD.source_kind
|
|
1705
|
+
OR NEW.tenant_id IS NOT OLD.tenant_id
|
|
1706
|
+
BEGIN
|
|
1707
|
+
SELECT CASE
|
|
1708
|
+
WHEN NEW.source_kind != (SELECT kind FROM memories WHERE id = NEW.memory_id)
|
|
1709
|
+
THEN RAISE(ABORT, 'entities.source_kind must equal the referenced memory kind; the graph indexes consolidated state only (no raw)')
|
|
1710
|
+
WHEN NEW.tenant_id != (SELECT tenant_id FROM memories WHERE id = NEW.memory_id)
|
|
1711
|
+
THEN RAISE(ABORT, 'entities.tenant_id must match memories.tenant_id for the referenced memory')
|
|
1712
|
+
END;
|
|
1713
|
+
END
|
|
1714
|
+
`);
|
|
1715
|
+
// relations guard: source_kind must equal the FK'd memory's kind; tenant must
|
|
1716
|
+
// match the memory AND both endpoint entities (no cross-tenant edges).
|
|
1717
|
+
db.exec(`
|
|
1718
|
+
CREATE TRIGGER IF NOT EXISTS trg_relations_consolidated_only_insert
|
|
1719
|
+
BEFORE INSERT ON relations
|
|
1720
|
+
BEGIN
|
|
1721
|
+
SELECT CASE
|
|
1722
|
+
WHEN NEW.source_kind != (SELECT kind FROM memories WHERE id = NEW.memory_id)
|
|
1723
|
+
THEN RAISE(ABORT, 'relations.source_kind must equal the referenced memory kind; the graph indexes consolidated state only (no raw)')
|
|
1724
|
+
WHEN NEW.tenant_id != (SELECT tenant_id FROM memories WHERE id = NEW.memory_id)
|
|
1725
|
+
THEN RAISE(ABORT, 'relations.tenant_id must match memories.tenant_id for the referenced memory')
|
|
1726
|
+
WHEN NEW.tenant_id != (SELECT tenant_id FROM entities WHERE id = NEW.from_entity_id)
|
|
1727
|
+
THEN RAISE(ABORT, 'relations.tenant_id must match the from_entity tenant (no cross-tenant edges)')
|
|
1728
|
+
WHEN NEW.tenant_id != (SELECT tenant_id FROM entities WHERE id = NEW.to_entity_id)
|
|
1729
|
+
THEN RAISE(ABORT, 'relations.tenant_id must match the to_entity tenant (no cross-tenant edges)')
|
|
1730
|
+
END;
|
|
1731
|
+
END
|
|
1732
|
+
`);
|
|
1733
|
+
db.exec(`
|
|
1734
|
+
CREATE TRIGGER IF NOT EXISTS trg_relations_consolidated_only_update
|
|
1735
|
+
BEFORE UPDATE ON relations
|
|
1736
|
+
WHEN NEW.memory_id IS NOT OLD.memory_id
|
|
1737
|
+
OR NEW.source_kind IS NOT OLD.source_kind
|
|
1738
|
+
OR NEW.tenant_id IS NOT OLD.tenant_id
|
|
1739
|
+
OR NEW.from_entity_id IS NOT OLD.from_entity_id
|
|
1740
|
+
OR NEW.to_entity_id IS NOT OLD.to_entity_id
|
|
1741
|
+
BEGIN
|
|
1742
|
+
SELECT CASE
|
|
1743
|
+
WHEN NEW.source_kind != (SELECT kind FROM memories WHERE id = NEW.memory_id)
|
|
1744
|
+
THEN RAISE(ABORT, 'relations.source_kind must equal the referenced memory kind; the graph indexes consolidated state only (no raw)')
|
|
1745
|
+
WHEN NEW.tenant_id != (SELECT tenant_id FROM memories WHERE id = NEW.memory_id)
|
|
1746
|
+
THEN RAISE(ABORT, 'relations.tenant_id must match memories.tenant_id for the referenced memory')
|
|
1747
|
+
WHEN NEW.tenant_id != (SELECT tenant_id FROM entities WHERE id = NEW.from_entity_id)
|
|
1748
|
+
THEN RAISE(ABORT, 'relations.tenant_id must match the from_entity tenant (no cross-tenant edges)')
|
|
1749
|
+
WHEN NEW.tenant_id != (SELECT tenant_id FROM entities WHERE id = NEW.to_entity_id)
|
|
1750
|
+
THEN RAISE(ABORT, 'relations.tenant_id must match the to_entity tenant (no cross-tenant edges)')
|
|
1751
|
+
END;
|
|
1752
|
+
END
|
|
1753
|
+
`);
|
|
1754
|
+
// graph_extraction_queue guard: kind must equal the FK'd memory's actual kind
|
|
1755
|
+
// (so a raw memory ABORTs), and tenant must match. INSERT and UPDATE.
|
|
1756
|
+
db.exec(`
|
|
1757
|
+
CREATE TRIGGER IF NOT EXISTS trg_graph_queue_consolidated_only_insert
|
|
1758
|
+
BEFORE INSERT ON graph_extraction_queue
|
|
1759
|
+
BEGIN
|
|
1760
|
+
SELECT CASE
|
|
1761
|
+
WHEN NEW.kind != (SELECT kind FROM memories WHERE id = NEW.memory_id)
|
|
1762
|
+
THEN RAISE(ABORT, 'graph_extraction_queue.kind must equal the referenced memory kind; only consolidated memories are queued (no raw)')
|
|
1763
|
+
WHEN NEW.tenant_id != (SELECT tenant_id FROM memories WHERE id = NEW.memory_id)
|
|
1764
|
+
THEN RAISE(ABORT, 'graph_extraction_queue.tenant_id must match memories.tenant_id for the referenced memory')
|
|
1765
|
+
END;
|
|
1766
|
+
END
|
|
1767
|
+
`);
|
|
1768
|
+
db.exec(`
|
|
1769
|
+
CREATE TRIGGER IF NOT EXISTS trg_graph_queue_consolidated_only_update
|
|
1770
|
+
BEFORE UPDATE ON graph_extraction_queue
|
|
1771
|
+
WHEN NEW.memory_id IS NOT OLD.memory_id
|
|
1772
|
+
OR NEW.kind IS NOT OLD.kind
|
|
1773
|
+
OR NEW.tenant_id IS NOT OLD.tenant_id
|
|
1774
|
+
BEGIN
|
|
1775
|
+
SELECT CASE
|
|
1776
|
+
WHEN NEW.kind != (SELECT kind FROM memories WHERE id = NEW.memory_id)
|
|
1777
|
+
THEN RAISE(ABORT, 'graph_extraction_queue.kind must equal the referenced memory kind; only consolidated memories are queued (no raw)')
|
|
1778
|
+
WHEN NEW.tenant_id != (SELECT tenant_id FROM memories WHERE id = NEW.memory_id)
|
|
1779
|
+
THEN RAISE(ABORT, 'graph_extraction_queue.tenant_id must match memories.tenant_id for the referenced memory')
|
|
1780
|
+
END;
|
|
1781
|
+
END
|
|
1782
|
+
`);
|
|
1783
|
+
// Reverse guard (codex-review-critic 2026-06-01, P1): the graph-table triggers
|
|
1784
|
+
// only fire on writes to the GRAPH tables. They do NOT fire when an
|
|
1785
|
+
// already-indexed memory is later mutated. So 'UPDATE memories SET kind=raw'
|
|
1786
|
+
// (or a tenant change) on a memory the graph references would silently leave
|
|
1787
|
+
// entity/relation/queue rows pointing at a raw / cross-tenant memory while
|
|
1788
|
+
// their source_kind stays 'distilled' - bypassing the central 'graph never
|
|
1789
|
+
// indexes raw' invariant after insertion. This trigger closes that direction:
|
|
1790
|
+
// a memory cannot be reclassified to raw, nor moved cross-tenant, WHILE the
|
|
1791
|
+
// graph references it (rebuild/remove the graph rows first). Cheap: the EXISTS
|
|
1792
|
+
// checks are only evaluated when kind actually becomes raw or tenant changes.
|
|
1793
|
+
db.exec(`
|
|
1794
|
+
CREATE TRIGGER IF NOT EXISTS trg_memories_graph_referenced_guard
|
|
1795
|
+
BEFORE UPDATE ON memories
|
|
1796
|
+
WHEN (NEW.kind IS NOT OLD.kind OR NEW.tenant_id IS NOT OLD.tenant_id)
|
|
1797
|
+
AND (
|
|
1798
|
+
EXISTS (SELECT 1 FROM entities WHERE memory_id = OLD.id)
|
|
1799
|
+
OR EXISTS (SELECT 1 FROM relations WHERE memory_id = OLD.id)
|
|
1800
|
+
OR EXISTS (SELECT 1 FROM graph_extraction_queue WHERE memory_id = OLD.id)
|
|
1801
|
+
)
|
|
1802
|
+
BEGIN
|
|
1803
|
+
SELECT RAISE(ABORT, 'cannot change the kind or tenant of a memory while the graph references it (E3.3 graph-on-consolidated guard); a graph-referenced memory is immutable in kind/tenant - rebuild/remove the graph rows first, or rebuild them after supersession');
|
|
1804
|
+
END
|
|
1805
|
+
`);
|
|
1806
|
+
// Reverse guard #2 (codex-review-critic 2026-06-01 retry, P2): an entity that is
|
|
1807
|
+
// a relation endpoint cannot be moved cross-tenant. The entity UPDATE trigger
|
|
1808
|
+
// validates the entity against its source memory, but an existing relation
|
|
1809
|
+
// pointing at the entity is NOT re-validated, so a raw 'UPDATE entities SET
|
|
1810
|
+
// tenant_id=?, memory_id=?' to another tenant would leave a tenant-A relation
|
|
1811
|
+
// pointing at a tenant-B entity. Block the tenant move while the entity is
|
|
1812
|
+
// referenced by any relation (rebuild the relations first).
|
|
1813
|
+
db.exec(`
|
|
1814
|
+
CREATE TRIGGER IF NOT EXISTS trg_entities_no_tenant_move_when_referenced
|
|
1815
|
+
BEFORE UPDATE ON entities
|
|
1816
|
+
WHEN NEW.tenant_id IS NOT OLD.tenant_id
|
|
1817
|
+
AND EXISTS (SELECT 1 FROM relations WHERE from_entity_id = OLD.id OR to_entity_id = OLD.id)
|
|
1818
|
+
BEGIN
|
|
1819
|
+
SELECT RAISE(ABORT, 'cannot move an entity cross-tenant while a relation references it as an endpoint (E3.3 graph-on-consolidated guard); rebuild/remove the relations first');
|
|
1820
|
+
END
|
|
1007
1821
|
`);
|
|
1008
1822
|
}
|
|
1009
1823
|
},
|
|
@@ -1101,11 +1915,11 @@ function runMigrations(db) {
|
|
|
1101
1915
|
ensureOptionalFts(db);
|
|
1102
1916
|
}
|
|
1103
1917
|
function ensureMetaTable(db) {
|
|
1104
|
-
db.exec(`
|
|
1105
|
-
CREATE TABLE IF NOT EXISTS meta (
|
|
1106
|
-
key TEXT PRIMARY KEY,
|
|
1107
|
-
value TEXT NOT NULL
|
|
1108
|
-
);
|
|
1918
|
+
db.exec(`
|
|
1919
|
+
CREATE TABLE IF NOT EXISTS meta (
|
|
1920
|
+
key TEXT PRIMARY KEY,
|
|
1921
|
+
value TEXT NOT NULL
|
|
1922
|
+
);
|
|
1109
1923
|
`);
|
|
1110
1924
|
}
|
|
1111
1925
|
export function getSchemaVersion(db) {
|
|
@@ -1148,17 +1962,17 @@ function backfillFtsIndex(db) {
|
|
|
1148
1962
|
const ftsCount = db.prepare(`SELECT COUNT(*) AS c FROM memories_fts`).get()?.c ?? 0;
|
|
1149
1963
|
if (memCount === ftsCount)
|
|
1150
1964
|
return;
|
|
1151
|
-
db.exec(`
|
|
1152
|
-
INSERT INTO memories_fts(id, content, tags)
|
|
1153
|
-
SELECT m.id, m.content, m.tags_json
|
|
1154
|
-
FROM memories m
|
|
1155
|
-
WHERE NOT EXISTS (
|
|
1156
|
-
SELECT 1 FROM memories_fts f WHERE f.id = m.id
|
|
1157
|
-
)
|
|
1965
|
+
db.exec(`
|
|
1966
|
+
INSERT INTO memories_fts(id, content, tags)
|
|
1967
|
+
SELECT m.id, m.content, m.tags_json
|
|
1968
|
+
FROM memories m
|
|
1969
|
+
WHERE NOT EXISTS (
|
|
1970
|
+
SELECT 1 FROM memories_fts f WHERE f.id = m.id
|
|
1971
|
+
)
|
|
1158
1972
|
`);
|
|
1159
|
-
db.exec(`
|
|
1160
|
-
DELETE FROM memories_fts
|
|
1161
|
-
WHERE id NOT IN (SELECT id FROM memories)
|
|
1973
|
+
db.exec(`
|
|
1974
|
+
DELETE FROM memories_fts
|
|
1975
|
+
WHERE id NOT IN (SELECT id FROM memories)
|
|
1162
1976
|
`);
|
|
1163
1977
|
}
|
|
1164
1978
|
export function closeHippoDb(db) {
|
|
@@ -1175,13 +1989,13 @@ export function isFtsAvailable(db) {
|
|
|
1175
1989
|
return getMeta(db, 'fts5_available', '0') === '1';
|
|
1176
1990
|
}
|
|
1177
1991
|
export function pruneConsolidationRuns(db, keep = 50) {
|
|
1178
|
-
db.prepare(`
|
|
1179
|
-
DELETE FROM consolidation_runs
|
|
1180
|
-
WHERE id NOT IN (
|
|
1181
|
-
SELECT id FROM consolidation_runs
|
|
1182
|
-
ORDER BY timestamp DESC, id DESC
|
|
1183
|
-
LIMIT ?
|
|
1184
|
-
)
|
|
1992
|
+
db.prepare(`
|
|
1993
|
+
DELETE FROM consolidation_runs
|
|
1994
|
+
WHERE id NOT IN (
|
|
1995
|
+
SELECT id FROM consolidation_runs
|
|
1996
|
+
ORDER BY timestamp DESC, id DESC
|
|
1997
|
+
LIMIT ?
|
|
1998
|
+
)
|
|
1185
1999
|
`).run(keep);
|
|
1186
2000
|
}
|
|
1187
2001
|
//# sourceMappingURL=db.js.map
|