renor-engine 0.0.0-stage → 1.0.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.
Files changed (176) hide show
  1. package/LICENSE +661 -0
  2. package/NOTICE +73 -0
  3. package/README.md +105 -2
  4. package/dist/BUILD.json +6 -0
  5. package/dist/api/apiAuth.js +43 -0
  6. package/dist/api/server.js +172 -0
  7. package/dist/buildIdentity.js +132 -0
  8. package/dist/cli-extractors.js +46 -0
  9. package/dist/cli.js +491 -0
  10. package/dist/config.js +65 -0
  11. package/dist/context/composer.js +320 -0
  12. package/dist/context/readHonesty.js +478 -0
  13. package/dist/context/rowStore.js +238 -0
  14. package/dist/conversation/formatter.js +81 -0
  15. package/dist/conversation/parser.js +189 -0
  16. package/dist/conversation/types.js +2 -0
  17. package/dist/conversation/writer.js +42 -0
  18. package/dist/db/client.js +147 -0
  19. package/dist/db/indexSalvage.js +81 -0
  20. package/dist/db/migrations.js +642 -0
  21. package/dist/db/readCache.js +109 -0
  22. package/dist/db/schema.js +5 -0
  23. package/dist/db/statements.js +89 -0
  24. package/dist/digests/digestCache.js +142 -0
  25. package/dist/digests/digests.js +597 -0
  26. package/dist/digests/worker.js +348 -0
  27. package/dist/embeddings/caching.js +337 -0
  28. package/dist/embeddings/coalescing.js +177 -0
  29. package/dist/embeddings/interactive.js +103 -0
  30. package/dist/embeddings/local.js +68 -0
  31. package/dist/embeddings/openai.js +105 -0
  32. package/dist/embeddings/rebuild.js +166 -0
  33. package/dist/embeddings/simdScan.js +258 -0
  34. package/dist/embeddings/space.js +21 -0
  35. package/dist/embeddings/types.js +14 -0
  36. package/dist/embeddings/vectorIndex.js +675 -0
  37. package/dist/embeddings/vertex.js +224 -0
  38. package/dist/eval/artifacts.js +42 -0
  39. package/dist/eval/cacheReport.js +139 -0
  40. package/dist/eval/comparison.js +182 -0
  41. package/dist/eval/context700-candidate.js +145 -0
  42. package/dist/eval/evalCache.js +525 -0
  43. package/dist/eval/hashEmbedding.js +35 -0
  44. package/dist/eval/longmemeval.js +56 -0
  45. package/dist/eval/metrics.js +181 -0
  46. package/dist/eval/renorbenchBaseline.js +486 -0
  47. package/dist/eval/renorbenchDocuments.js +134 -0
  48. package/dist/eval/renorbenchProduct.js +182 -0
  49. package/dist/eval/runLedger.js +279 -0
  50. package/dist/eval/runManifest.js +479 -0
  51. package/dist/eval/scoring.js +197 -0
  52. package/dist/evidence/attach.js +267 -0
  53. package/dist/evidence/background.js +147 -0
  54. package/dist/evidence/evidenceCache.js +182 -0
  55. package/dist/evidence/gate.js +37 -0
  56. package/dist/evidence/keyPool.js +85 -0
  57. package/dist/evidence/ollamaChat.js +84 -0
  58. package/dist/evidence/optin.js +165 -0
  59. package/dist/evidence/options.js +130 -0
  60. package/dist/evidence/secondPass.js +456 -0
  61. package/dist/extraction/classifyCapture.js +34 -0
  62. package/dist/extraction/decision-model.js +174 -0
  63. package/dist/extraction/eventExtractor.js +113 -0
  64. package/dist/extraction/extractionClient.js +122 -0
  65. package/dist/extraction/factExtractor.js +46 -0
  66. package/dist/extraction/factsWritePath.js +179 -0
  67. package/dist/extraction/preferenceExtractor.js +153 -0
  68. package/dist/extraction/secretGuard.js +176 -0
  69. package/dist/extraction/secretScanner.js +665 -0
  70. package/dist/extraction/typedFacts.js +224 -0
  71. package/dist/extraction/writePathAdjudication.js +110 -0
  72. package/dist/importing/apply.js +644 -0
  73. package/dist/importing/bundle-write.js +80 -0
  74. package/dist/importing/cli.js +407 -0
  75. package/dist/importing/existing.js +49 -0
  76. package/dist/importing/facts.js +123 -0
  77. package/dist/importing/forgetImported.js +79 -0
  78. package/dist/importing/hosted.js +467 -0
  79. package/dist/importing/index.js +29 -0
  80. package/dist/importing/journalLog.js +96 -0
  81. package/dist/importing/markdown.js +98 -0
  82. package/dist/importing/plan.js +206 -0
  83. package/dist/importing/secretScreen.js +113 -0
  84. package/dist/importing/serialize.js +188 -0
  85. package/dist/importing/source.js +463 -0
  86. package/dist/importing/types.js +6 -0
  87. package/dist/indexer/chunker.js +319 -0
  88. package/dist/indexer/dailyEntries.js +63 -0
  89. package/dist/indexer/hasher.js +10 -0
  90. package/dist/indexer/laneRuns.js +48 -0
  91. package/dist/indexer/o200k.js +481 -0
  92. package/dist/indexer/o200k.json +8 -0
  93. package/dist/indexer/outsideRoot.js +77 -0
  94. package/dist/indexer/parser.js +167 -0
  95. package/dist/indexer/pipeline.js +1168 -0
  96. package/dist/indexer/staleness.js +201 -0
  97. package/dist/indexer/tokenizer.js +77 -0
  98. package/dist/indexer/walker.js +184 -0
  99. package/dist/mcp/hive.js +88 -0
  100. package/dist/mcp/requestErrors.js +47 -0
  101. package/dist/mcp/server.js +396 -0
  102. package/dist/memory/agentLease.js +43 -0
  103. package/dist/memory/confinedWrite.js +315 -0
  104. package/dist/memory/controls.js +73 -0
  105. package/dist/memory/dailyNote.js +265 -0
  106. package/dist/memory/durableAppend.js +57 -0
  107. package/dist/memory/factConflicts.js +203 -0
  108. package/dist/memory/factTerms.js +321 -0
  109. package/dist/memory/hiveNameHeader.cjs +10 -0
  110. package/dist/memory/inventory.js +55 -0
  111. package/dist/memory/invisibleText.js +20 -0
  112. package/dist/memory/lifecycle.js +1756 -0
  113. package/dist/memory/limits.js +13 -0
  114. package/dist/memory/requests.js +183 -0
  115. package/dist/memory/sourceName.js +200 -0
  116. package/dist/memory/whatsNew.js +158 -0
  117. package/dist/memory/writer.js +175 -0
  118. package/dist/memory/writerConflicts.js +85 -0
  119. package/dist/memorymap/memoryMap.js +1568 -0
  120. package/dist/memorymap/replacedJournal.js +58 -0
  121. package/dist/memorymap/replacements.js +405 -0
  122. package/dist/mvp/activity.js +86 -0
  123. package/dist/mvp/agentMemory.js +1744 -0
  124. package/dist/mvp/auth.js +531 -0
  125. package/dist/mvp/cli.js +73 -0
  126. package/dist/mvp/client-store.js +462 -0
  127. package/dist/mvp/compileCache.js +249 -0
  128. package/dist/mvp/compileSeed.js +27 -0
  129. package/dist/mvp/config.js +288 -0
  130. package/dist/mvp/connectionStatus.js +80 -0
  131. package/dist/mvp/contextRelevance.js +413 -0
  132. package/dist/mvp/engine.js +1065 -0
  133. package/dist/mvp/exportAll.js +359 -0
  134. package/dist/mvp/factLanePack.js +77 -0
  135. package/dist/mvp/factMarkers.js +49 -0
  136. package/dist/mvp/factsLane.js +203 -0
  137. package/dist/mvp/hiddenSpans.js +585 -0
  138. package/dist/mvp/history.js +413 -0
  139. package/dist/mvp/hitDates.js +31 -0
  140. package/dist/mvp/hitFloor.js +36 -0
  141. package/dist/mvp/identity.js +18 -0
  142. package/dist/mvp/idle.js +113 -0
  143. package/dist/mvp/memoryDir.js +98 -0
  144. package/dist/mvp/posthogMcp.js +210 -0
  145. package/dist/mvp/readScope.js +104 -0
  146. package/dist/mvp/readUsefulness.js +222 -0
  147. package/dist/mvp/sanitize.js +48 -0
  148. package/dist/mvp/schemaRejections.js +91 -0
  149. package/dist/mvp/scope.js +127 -0
  150. package/dist/mvp/server.js +2089 -0
  151. package/dist/mvp/serverCard.js +29 -0
  152. package/dist/mvp/storedSize.js +139 -0
  153. package/dist/mvp/telemetry.js +441 -0
  154. package/dist/retrieval/adviceLines.js +195 -0
  155. package/dist/retrieval/adviceQuestion.js +44 -0
  156. package/dist/retrieval/conversationShape.js +44 -0
  157. package/dist/retrieval/dailyEntryTime.js +55 -0
  158. package/dist/retrieval/entityLookup.js +113 -0
  159. package/dist/retrieval/factsFirst.js +188 -0
  160. package/dist/retrieval/keyword.js +84 -0
  161. package/dist/retrieval/ledgerEventTime.js +51 -0
  162. package/dist/retrieval/ledgerLiveness.js +83 -0
  163. package/dist/retrieval/ledgerPresentation.js +161 -0
  164. package/dist/retrieval/noteIdentity.js +57 -0
  165. package/dist/retrieval/orchestrator.js +1086 -0
  166. package/dist/retrieval/queryHelpers.js +142 -0
  167. package/dist/retrieval/questionSpan.js +475 -0
  168. package/dist/retrieval/readerEvents.js +690 -0
  169. package/dist/retrieval/readerLayout.js +503 -0
  170. package/dist/retrieval/relativeTimeHints.js +113 -0
  171. package/dist/retrieval/semantic.js +67 -0
  172. package/dist/router/routerCache.js +106 -0
  173. package/dist/router/sessionRouter.js +726 -0
  174. package/dist/toolRefusal.js +120 -0
  175. package/dist/watcher/watcher.js +75 -0
  176. package/package.json +120 -4
