hippo-memory 1.14.0 → 1.15.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/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 +173 -48
- package/dist/cli.js.map +1 -1
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +556 -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/server.d.ts.map +1 -1
- package/dist/server.js +161 -0
- package/dist/server.js.map +1 -1
- package/dist/src/audit.js.map +1 -1
- package/dist/src/cli.js +173 -48
- package/dist/src/cli.js.map +1 -1
- package/dist/src/db.js +556 -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/server.js +161 -0
- package/dist/src/server.js.map +1 -1
- 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/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 = 30;
|
|
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,142 @@ 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
|
|
1007
1091
|
`);
|
|
1008
1092
|
}
|
|
1009
1093
|
},
|
|
@@ -1101,11 +1185,11 @@ function runMigrations(db) {
|
|
|
1101
1185
|
ensureOptionalFts(db);
|
|
1102
1186
|
}
|
|
1103
1187
|
function ensureMetaTable(db) {
|
|
1104
|
-
db.exec(`
|
|
1105
|
-
CREATE TABLE IF NOT EXISTS meta (
|
|
1106
|
-
key TEXT PRIMARY KEY,
|
|
1107
|
-
value TEXT NOT NULL
|
|
1108
|
-
);
|
|
1188
|
+
db.exec(`
|
|
1189
|
+
CREATE TABLE IF NOT EXISTS meta (
|
|
1190
|
+
key TEXT PRIMARY KEY,
|
|
1191
|
+
value TEXT NOT NULL
|
|
1192
|
+
);
|
|
1109
1193
|
`);
|
|
1110
1194
|
}
|
|
1111
1195
|
export function getSchemaVersion(db) {
|
|
@@ -1148,17 +1232,17 @@ function backfillFtsIndex(db) {
|
|
|
1148
1232
|
const ftsCount = db.prepare(`SELECT COUNT(*) AS c FROM memories_fts`).get()?.c ?? 0;
|
|
1149
1233
|
if (memCount === ftsCount)
|
|
1150
1234
|
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
|
-
)
|
|
1235
|
+
db.exec(`
|
|
1236
|
+
INSERT INTO memories_fts(id, content, tags)
|
|
1237
|
+
SELECT m.id, m.content, m.tags_json
|
|
1238
|
+
FROM memories m
|
|
1239
|
+
WHERE NOT EXISTS (
|
|
1240
|
+
SELECT 1 FROM memories_fts f WHERE f.id = m.id
|
|
1241
|
+
)
|
|
1158
1242
|
`);
|
|
1159
|
-
db.exec(`
|
|
1160
|
-
DELETE FROM memories_fts
|
|
1161
|
-
WHERE id NOT IN (SELECT id FROM memories)
|
|
1243
|
+
db.exec(`
|
|
1244
|
+
DELETE FROM memories_fts
|
|
1245
|
+
WHERE id NOT IN (SELECT id FROM memories)
|
|
1162
1246
|
`);
|
|
1163
1247
|
}
|
|
1164
1248
|
export function closeHippoDb(db) {
|
|
@@ -1175,13 +1259,13 @@ export function isFtsAvailable(db) {
|
|
|
1175
1259
|
return getMeta(db, 'fts5_available', '0') === '1';
|
|
1176
1260
|
}
|
|
1177
1261
|
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
|
-
)
|
|
1262
|
+
db.prepare(`
|
|
1263
|
+
DELETE FROM consolidation_runs
|
|
1264
|
+
WHERE id NOT IN (
|
|
1265
|
+
SELECT id FROM consolidation_runs
|
|
1266
|
+
ORDER BY timestamp DESC, id DESC
|
|
1267
|
+
LIMIT ?
|
|
1268
|
+
)
|
|
1185
1269
|
`).run(keep);
|
|
1186
1270
|
}
|
|
1187
1271
|
//# sourceMappingURL=db.js.map
|