@@ -0,0 +1,642 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.runMigrations = runMigrations;
4
+ const migrations = [
5
+ {
6
+ version: 1,
7
+ name: 'initial_schema',
8
+ up: (db) => {
9
+ const ddl = [
10
+ `CREATE TABLE IF NOT EXISTS notes (
11
+ path TEXT PRIMARY KEY,
12
+ title TEXT NOT NULL,
13
+ kind TEXT,
14
+ note_hash TEXT NOT NULL,
15
+ modified_at TEXT NOT NULL,
16
+ frontmatter_json TEXT NOT NULL DEFAULT '{}'
17
+ )`,
18
+ `CREATE TABLE IF NOT EXISTS chunks (
19
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
20
+ note_path TEXT NOT NULL REFERENCES notes(path) ON DELETE CASCADE,
21
+ heading_path TEXT NOT NULL DEFAULT '',
22
+ text TEXT NOT NULL,
23
+ start_line INTEGER NOT NULL,
24
+ end_line INTEGER NOT NULL,
25
+ token_count INTEGER NOT NULL,
26
+ chunk_hash TEXT NOT NULL
27
+ )`,
28
+ `CREATE TABLE IF NOT EXISTS entities (
29
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
30
+ type TEXT NOT NULL,
31
+ canonical_name TEXT NOT NULL,
32
+ aliases_json TEXT NOT NULL DEFAULT '[]',
33
+ source_note TEXT NOT NULL REFERENCES notes(path) ON DELETE CASCADE,
34
+ confidence REAL NOT NULL DEFAULT 1.0,
35
+ updated_at TEXT NOT NULL
36
+ )`,
37
+ `CREATE TABLE IF NOT EXISTS facts (
38
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
39
+ subject_entity_id INTEGER REFERENCES entities(id) ON DELETE CASCADE,
40
+ predicate TEXT NOT NULL,
41
+ object_text TEXT,
42
+ object_entity_id INTEGER REFERENCES entities(id) ON DELETE SET NULL,
43
+ source_path TEXT NOT NULL,
44
+ source_chunk_id INTEGER REFERENCES chunks(id) ON DELETE SET NULL,
45
+ confidence REAL NOT NULL DEFAULT 1.0,
46
+ valid_from TEXT NOT NULL,
47
+ valid_to TEXT,
48
+ updated_at TEXT NOT NULL
49
+ )`,
50
+ `CREATE TABLE IF NOT EXISTS relations (
51
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
52
+ source_entity_id INTEGER NOT NULL REFERENCES entities(id) ON DELETE CASCADE,
53
+ relation TEXT NOT NULL,
54
+ target_entity_id INTEGER NOT NULL REFERENCES entities(id) ON DELETE CASCADE,
55
+ source_note TEXT NOT NULL,
56
+ confidence REAL NOT NULL DEFAULT 1.0
57
+ )`,
58
+ `CREATE TABLE IF NOT EXISTS write_events (
59
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
60
+ note_path TEXT NOT NULL,
61
+ action TEXT NOT NULL,
62
+ summary TEXT,
63
+ created_at TEXT NOT NULL
64
+ )`,
65
+ `CREATE INDEX IF NOT EXISTS idx_chunks_note ON chunks(note_path)`,
66
+ `CREATE INDEX IF NOT EXISTS idx_entities_name ON entities(canonical_name)`,
67
+ `CREATE INDEX IF NOT EXISTS idx_facts_source ON facts(source_path)`,
68
+ `CREATE INDEX IF NOT EXISTS idx_relations_source ON relations(source_entity_id)`,
69
+ `CREATE INDEX IF NOT EXISTS idx_relations_target ON relations(target_entity_id)`,
70
+ `CREATE INDEX IF NOT EXISTS idx_entities_source_note ON entities(source_note)`,
71
+ `CREATE INDEX IF NOT EXISTS idx_facts_subject ON facts(subject_entity_id)`,
72
+ ];
73
+ for (const sql of ddl)
74
+ db.prepare(sql).run();
75
+ },
76
+ },
77
+ {
78
+ version: 2,
79
+ name: 'uniqueness_constraints',
80
+ up: (db) => {
81
+ db.prepare(`CREATE UNIQUE INDEX IF NOT EXISTS idx_relations_unique ON relations(source_entity_id, relation, target_entity_id)`).run();
82
+ db.prepare(`CREATE UNIQUE INDEX IF NOT EXISTS idx_facts_unique ON facts(subject_entity_id, predicate, object_text)`).run();
83
+ },
84
+ },
85
+ {
86
+ version: 3,
87
+ name: 'fts5_chunks',
88
+ up: (db) => {
89
+ // Intentionally left as no-op — superseded by migration 4
90
+ },
91
+ },
92
+ {
93
+ version: 4,
94
+ name: 'fts5_chunks_fixed',
95
+ up: (db) => {
96
+ // Drop any partial v3 artefacts
97
+ db.prepare(`DROP TABLE IF EXISTS chunks_fts`).run();
98
+ db.prepare(`DROP TRIGGER IF EXISTS chunks_fts_insert`).run();
99
+ db.prepare(`DROP TRIGGER IF EXISTS chunks_fts_delete`).run();
100
+ db.prepare(`DROP TRIGGER IF EXISTS chunks_fts_update`).run();
101
+ db.prepare(`
102
+ CREATE VIRTUAL TABLE chunks_fts
103
+ USING fts5(text, heading_path, note_path UNINDEXED, content='chunks', content_rowid='id')
104
+ `).run();
105
+ // Populate from existing chunks
106
+ db.prepare(`INSERT INTO chunks_fts(rowid, text, heading_path, note_path) SELECT id, text, heading_path, note_path FROM chunks`).run();
107
+ // Triggers to keep FTS in sync with chunks table
108
+ db.prepare(`
109
+ CREATE TRIGGER chunks_fts_insert AFTER INSERT ON chunks BEGIN
110
+ INSERT INTO chunks_fts(rowid, text, heading_path, note_path) VALUES (new.id, new.text, new.heading_path, new.note_path);
111
+ END
112
+ `).run();
113
+ db.prepare(`
114
+ CREATE TRIGGER chunks_fts_delete AFTER DELETE ON chunks BEGIN
115
+ INSERT INTO chunks_fts(chunks_fts, rowid, text, heading_path, note_path) VALUES ('delete', old.id, old.text, old.heading_path, old.note_path);
116
+ END
117
+ `).run();
118
+ db.prepare(`
119
+ CREATE TRIGGER chunks_fts_update AFTER UPDATE ON chunks BEGIN
120
+ INSERT INTO chunks_fts(chunks_fts, rowid, text, heading_path, note_path) VALUES ('delete', old.id, old.text, old.heading_path, old.note_path);
121
+ INSERT INTO chunks_fts(rowid, text, heading_path, note_path) VALUES (new.id, new.text, new.heading_path, new.note_path);
122
+ END
123
+ `).run();
124
+ },
125
+ },
126
+ {
127
+ version: 5,
128
+ name: '5_facts_event_id',
129
+ up: (db) => {
130
+ const cols = db.prepare('PRAGMA table_info(facts)').all().map((c) => c.name);
131
+ if (!cols.includes('event_id')) {
132
+ db.exec('ALTER TABLE facts ADD COLUMN event_id TEXT');
133
+ db.exec('CREATE UNIQUE INDEX IF NOT EXISTS idx_facts_event ON facts(event_id)');
134
+ }
135
+ },
136
+ },
137
+ {
138
+ version: 6,
139
+ name: '6_lifecycle_event_identity',
140
+ up: (db) => {
141
+ // B1: uniqueness is event identity, not (subject, predicate, object).
142
+ // The v2 index made A→B→A supersession impossible (replay and even the
143
+ // live write threw UNIQUE) — closed history may legitimately repeat a
144
+ // value. Replace it with the engine's real invariant: at most ONE
145
+ // current (valid_to IS NULL) fact per (subject_entity_id, predicate).
146
+ db.prepare('DROP INDEX IF EXISTS idx_facts_unique').run();
147
+ const now = new Date().toISOString();
148
+ // Repair pre-existing violations of that invariant — SCOPED to canonical
149
+ // lifecycle facts (source_path '_mcp_'): only lifecycle streams are
150
+ // single-current by design. Extracted/frontmatter facts may legitimately
151
+ // hold multiple current values per (entity, predicate) and are NOT
152
+ // touched. Deterministically keep the newest current fact per
153
+ // (subject, predicate) and close the rest. NOTE (K12): this interim
154
+ // choice is made without ledger access (migrations are DB-only); the
155
+ // startup replay that follows canonicalizes every lifecycle stream from
156
+ // ledger order (reconcileStreamCurrent) BEFORE readiness, so any
157
+ // non-canonical survivor/valid_to this repair leaves is corrected there.
158
+ db.prepare(`
159
+ UPDATE facts SET valid_to = ?, updated_at = ?
160
+ WHERE source_path = '_mcp_' AND id IN (
161
+ SELECT id FROM (
162
+ SELECT f.id AS id,
163
+ ROW_NUMBER() OVER (
164
+ PARTITION BY f.subject_entity_id, f.predicate
165
+ ORDER BY f.valid_from DESC, f.id DESC
166
+ ) AS rn
167
+ FROM facts f WHERE f.valid_to IS NULL AND f.source_path = '_mcp_'
168
+ ) WHERE rn > 1
169
+ )
170
+ `).run(now, now);
171
+ db.prepare(`CREATE UNIQUE INDEX IF NOT EXISTS idx_facts_current ON facts(subject_entity_id, predicate) WHERE valid_to IS NULL AND source_path = '_mcp_'`).run();
172
+ // B1 replay idempotency: byte-identical events dedup, a reused event id
173
+ // with a conflicting payload fails explicitly instead of being skipped.
174
+ db.exec(`
175
+ CREATE TABLE IF NOT EXISTS applied_ledger_events (
176
+ event_id TEXT PRIMARY KEY,
177
+ digest TEXT NOT NULL,
178
+ applied_at TEXT NOT NULL
179
+ )
180
+ `);
181
+ },
182
+ },
183
+ {
184
+ version: 7,
185
+ name: '7_scope_lifecycle_current_index',
186
+ up: (db) => {
187
+ // Repair pass (Astra A1/Kimi K3): the first Gate1A candidate's migration 6
188
+ // scoped the single-current index to ALL facts, which silently rejected
189
+ // legitimate extracted/frontmatter facts sharing (entity, predicate) and
190
+ // (in its duplicate repair) closed unrelated current rows. Re-scope the
191
+ // index to canonical lifecycle facts only (source_path '_mcp_'). This
192
+ // also upgrades DBs that already applied the candidate's globally-scoped
193
+ // migration 6. Limitation (documented in GATE1A-EVIDENCE.md): duplicate
194
+ // non-lifecycle current rows that the candidate's global v6 repair
195
+ // already closed cannot be un-closed; recovery is a rebuild/reindex,
196
+ // which re-extracts them from the source notes.
197
+ db.prepare('DROP INDEX IF EXISTS idx_facts_current').run();
198
+ db.prepare(`CREATE UNIQUE INDEX IF NOT EXISTS idx_facts_current ON facts(subject_entity_id, predicate) WHERE valid_to IS NULL AND source_path = '_mcp_'`).run();
199
+ },
200
+ },
201
+ {
202
+ version: 8,
203
+ name: '8_facts_scope',
204
+ up: (db) => {
205
+ // Optional product scope on lifecycle facts. Missing/legacy = 'global'.
206
+ // Uniqueness becomes (subject_entity_id, predicate, scope) for current
207
+ // _mcp_ facts so the same subject+predicate can coexist across scopes.
208
+ const cols = db.prepare('PRAGMA table_info(facts)').all().map((c) => c.name);
209
+ if (!cols.includes('scope')) {
210
+ db.exec(`ALTER TABLE facts ADD COLUMN scope TEXT NOT NULL DEFAULT 'global'`);
211
+ }
212
+ db.prepare('DROP INDEX IF EXISTS idx_facts_current').run();
213
+ db.prepare(`CREATE UNIQUE INDEX IF NOT EXISTS idx_facts_current ON facts(subject_entity_id, predicate, scope) WHERE valid_to IS NULL AND source_path = '_mcp_'`).run();
214
+ },
215
+ },
216
+ {
217
+ version: 9,
218
+ name: '9_structured_rows',
219
+ up: (db) => {
220
+ // C0 (ADR-0003 §4): one provenance-backed structured-row store for the
221
+ // fact/event/preference extraction lanes. Every row traces to its source
222
+ // note via source_path FK ON DELETE CASCADE — the legacy `facts` table
223
+ // (v1) has no FK and the watcher's unlink handler orphans rows today
224
+ // (src/watcher/watcher.ts), so this migration also performs a one-time
225
+ // purge of already-orphaned legacy fact rows.
226
+ db.exec(`
227
+ CREATE TABLE IF NOT EXISTS structured_rows (
228
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
229
+ row_kind TEXT NOT NULL CHECK (row_kind IN ('fact', 'event', 'preference')),
230
+ source_path TEXT NOT NULL REFERENCES notes(path) ON DELETE CASCADE,
231
+ source_hash TEXT NOT NULL,
232
+ source_chunk_id INTEGER NOT NULL REFERENCES chunks(id) ON DELETE CASCADE,
233
+ scope TEXT NOT NULL DEFAULT 'global',
234
+ extraction_version TEXT NOT NULL,
235
+ valid_from TEXT,
236
+ valid_to TEXT,
237
+ payload_json TEXT NOT NULL,
238
+ dedupe_key TEXT NOT NULL,
239
+ created_at TEXT NOT NULL
240
+ )
241
+ `);
242
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_structured_rows_source ON structured_rows(source_path)`);
243
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_structured_rows_current ON structured_rows(row_kind, scope, dedupe_key) WHERE valid_to IS NULL`);
244
+ // One-time orphan purge: rows in the legacy (FK-less) `facts` table whose
245
+ // source note no longer resolves can never be composed (no provenance);
246
+ // remove them instead of carrying a permanent live defect.
247
+ const hasFacts = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='facts'").get();
248
+ if (hasFacts) {
249
+ db.prepare('DELETE FROM facts WHERE source_path NOT IN (SELECT path FROM notes)').run();
250
+ }
251
+ },
252
+ },
253
+ {
254
+ version: 10,
255
+ name: '10_structured_rows_confirm_count',
256
+ up: (db) => {
257
+ // C2 facts write-path: confirm count for dedupe adjudication (HYP card
258
+ // plan step 2 — dedupe on (value, NFKD-ASCII-lowercased object) with a
259
+ // confirm count). Identical re-extraction bumps the count instead of
260
+ // writing a duplicate row.
261
+ const cols = db.prepare('PRAGMA table_info(structured_rows)').all().map((c) => c.name);
262
+ if (!cols.includes('confirm_count')) {
263
+ db.exec('ALTER TABLE structured_rows ADD COLUMN confirm_count INTEGER NOT NULL DEFAULT 1');
264
+ }
265
+ },
266
+ },
267
+ {
268
+ version: 11,
269
+ name: '11_structured_rows_current_identity_unique',
270
+ up: (db) => {
271
+ // C2 review fix (P2): close the check-then-act race across `await
272
+ // model.decide` — concurrent same-identity inserts from the same source
273
+ // note can no longer double-insert. Partial index: only current rows
274
+ // (valid_to IS NULL) participate, so closed/superseded history may
275
+ // legitimately repeat an identity, and the source_path column keeps
276
+ // cross-note NULL-date coexistence intact. Pre-repair: deterministically
277
+ // close any pre-existing same-source duplicate current rows (keep the
278
+ // lowest id — the same row the dedupe adjudication reads first), so the
279
+ // index creation cannot fail on legacy data.
280
+ const now = new Date().toISOString();
281
+ db.prepare(`
282
+ UPDATE structured_rows SET valid_to = ?
283
+ WHERE valid_to IS NULL AND id IN (
284
+ SELECT id FROM (
285
+ SELECT r.id AS id,
286
+ ROW_NUMBER() OVER (
287
+ PARTITION BY r.row_kind, r.scope, r.dedupe_key, r.source_path
288
+ ORDER BY r.id
289
+ ) AS rn
290
+ FROM structured_rows r WHERE r.valid_to IS NULL
291
+ ) WHERE rn > 1
292
+ )
293
+ `).run(now);
294
+ db.prepare(`
295
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_structured_rows_current_identity
296
+ ON structured_rows(row_kind, scope, dedupe_key, source_path)
297
+ WHERE valid_to IS NULL
298
+ `).run();
299
+ },
300
+ },
301
+ {
302
+ version: 12,
303
+ name: '12_lane_runs',
304
+ up: (db) => {
305
+ // #189: the zero-yield half of the ADR-0003 §4.2 lane cache.
306
+ //
307
+ // The row-based half lives in structured_rows: a CURRENT row for
308
+ // (source_path, row_kind, source_hash, extraction_version) means "this
309
+ // lane is done for these bytes". A lane that RUNS and produces nothing
310
+ // leaves no row, so that half cannot record it and every later pass over
311
+ // the same unchanged note calls the extractor again — repeated model
312
+ // calls and rate-limit exposure for a note that yielded nothing.
313
+ //
314
+ // This table is the checkpoint of the last COMPLETED run of one lane on
315
+ // one note. It is deliberately NOT a row in structured_rows: that table
316
+ // is the scored side of the eval boundary, and a marker row there would
317
+ // be served as an extracted fact.
318
+ //
319
+ // `max_rows_written` is what a COMPLETED run's adjudicated writes
320
+ // returned, kept as a MAXIMUM over the runs of one key. It is deliberately
321
+ // NOT a count read back out of structured_rows: that was a snapshot of
322
+ // mutable DB state, and a delayed writer could overwrite a truthful
323
+ // nonzero record with a stale zero (primary review B1 at head aeb388e),
324
+ // suppressing the lane for bytes that do yield rows. It is also
325
+ // deliberately monotone (never lowered): two concurrent runs can disagree
326
+ // about the same bytes — the extractor is a model — and the later-finishing
327
+ // one must not be able to erase a positive outcome with a zero (review B1
328
+ // at head 9397d50). 0 therefore means "no completed run for these bytes
329
+ // ever wrote a row", which is the safe direction to skip on.
330
+ //
331
+ // Key shape is the same cache key plus the lane, so a zero-yield fact
332
+ // lane can never suppress events or preferences on the same bytes.
333
+ // Growth is bounded by the armed lanes being default-off and by the
334
+ // indexer only touching changed notes; one row per (note, lane,
335
+ // content-version) is the ceiling.
336
+ db.exec(`
337
+ CREATE TABLE IF NOT EXISTS lane_runs (
338
+ source_path TEXT NOT NULL REFERENCES notes(path) ON DELETE CASCADE,
339
+ row_kind TEXT NOT NULL CHECK (row_kind IN ('fact', 'event', 'preference')),
340
+ source_hash TEXT NOT NULL,
341
+ extraction_version TEXT NOT NULL,
342
+ max_rows_written INTEGER NOT NULL,
343
+ completed_at TEXT NOT NULL,
344
+ PRIMARY KEY (source_path, row_kind, source_hash, extraction_version)
345
+ )
346
+ `);
347
+ },
348
+ },
349
+ {
350
+ version: 13,
351
+ name: '13_note_identity_fts',
352
+ up: (db) => {
353
+ // An identifier that NAMES a note (slug, file name, frontmatter id) is not in
354
+ // chunk text, so chunks_fts cannot find it. Index it once, per note, with `-_.%`
355
+ // as token characters: `space-bunny-eval-1001` is then a single whole token and
356
+ // a query for it matches exactly that token — not `4242`, not `md`.
357
+ db.exec(`
358
+ CREATE VIRTUAL TABLE IF NOT EXISTS note_identity_fts
359
+ USING fts5(ident, tokenize = "unicode61 tokenchars '-_.%'")
360
+ `);
361
+ const ident = (n) => `(
362
+ ${n}.path || ' ' || CASE WHEN ${n}.path LIKE '%.md' THEN substr(${n}.path, 1, length(${n}.path) - 3) ELSE ${n}.path END
363
+ || ' ' || CASE WHEN json_valid(${n}.frontmatter_json)
364
+ THEN coalesce((SELECT group_concat(value, ' ') FROM json_tree(${n}.frontmatter_json) WHERE type = 'text'), '')
365
+ ELSE '' END
366
+ )`;
367
+ db.exec(`
368
+ DROP TRIGGER IF EXISTS note_identity_fts_insert;
369
+ DROP TRIGGER IF EXISTS note_identity_fts_update;
370
+ DROP TRIGGER IF EXISTS note_identity_fts_delete;
371
+ CREATE TRIGGER note_identity_fts_insert AFTER INSERT ON notes BEGIN
372
+ INSERT INTO note_identity_fts(rowid, ident) VALUES (new.rowid, ${ident('new')});
373
+ END;
374
+ CREATE TRIGGER note_identity_fts_update AFTER UPDATE OF path, frontmatter_json ON notes BEGIN
375
+ DELETE FROM note_identity_fts WHERE rowid = old.rowid;
376
+ INSERT INTO note_identity_fts(rowid, ident) VALUES (new.rowid, ${ident('new')});
377
+ END;
378
+ CREATE TRIGGER note_identity_fts_delete AFTER DELETE ON notes BEGIN
379
+ DELETE FROM note_identity_fts WHERE rowid = old.rowid;
380
+ END;
381
+ `);
382
+ db.exec(`DELETE FROM note_identity_fts`);
383
+ db.exec(`INSERT INTO note_identity_fts(rowid, ident) SELECT n.rowid, ${ident('n')} FROM notes n`);
384
+ },
385
+ },
386
+ {
387
+ version: 14,
388
+ name: '14_fact_terms',
389
+ up: (db) => {
390
+ // Lifecycle facts are found by the words in them. This inverted index (memory/factTerms.ts)
391
+ // lets a search read the rows of the query's words instead of every fact. The write path fills
392
+ // it in the transaction that writes a fact and ensureFactTerms fills it for facts that already
393
+ // exist, so the migration only creates the schema.
394
+ db.exec(`
395
+ CREATE TABLE IF NOT EXISTS fact_terms (
396
+ term TEXT NOT NULL,
397
+ scope TEXT NOT NULL,
398
+ fact_id INTEGER NOT NULL,
399
+ PRIMARY KEY (term, scope, fact_id)
400
+ ) WITHOUT ROWID
401
+ `);
402
+ // The current lifecycle facts in time order (every scope, and one scope), for a read of the newest
403
+ // with no query: read backwards, they need no sort. Ascending, so a new fact appends to them.
404
+ // Partial, so note-derived facts never touch them.
405
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_facts_lifecycle_recent ON facts(valid_from, id) WHERE valid_to IS NULL AND source_path = '_mcp_'`);
406
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_facts_lifecycle_scope_recent ON facts(scope, valid_from, id) WHERE valid_to IS NULL AND source_path = '_mcp_'`);
407
+ },
408
+ },
409
+ {
410
+ version: 15,
411
+ name: '15_notes_unfinished',
412
+ up: (db) => {
413
+ // A file whose last index pass did not finish (its embedding failed) keeps an empty note hash until a later
414
+ // pass finishes it (indexer/pipeline.ts). A search asks whether there is one (mvp/engine.ts, UNFINISHED_FILES):
415
+ // partial, so the answer comes from those few rows, usually none, not from every note.
416
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_notes_unfinished ON notes(path) WHERE note_hash = ''`);
417
+ },
418
+ },
419
+ {
420
+ version: 16,
421
+ name: '16_foreign_key_indexes',
422
+ up: (db) => {
423
+ // Every index pass replaces a note's entity, and a pass over an edited or appended note deletes the chunks it
424
+ // replaces. Each deleted row makes SQLite find the rows whose foreign keys point at it, and these three columns
425
+ // had no index, so every save read every fact (and every structured row) once per row it deleted. A table
426
+ // older than these columns has no such key to look up, and gets no index.
427
+ const has = (table, column) => db.prepare(`PRAGMA table_info(${table})`).all().some(c => c.name === column);
428
+ if (has('facts', 'source_chunk_id'))
429
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_facts_source_chunk ON facts(source_chunk_id)`);
430
+ if (has('facts', 'object_entity_id'))
431
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_facts_object_entity ON facts(object_entity_id)`);
432
+ if (has('structured_rows', 'source_chunk_id')) {
433
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_structured_rows_source_chunk ON structured_rows(source_chunk_id)`);
434
+ }
435
+ },
436
+ },
437
+ {
438
+ version: 17,
439
+ name: '17_retired_facts',
440
+ up: (db) => {
441
+ // A retired lifecycle fact (memory/lifecycle.ts, retireFact) leaves `facts` for this table, row and id unchanged,
442
+ // so no read of `facts` can return it and restore puts back the same row. Like `facts`, it is derived from the
443
+ // ledger's retire and restore events and rebuilt by a replay. `fact_retirements` lists those events, whose
444
+ // ledger text a search never shows (retrieval/ledgerLiveness.ts).
445
+ db.exec(`
446
+ CREATE TABLE IF NOT EXISTS retired_facts (
447
+ id INTEGER PRIMARY KEY,
448
+ subject_entity_id INTEGER NOT NULL,
449
+ predicate TEXT NOT NULL,
450
+ object_text TEXT,
451
+ confidence REAL NOT NULL,
452
+ valid_from TEXT NOT NULL,
453
+ valid_to TEXT,
454
+ updated_at TEXT NOT NULL,
455
+ event_id TEXT NOT NULL UNIQUE,
456
+ scope TEXT NOT NULL,
457
+ retire_event_id TEXT NOT NULL,
458
+ retired_at TEXT NOT NULL,
459
+ retired_by TEXT NOT NULL
460
+ )
461
+ `);
462
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_retired_facts_stream ON retired_facts(subject_entity_id, predicate)`);
463
+ db.exec(`
464
+ CREATE TABLE IF NOT EXISTS fact_retirements (
465
+ event_id TEXT PRIMARY KEY,
466
+ kind TEXT NOT NULL,
467
+ target TEXT NOT NULL,
468
+ at TEXT NOT NULL,
469
+ source TEXT NOT NULL
470
+ )
471
+ `);
472
+ },
473
+ },
474
+ {
475
+ version: 18,
476
+ name: '18_fact_author',
477
+ up: (db) => {
478
+ // Who wrote a lifecycle fact and a write event. Null on rows saved before a connection key had a name.
479
+ // A table this database does not have (a partial legacy schema) is left alone.
480
+ const add = (table, column) => {
481
+ const cols = db.prepare(`PRAGMA table_info(${table})`).all();
482
+ if (cols.length > 0 && !cols.some(c => c.name === column))
483
+ db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} TEXT`);
484
+ };
485
+ add('facts', 'author');
486
+ add('retired_facts', 'author');
487
+ add('write_events', 'author');
488
+ },
489
+ },
490
+ {
491
+ version: 19,
492
+ name: '19_whats_new_cursor',
493
+ up: (db) => {
494
+ const cols = db.prepare('PRAGMA table_info(write_events)').all();
495
+ if (cols.length > 0 && !cols.some(c => c.name === 'scope'))
496
+ db.exec('ALTER TABLE write_events ADD COLUMN scope TEXT');
497
+ db.exec(`CREATE TABLE IF NOT EXISTS agent_cursors (
498
+ agent TEXT NOT NULL,
499
+ scope TEXT NOT NULL,
500
+ last_at TEXT NOT NULL,
501
+ last_id INTEGER NOT NULL,
502
+ PRIMARY KEY (agent, scope)
503
+ )`);
504
+ },
505
+ },
506
+ {
507
+ version: 20,
508
+ name: '20_agent_requests',
509
+ up: (db) => {
510
+ // A request is a record. Nothing here runs a tool or writes a memory.
511
+ db.exec(`CREATE TABLE IF NOT EXISTS agent_requests (
512
+ id TEXT PRIMARY KEY,
513
+ scope TEXT,
514
+ from_agent TEXT NOT NULL,
515
+ to_agent TEXT NOT NULL,
516
+ question TEXT NOT NULL,
517
+ answer TEXT,
518
+ status TEXT NOT NULL CHECK (status IN ('open', 'answered', 'closed')),
519
+ created_at TEXT NOT NULL,
520
+ answered_at TEXT,
521
+ closed_at TEXT
522
+ )`);
523
+ },
524
+ },
525
+ {
526
+ version: 21,
527
+ name: '21_known_agents',
528
+ up: (db) => {
529
+ // The connection names (x-renor-agent, set by the gateway from each key's label) this workspace has seen:
530
+ // who ask_agent can address. ask_agent's hourly limit counts agent_requests by sender.
531
+ db.exec(`CREATE TABLE IF NOT EXISTS known_agents (
532
+ agent TEXT PRIMARY KEY,
533
+ first_seen TEXT NOT NULL
534
+ )`);
535
+ db.exec('CREATE INDEX IF NOT EXISTS agent_requests_from_at ON agent_requests (from_agent, created_at)');
536
+ },
537
+ },
538
+ {
539
+ version: 22,
540
+ name: '22_agent_leases',
541
+ up: (db) => {
542
+ // One live stdio process per hive name. The holder is pid:nonce; a dead pid may be replaced.
543
+ db.exec(`CREATE TABLE IF NOT EXISTS agent_leases (
544
+ agent TEXT PRIMARY KEY,
545
+ holder TEXT NOT NULL,
546
+ seen_at TEXT NOT NULL
547
+ )`);
548
+ },
549
+ },
550
+ {
551
+ version: 23,
552
+ name: '23_index_state',
553
+ up: (db) => {
554
+ // Small facts about the index itself that must survive a crash: `reindex_pending` is set when the rows of a moved
555
+ // folder were removed, and cleared only once the folder is indexed again (see outsideRoot.ts).
556
+ db.exec(`CREATE TABLE IF NOT EXISTS index_state (
557
+ key TEXT PRIMARY KEY,
558
+ value TEXT NOT NULL
559
+ )`);
560
+ },
561
+ },
562
+ {
563
+ // Versions 22 (agent_leases) and 23 (index_state) are taken on main; this clock migration follows them.
564
+ version: 24,
565
+ name: '24_note_index_clock',
566
+ up: (db) => {
567
+ // Observation time, independent of a note's content-date window and of rows indexing can replace/delete.
568
+ db.exec(`CREATE TABLE IF NOT EXISTS note_index_clock (
569
+ scope TEXT PRIMARY KEY,
570
+ observed_at TEXT NOT NULL
571
+ )`);
572
+ // Existing indexes already carry the indexer's own stamp in updated_at. Never seed from valid_from:
573
+ // that can be a content date in the future. Scope spellings stay compatible with savedScopeSqlPredicate.
574
+ db.exec(`INSERT INTO note_index_clock (scope, observed_at)
575
+ SELECT COALESCE(scope, 'global'), MAX(updated_at) FROM facts WHERE source_path <> '_mcp_'
576
+ GROUP BY COALESCE(scope, 'global')
577
+ ON CONFLICT(scope) DO UPDATE SET observed_at = MAX(note_index_clock.observed_at, excluded.observed_at)`);
578
+ },
579
+ },
580
+ {
581
+ version: 25,
582
+ name: '25_fact_statement_words',
583
+ up: (db) => {
584
+ // The words of the statement a lifecycle fact was saved with (memory/factTerms.ts `statementTerms`), so the facts
585
+ // lane can count them toward a question's words. Null on a fact with no statement (save_memory) and on rows saved
586
+ // before this column; the replay backfills those from the ledger (lifecycle.ts `backfillStatementWords`).
587
+ // A table this database does not have (a partial legacy schema) is left alone.
588
+ const add = (table, column) => {
589
+ const cols = db.prepare(`PRAGMA table_info(${table})`).all();
590
+ if (cols.length > 0 && !cols.some(c => c.name === column))
591
+ db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} TEXT`);
592
+ };
593
+ add('facts', 'statement_words');
594
+ add('retired_facts', 'statement_words');
595
+ },
596
+ },
597
+ {
598
+ version: 26,
599
+ name: '26_fact_writers',
600
+ up: (db) => {
601
+ // Who saved each lifecycle fact (memory/writerConflicts.ts): the writer its connection authenticated, and the values
602
+ // it replaced that their writers have not confirmed yet. Keyed by the fact's event, so retiring and restoring a
603
+ // fact (which keep the event) keep its row, and a replay of the ledger writes it again. A fact with no row was saved
604
+ // before this table: its writer is its source and it has no open displacements.
605
+ db.exec(`
606
+ CREATE TABLE IF NOT EXISTS fact_writers (
607
+ event_id TEXT PRIMARY KEY,
608
+ writer TEXT NOT NULL,
609
+ displaced TEXT NOT NULL DEFAULT '[]'
610
+ )
611
+ `);
612
+ },
613
+ },
614
+ ];
615
+ function runMigrations(db) {
616
+ db.prepare(`CREATE TABLE IF NOT EXISTS schema_version (version INTEGER NOT NULL)`).run();
617
+ const row = db.prepare('SELECT version FROM schema_version LIMIT 1').get();
618
+ let currentVersion = row?.version ?? 0;
619
+ // If DB has no version but has tables (existing install), assume v1
620
+ if (currentVersion === 0) {
621
+ const hasNotes = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='notes'").get();
622
+ if (hasNotes) {
623
+ migrations[0].up(db);
624
+ currentVersion = 1;
625
+ }
626
+ }
627
+ for (const migration of migrations) {
628
+ if (migration.version <= currentVersion)
629
+ continue;
630
+ db.transaction(() => {
631
+ migration.up(db);
632
+ if (currentVersion === 0 && migration.version === 1) {
633
+ db.prepare('INSERT INTO schema_version (version) VALUES (?)').run(migration.version);
634
+ }
635
+ else {
636
+ db.prepare('UPDATE schema_version SET version = ?').run(migration.version);
637
+ }
638
+ })();
639
+ currentVersion = migration.version;
640
+ console.error(`[migrations] applied: ${migration.version}_${migration.name}`);
641
+ }
642
+ }