@psiclawops/hypermem 0.5.0 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (163) hide show
  1. package/ARCHITECTURE.md +12 -3
  2. package/README.md +30 -6
  3. package/bin/hypermem-status.mjs +166 -0
  4. package/dist/background-indexer.d.ts +132 -0
  5. package/dist/background-indexer.d.ts.map +1 -0
  6. package/dist/background-indexer.js +1044 -0
  7. package/dist/cache.d.ts +110 -0
  8. package/dist/cache.d.ts.map +1 -0
  9. package/dist/cache.js +495 -0
  10. package/dist/compaction-fence.d.ts +89 -0
  11. package/dist/compaction-fence.d.ts.map +1 -0
  12. package/dist/compaction-fence.js +153 -0
  13. package/dist/compositor.d.ts +226 -0
  14. package/dist/compositor.d.ts.map +1 -0
  15. package/dist/compositor.js +2558 -0
  16. package/dist/content-type-classifier.d.ts +41 -0
  17. package/dist/content-type-classifier.d.ts.map +1 -0
  18. package/dist/content-type-classifier.js +181 -0
  19. package/dist/cross-agent.d.ts +62 -0
  20. package/dist/cross-agent.d.ts.map +1 -0
  21. package/dist/cross-agent.js +259 -0
  22. package/dist/db.d.ts +131 -0
  23. package/dist/db.d.ts.map +1 -0
  24. package/dist/db.js +402 -0
  25. package/dist/desired-state-store.d.ts +100 -0
  26. package/dist/desired-state-store.d.ts.map +1 -0
  27. package/dist/desired-state-store.js +222 -0
  28. package/dist/doc-chunk-store.d.ts +140 -0
  29. package/dist/doc-chunk-store.d.ts.map +1 -0
  30. package/dist/doc-chunk-store.js +391 -0
  31. package/dist/doc-chunker.d.ts +99 -0
  32. package/dist/doc-chunker.d.ts.map +1 -0
  33. package/dist/doc-chunker.js +324 -0
  34. package/dist/dreaming-promoter.d.ts +86 -0
  35. package/dist/dreaming-promoter.d.ts.map +1 -0
  36. package/dist/dreaming-promoter.js +381 -0
  37. package/dist/episode-store.d.ts +49 -0
  38. package/dist/episode-store.d.ts.map +1 -0
  39. package/dist/episode-store.js +135 -0
  40. package/dist/fact-store.d.ts +75 -0
  41. package/dist/fact-store.d.ts.map +1 -0
  42. package/dist/fact-store.js +236 -0
  43. package/dist/fleet-store.d.ts +144 -0
  44. package/dist/fleet-store.d.ts.map +1 -0
  45. package/dist/fleet-store.js +276 -0
  46. package/dist/fos-mod.d.ts +178 -0
  47. package/dist/fos-mod.d.ts.map +1 -0
  48. package/dist/fos-mod.js +416 -0
  49. package/dist/hybrid-retrieval.d.ts +64 -0
  50. package/dist/hybrid-retrieval.d.ts.map +1 -0
  51. package/dist/hybrid-retrieval.js +344 -0
  52. package/dist/image-eviction.d.ts +49 -0
  53. package/dist/image-eviction.d.ts.map +1 -0
  54. package/dist/image-eviction.js +251 -0
  55. package/dist/index.d.ts +650 -0
  56. package/dist/index.d.ts.map +1 -0
  57. package/dist/index.js +1072 -0
  58. package/dist/keystone-scorer.d.ts +51 -0
  59. package/dist/keystone-scorer.d.ts.map +1 -0
  60. package/dist/keystone-scorer.js +52 -0
  61. package/dist/knowledge-graph.d.ts +110 -0
  62. package/dist/knowledge-graph.d.ts.map +1 -0
  63. package/dist/knowledge-graph.js +305 -0
  64. package/dist/knowledge-lint.d.ts +29 -0
  65. package/dist/knowledge-lint.d.ts.map +1 -0
  66. package/dist/knowledge-lint.js +116 -0
  67. package/dist/knowledge-store.d.ts +72 -0
  68. package/dist/knowledge-store.d.ts.map +1 -0
  69. package/dist/knowledge-store.js +247 -0
  70. package/dist/library-schema.d.ts +22 -0
  71. package/dist/library-schema.d.ts.map +1 -0
  72. package/dist/library-schema.js +1038 -0
  73. package/dist/message-store.d.ts +89 -0
  74. package/dist/message-store.d.ts.map +1 -0
  75. package/dist/message-store.js +323 -0
  76. package/dist/metrics-dashboard.d.ts +114 -0
  77. package/dist/metrics-dashboard.d.ts.map +1 -0
  78. package/dist/metrics-dashboard.js +260 -0
  79. package/dist/obsidian-exporter.d.ts +57 -0
  80. package/dist/obsidian-exporter.d.ts.map +1 -0
  81. package/dist/obsidian-exporter.js +274 -0
  82. package/dist/obsidian-watcher.d.ts +147 -0
  83. package/dist/obsidian-watcher.d.ts.map +1 -0
  84. package/dist/obsidian-watcher.js +403 -0
  85. package/dist/open-domain.d.ts +46 -0
  86. package/dist/open-domain.d.ts.map +1 -0
  87. package/dist/open-domain.js +125 -0
  88. package/dist/preference-store.d.ts +54 -0
  89. package/dist/preference-store.d.ts.map +1 -0
  90. package/dist/preference-store.js +109 -0
  91. package/dist/preservation-gate.d.ts +82 -0
  92. package/dist/preservation-gate.d.ts.map +1 -0
  93. package/dist/preservation-gate.js +150 -0
  94. package/dist/proactive-pass.d.ts +63 -0
  95. package/dist/proactive-pass.d.ts.map +1 -0
  96. package/dist/proactive-pass.js +239 -0
  97. package/dist/profiles.d.ts +44 -0
  98. package/dist/profiles.d.ts.map +1 -0
  99. package/dist/profiles.js +227 -0
  100. package/dist/provider-translator.d.ts +50 -0
  101. package/dist/provider-translator.d.ts.map +1 -0
  102. package/dist/provider-translator.js +403 -0
  103. package/dist/rate-limiter.d.ts +76 -0
  104. package/dist/rate-limiter.d.ts.map +1 -0
  105. package/dist/rate-limiter.js +179 -0
  106. package/dist/repair-tool-pairs.d.ts +38 -0
  107. package/dist/repair-tool-pairs.d.ts.map +1 -0
  108. package/dist/repair-tool-pairs.js +138 -0
  109. package/dist/retrieval-policy.d.ts +51 -0
  110. package/dist/retrieval-policy.d.ts.map +1 -0
  111. package/dist/retrieval-policy.js +77 -0
  112. package/dist/schema.d.ts +15 -0
  113. package/dist/schema.d.ts.map +1 -0
  114. package/dist/schema.js +229 -0
  115. package/dist/secret-scanner.d.ts +51 -0
  116. package/dist/secret-scanner.d.ts.map +1 -0
  117. package/dist/secret-scanner.js +248 -0
  118. package/dist/seed.d.ts +108 -0
  119. package/dist/seed.d.ts.map +1 -0
  120. package/dist/seed.js +177 -0
  121. package/dist/session-flusher.d.ts +53 -0
  122. package/dist/session-flusher.d.ts.map +1 -0
  123. package/dist/session-flusher.js +69 -0
  124. package/dist/session-topic-map.d.ts +41 -0
  125. package/dist/session-topic-map.d.ts.map +1 -0
  126. package/dist/session-topic-map.js +77 -0
  127. package/dist/spawn-context.d.ts +54 -0
  128. package/dist/spawn-context.d.ts.map +1 -0
  129. package/dist/spawn-context.js +159 -0
  130. package/dist/system-store.d.ts +73 -0
  131. package/dist/system-store.d.ts.map +1 -0
  132. package/dist/system-store.js +182 -0
  133. package/dist/temporal-store.d.ts +80 -0
  134. package/dist/temporal-store.d.ts.map +1 -0
  135. package/dist/temporal-store.js +149 -0
  136. package/dist/topic-detector.d.ts +35 -0
  137. package/dist/topic-detector.d.ts.map +1 -0
  138. package/dist/topic-detector.js +249 -0
  139. package/dist/topic-store.d.ts +45 -0
  140. package/dist/topic-store.d.ts.map +1 -0
  141. package/dist/topic-store.js +136 -0
  142. package/dist/topic-synthesizer.d.ts +51 -0
  143. package/dist/topic-synthesizer.d.ts.map +1 -0
  144. package/dist/topic-synthesizer.js +315 -0
  145. package/dist/trigger-registry.d.ts +63 -0
  146. package/dist/trigger-registry.d.ts.map +1 -0
  147. package/dist/trigger-registry.js +163 -0
  148. package/dist/types.d.ts +537 -0
  149. package/dist/types.d.ts.map +1 -0
  150. package/dist/types.js +9 -0
  151. package/dist/vector-store.d.ts +170 -0
  152. package/dist/vector-store.d.ts.map +1 -0
  153. package/dist/vector-store.js +677 -0
  154. package/dist/version.d.ts +34 -0
  155. package/dist/version.d.ts.map +1 -0
  156. package/dist/version.js +34 -0
  157. package/dist/wiki-page-emitter.d.ts +65 -0
  158. package/dist/wiki-page-emitter.d.ts.map +1 -0
  159. package/dist/wiki-page-emitter.js +258 -0
  160. package/dist/work-store.d.ts +112 -0
  161. package/dist/work-store.d.ts.map +1 -0
  162. package/dist/work-store.js +273 -0
  163. package/package.json +4 -1
@@ -0,0 +1,1038 @@
1
+ /**
2
+ * hypermem Library Schema — Fleet-Wide Structured Knowledge
3
+ *
4
+ * Single database: ~/.openclaw/hypermem/library.db
5
+ * The "crown jewel" — durable, backed up, low-write-frequency.
6
+ *
7
+ * Collections:
8
+ * 1. Library entries (versioned docs, specs, reference material)
9
+ * 2. Facts (agent-learned truths)
10
+ * 3. Preferences (behavioral patterns)
11
+ * 4. Knowledge (structured domain knowledge, supersedable)
12
+ * 5. Episodes (significant events)
13
+ * 6. Fleet registry (agents, orgs)
14
+ * 7. System registry (server state, config)
15
+ * 8. Session registry (lifecycle tracking)
16
+ * 9. Work items (fleet kanban)
17
+ * 10. Topics (cross-session thread tracking)
18
+ */
19
+ export const LIBRARY_SCHEMA_VERSION = 13;
20
+ function nowIso() {
21
+ return new Date().toISOString();
22
+ }
23
+ // ── V1: Original library + subscriptions + changelog ──────────
24
+ function applyV1Schema(db) {
25
+ db.exec(`
26
+ CREATE TABLE IF NOT EXISTS library_entries (
27
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
28
+ domain TEXT NOT NULL,
29
+ key TEXT NOT NULL,
30
+ content TEXT NOT NULL,
31
+ content_hash TEXT,
32
+ version INTEGER DEFAULT 1,
33
+ source TEXT,
34
+ agent_id TEXT,
35
+ visibility TEXT DEFAULT 'fleet',
36
+ tags TEXT,
37
+ created_at TEXT NOT NULL,
38
+ updated_at TEXT NOT NULL,
39
+ superseded_at TEXT,
40
+ superseded_by INTEGER,
41
+ UNIQUE(domain, key, version)
42
+ )
43
+ `);
44
+ db.exec('CREATE INDEX IF NOT EXISTS idx_lib_entries_domain ON library_entries(domain, key)');
45
+ db.exec('CREATE INDEX IF NOT EXISTS idx_lib_entries_active ON library_entries(domain, key, superseded_by)');
46
+ db.exec(`
47
+ CREATE TABLE IF NOT EXISTS library_changelog (
48
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
49
+ library_entry_id INTEGER NOT NULL REFERENCES library_entries(id),
50
+ change_type TEXT NOT NULL,
51
+ changed_by TEXT NOT NULL,
52
+ diff_summary TEXT,
53
+ version INTEGER NOT NULL,
54
+ created_at TEXT NOT NULL
55
+ )
56
+ `);
57
+ db.exec('CREATE INDEX IF NOT EXISTS idx_lib_changelog_item ON library_changelog(library_entry_id, created_at DESC)');
58
+ db.exec(`
59
+ CREATE TABLE IF NOT EXISTS library_subscriptions (
60
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
61
+ agent_id TEXT NOT NULL,
62
+ domain TEXT,
63
+ item_type TEXT,
64
+ created_at TEXT NOT NULL,
65
+ UNIQUE(agent_id, domain, item_type)
66
+ )
67
+ `);
68
+ // FTS on library content
69
+ db.exec(`
70
+ CREATE VIRTUAL TABLE IF NOT EXISTS library_fts USING fts5(
71
+ key,
72
+ content,
73
+ content='library_entries',
74
+ content_rowid='id'
75
+ )
76
+ `);
77
+ db.exec(`
78
+ CREATE TRIGGER IF NOT EXISTS lib_fts_ai AFTER INSERT ON library_entries BEGIN
79
+ INSERT INTO library_fts(rowid, key, content) VALUES (new.id, new.key, new.content);
80
+ END
81
+ `);
82
+ db.exec(`
83
+ CREATE TRIGGER IF NOT EXISTS lib_fts_ad AFTER DELETE ON library_entries BEGIN
84
+ INSERT INTO library_fts(library_fts, rowid, key, content) VALUES('delete', old.id, old.key, old.content);
85
+ END
86
+ `);
87
+ db.exec(`
88
+ CREATE TRIGGER IF NOT EXISTS lib_fts_au AFTER UPDATE ON library_entries BEGIN
89
+ INSERT INTO library_fts(library_fts, rowid, key, content) VALUES('delete', old.id, old.key, old.content);
90
+ INSERT INTO library_fts(rowid, key, content) VALUES (new.id, new.key, new.content);
91
+ END
92
+ `);
93
+ }
94
+ // ── V2: Session registry ──────────────────────────────────────
95
+ function applyV2SessionRegistry(db) {
96
+ db.exec(`
97
+ CREATE TABLE IF NOT EXISTS session_registry (
98
+ id TEXT PRIMARY KEY,
99
+ agent_id TEXT NOT NULL,
100
+ channel TEXT,
101
+ channel_type TEXT,
102
+ started_at TEXT NOT NULL,
103
+ ended_at TEXT,
104
+ status TEXT DEFAULT 'active',
105
+ summary TEXT,
106
+ decisions_made INTEGER DEFAULT 0,
107
+ facts_extracted INTEGER DEFAULT 0,
108
+ messages_count INTEGER DEFAULT 0
109
+ )
110
+ `);
111
+ db.exec('CREATE INDEX IF NOT EXISTS idx_session_agent ON session_registry(agent_id, status, started_at DESC)');
112
+ db.exec('CREATE INDEX IF NOT EXISTS idx_session_status ON session_registry(status, started_at DESC)');
113
+ db.exec(`
114
+ CREATE TABLE IF NOT EXISTS session_events (
115
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
116
+ session_id TEXT NOT NULL REFERENCES session_registry(id),
117
+ event_type TEXT NOT NULL,
118
+ timestamp TEXT NOT NULL,
119
+ payload TEXT
120
+ )
121
+ `);
122
+ db.exec('CREATE INDEX IF NOT EXISTS idx_session_events ON session_events(session_id, timestamp DESC)');
123
+ db.exec('CREATE INDEX IF NOT EXISTS idx_session_events_type ON session_events(event_type, timestamp DESC)');
124
+ db.exec(`
125
+ CREATE VIRTUAL TABLE IF NOT EXISTS session_fts USING fts5(
126
+ summary,
127
+ content='session_registry',
128
+ content_rowid='rowid'
129
+ )
130
+ `);
131
+ }
132
+ // ── V3: Centralized collections ───────────────────────────────
133
+ // Facts, preferences, knowledge, episodes, topics move here from per-agent DBs.
134
+ // Fleet registry, system registry, work items are new.
135
+ function applyV3Collections(db) {
136
+ // ── Facts (agent-learned truths) ──
137
+ db.exec(`
138
+ CREATE TABLE IF NOT EXISTS facts (
139
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
140
+ agent_id TEXT NOT NULL,
141
+ scope TEXT NOT NULL DEFAULT 'agent',
142
+ domain TEXT,
143
+ content TEXT NOT NULL,
144
+ confidence REAL DEFAULT 1.0,
145
+ visibility TEXT NOT NULL DEFAULT 'private',
146
+ source_type TEXT DEFAULT 'conversation',
147
+ source_session_key TEXT,
148
+ source_ref TEXT,
149
+ created_at TEXT NOT NULL,
150
+ updated_at TEXT NOT NULL,
151
+ expires_at TEXT,
152
+ superseded_by INTEGER,
153
+ decay_score REAL DEFAULT 0.0
154
+ )
155
+ `);
156
+ db.exec('CREATE INDEX IF NOT EXISTS idx_facts_agent ON facts(agent_id, scope, domain)');
157
+ db.exec('CREATE INDEX IF NOT EXISTS idx_facts_visibility ON facts(visibility, agent_id)');
158
+ db.exec('CREATE INDEX IF NOT EXISTS idx_facts_active ON facts(agent_id, superseded_by, decay_score, confidence DESC)');
159
+ db.exec(`
160
+ CREATE VIRTUAL TABLE IF NOT EXISTS facts_fts USING fts5(
161
+ content,
162
+ domain,
163
+ content='facts',
164
+ content_rowid='id'
165
+ )
166
+ `);
167
+ db.exec(`
168
+ CREATE TRIGGER IF NOT EXISTS facts_fts_ai AFTER INSERT ON facts BEGIN
169
+ INSERT INTO facts_fts(rowid, content, domain) VALUES (new.id, new.content, new.domain);
170
+ END
171
+ `);
172
+ db.exec(`
173
+ CREATE TRIGGER IF NOT EXISTS facts_fts_ad AFTER DELETE ON facts BEGIN
174
+ INSERT INTO facts_fts(facts_fts, rowid, content, domain) VALUES('delete', old.id, old.content, old.domain);
175
+ END
176
+ `);
177
+ db.exec(`
178
+ CREATE TRIGGER IF NOT EXISTS facts_fts_au AFTER UPDATE ON facts BEGIN
179
+ INSERT INTO facts_fts(facts_fts, rowid, content, domain) VALUES('delete', old.id, old.content, old.domain);
180
+ INSERT INTO facts_fts(rowid, content, domain) VALUES (new.id, new.content, new.domain);
181
+ END
182
+ `);
183
+ // ── Preferences (behavioral patterns) ──
184
+ db.exec(`
185
+ CREATE TABLE IF NOT EXISTS preferences (
186
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
187
+ subject TEXT NOT NULL,
188
+ domain TEXT NOT NULL DEFAULT 'general',
189
+ key TEXT NOT NULL,
190
+ value TEXT NOT NULL,
191
+ agent_id TEXT NOT NULL,
192
+ confidence REAL DEFAULT 1.0,
193
+ visibility TEXT NOT NULL DEFAULT 'fleet',
194
+ source_type TEXT DEFAULT 'observation',
195
+ source_ref TEXT,
196
+ created_at TEXT NOT NULL,
197
+ updated_at TEXT NOT NULL,
198
+ UNIQUE(subject, domain, key)
199
+ )
200
+ `);
201
+ db.exec('CREATE INDEX IF NOT EXISTS idx_prefs_subject ON preferences(subject, domain)');
202
+ db.exec('CREATE INDEX IF NOT EXISTS idx_prefs_agent ON preferences(agent_id)');
203
+ // ── Knowledge (structured domain knowledge, supersedable) ──
204
+ db.exec(`
205
+ CREATE TABLE IF NOT EXISTS knowledge (
206
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
207
+ agent_id TEXT NOT NULL,
208
+ domain TEXT NOT NULL,
209
+ key TEXT NOT NULL,
210
+ content TEXT NOT NULL,
211
+ confidence REAL DEFAULT 1.0,
212
+ visibility TEXT NOT NULL DEFAULT 'private',
213
+ source_type TEXT NOT NULL DEFAULT 'manual',
214
+ source_ref TEXT,
215
+ created_at TEXT NOT NULL,
216
+ updated_at TEXT NOT NULL,
217
+ expires_at TEXT,
218
+ superseded_by INTEGER,
219
+ UNIQUE(agent_id, domain, key)
220
+ )
221
+ `);
222
+ db.exec('CREATE INDEX IF NOT EXISTS idx_knowledge_agent ON knowledge(agent_id, domain)');
223
+ db.exec('CREATE INDEX IF NOT EXISTS idx_knowledge_visibility ON knowledge(visibility, agent_id)');
224
+ db.exec(`
225
+ CREATE VIRTUAL TABLE IF NOT EXISTS knowledge_fts USING fts5(
226
+ key,
227
+ content,
228
+ domain,
229
+ content='knowledge',
230
+ content_rowid='id'
231
+ )
232
+ `);
233
+ db.exec(`
234
+ CREATE TRIGGER IF NOT EXISTS knowledge_fts_ai AFTER INSERT ON knowledge BEGIN
235
+ INSERT INTO knowledge_fts(rowid, key, content, domain) VALUES (new.id, new.key, new.content, new.domain);
236
+ END
237
+ `);
238
+ db.exec(`
239
+ CREATE TRIGGER IF NOT EXISTS knowledge_fts_ad AFTER DELETE ON knowledge BEGIN
240
+ INSERT INTO knowledge_fts(knowledge_fts, rowid, key, content, domain) VALUES('delete', old.id, old.key, old.content, old.domain);
241
+ END
242
+ `);
243
+ db.exec(`
244
+ CREATE TRIGGER IF NOT EXISTS knowledge_fts_au AFTER UPDATE ON knowledge BEGIN
245
+ INSERT INTO knowledge_fts(knowledge_fts, rowid, key, content, domain) VALUES('delete', old.id, old.key, old.content, old.domain);
246
+ INSERT INTO knowledge_fts(rowid, key, content, domain) VALUES (new.id, new.key, new.content, new.domain);
247
+ END
248
+ `);
249
+ // ── Knowledge relationships (DAG edges) ──
250
+ db.exec(`
251
+ CREATE TABLE IF NOT EXISTS knowledge_links (
252
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
253
+ from_type TEXT NOT NULL,
254
+ from_id INTEGER NOT NULL,
255
+ to_type TEXT NOT NULL,
256
+ to_id INTEGER NOT NULL,
257
+ link_type TEXT NOT NULL,
258
+ created_at TEXT NOT NULL,
259
+ UNIQUE(from_type, from_id, to_type, to_id, link_type)
260
+ )
261
+ `);
262
+ db.exec('CREATE INDEX IF NOT EXISTS idx_klinks_from ON knowledge_links(from_type, from_id)');
263
+ db.exec('CREATE INDEX IF NOT EXISTS idx_klinks_to ON knowledge_links(to_type, to_id)');
264
+ // ── Episodes (significant events) ──
265
+ db.exec(`
266
+ CREATE TABLE IF NOT EXISTS episodes (
267
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
268
+ agent_id TEXT NOT NULL,
269
+ event_type TEXT NOT NULL,
270
+ summary TEXT NOT NULL,
271
+ significance REAL NOT NULL DEFAULT 0.5,
272
+ visibility TEXT NOT NULL DEFAULT 'org',
273
+ participants TEXT,
274
+ session_key TEXT,
275
+ created_at TEXT NOT NULL,
276
+ decay_score REAL DEFAULT 0.0
277
+ )
278
+ `);
279
+ db.exec('CREATE INDEX IF NOT EXISTS idx_episodes_agent ON episodes(agent_id, significance DESC, created_at DESC)');
280
+ db.exec('CREATE INDEX IF NOT EXISTS idx_episodes_visibility ON episodes(visibility, agent_id)');
281
+ db.exec(`
282
+ CREATE VIRTUAL TABLE IF NOT EXISTS episodes_fts USING fts5(
283
+ summary,
284
+ event_type,
285
+ content='episodes',
286
+ content_rowid='id'
287
+ )
288
+ `);
289
+ db.exec(`
290
+ CREATE TRIGGER IF NOT EXISTS episodes_fts_ai AFTER INSERT ON episodes BEGIN
291
+ INSERT INTO episodes_fts(rowid, summary, event_type) VALUES (new.id, new.summary, new.event_type);
292
+ END
293
+ `);
294
+ db.exec(`
295
+ CREATE TRIGGER IF NOT EXISTS episodes_fts_ad AFTER DELETE ON episodes BEGIN
296
+ INSERT INTO episodes_fts(episodes_fts, rowid, summary, event_type) VALUES('delete', old.id, old.summary, old.event_type);
297
+ END
298
+ `);
299
+ db.exec(`
300
+ CREATE TRIGGER IF NOT EXISTS episodes_fts_au AFTER UPDATE ON episodes BEGIN
301
+ INSERT INTO episodes_fts(episodes_fts, rowid, summary, event_type) VALUES('delete', old.id, old.summary, old.event_type);
302
+ INSERT INTO episodes_fts(rowid, summary, event_type) VALUES (new.id, new.summary, new.event_type);
303
+ END
304
+ `);
305
+ // ── Topics (cross-session thread tracking) ──
306
+ db.exec(`
307
+ CREATE TABLE IF NOT EXISTS topics (
308
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
309
+ agent_id TEXT NOT NULL,
310
+ name TEXT NOT NULL,
311
+ description TEXT,
312
+ status TEXT DEFAULT 'active',
313
+ visibility TEXT NOT NULL DEFAULT 'org',
314
+ last_session_key TEXT,
315
+ message_count INTEGER DEFAULT 0,
316
+ created_at TEXT NOT NULL,
317
+ updated_at TEXT NOT NULL
318
+ )
319
+ `);
320
+ db.exec('CREATE INDEX IF NOT EXISTS idx_topics_agent ON topics(agent_id, status, updated_at DESC)');
321
+ // ── Fleet registry ──
322
+ db.exec(`
323
+ CREATE TABLE IF NOT EXISTS fleet_agents (
324
+ id TEXT PRIMARY KEY,
325
+ display_name TEXT NOT NULL,
326
+ tier TEXT NOT NULL DEFAULT 'unknown',
327
+ org_id TEXT,
328
+ reports_to TEXT,
329
+ domains TEXT,
330
+ session_keys TEXT,
331
+ status TEXT DEFAULT 'active',
332
+ last_seen TEXT,
333
+ created_at TEXT NOT NULL,
334
+ updated_at TEXT NOT NULL,
335
+ metadata TEXT
336
+ )
337
+ `);
338
+ db.exec('CREATE INDEX IF NOT EXISTS idx_fleet_agents_tier ON fleet_agents(tier, status)');
339
+ db.exec('CREATE INDEX IF NOT EXISTS idx_fleet_agents_org ON fleet_agents(org_id)');
340
+ db.exec(`
341
+ CREATE TABLE IF NOT EXISTS fleet_orgs (
342
+ id TEXT PRIMARY KEY,
343
+ name TEXT NOT NULL,
344
+ lead_agent_id TEXT REFERENCES fleet_agents(id),
345
+ mission TEXT,
346
+ created_at TEXT NOT NULL
347
+ )
348
+ `);
349
+ // ── System registry ──
350
+ db.exec(`
351
+ CREATE TABLE IF NOT EXISTS system_state (
352
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
353
+ category TEXT NOT NULL,
354
+ key TEXT NOT NULL,
355
+ value TEXT NOT NULL,
356
+ updated_at TEXT NOT NULL,
357
+ updated_by TEXT,
358
+ ttl TEXT,
359
+ UNIQUE(category, key)
360
+ )
361
+ `);
362
+ db.exec('CREATE INDEX IF NOT EXISTS idx_system_state_cat ON system_state(category)');
363
+ db.exec(`
364
+ CREATE TABLE IF NOT EXISTS system_events (
365
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
366
+ category TEXT NOT NULL,
367
+ key TEXT NOT NULL,
368
+ event_type TEXT NOT NULL,
369
+ old_value TEXT,
370
+ new_value TEXT,
371
+ agent_id TEXT,
372
+ created_at TEXT NOT NULL,
373
+ metadata TEXT
374
+ )
375
+ `);
376
+ db.exec('CREATE INDEX IF NOT EXISTS idx_system_events ON system_events(category, key, created_at DESC)');
377
+ // ── Work items (fleet kanban) ──
378
+ db.exec(`
379
+ CREATE TABLE IF NOT EXISTS work_items (
380
+ id TEXT PRIMARY KEY,
381
+ title TEXT NOT NULL,
382
+ description TEXT,
383
+ status TEXT NOT NULL DEFAULT 'incoming',
384
+ priority INTEGER NOT NULL DEFAULT 3,
385
+ agent_id TEXT,
386
+ created_by TEXT NOT NULL,
387
+ domain TEXT,
388
+ parent_id TEXT,
389
+ blocked_by TEXT,
390
+ session_key TEXT,
391
+ created_at TEXT NOT NULL,
392
+ updated_at TEXT NOT NULL,
393
+ started_at TEXT,
394
+ completed_at TEXT,
395
+ due_at TEXT,
396
+ metadata TEXT
397
+ )
398
+ `);
399
+ db.exec('CREATE INDEX IF NOT EXISTS idx_work_status ON work_items(status, priority)');
400
+ db.exec('CREATE INDEX IF NOT EXISTS idx_work_agent ON work_items(agent_id, status)');
401
+ db.exec('CREATE INDEX IF NOT EXISTS idx_work_domain ON work_items(domain, status)');
402
+ db.exec('CREATE INDEX IF NOT EXISTS idx_work_parent ON work_items(parent_id)');
403
+ db.exec(`
404
+ CREATE TABLE IF NOT EXISTS work_events (
405
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
406
+ work_item_id TEXT NOT NULL REFERENCES work_items(id),
407
+ event_type TEXT NOT NULL,
408
+ old_status TEXT,
409
+ new_status TEXT,
410
+ agent_id TEXT,
411
+ comment TEXT,
412
+ created_at TEXT NOT NULL
413
+ )
414
+ `);
415
+ db.exec('CREATE INDEX IF NOT EXISTS idx_work_events ON work_events(work_item_id, created_at DESC)');
416
+ db.exec(`
417
+ CREATE VIRTUAL TABLE IF NOT EXISTS work_items_fts USING fts5(
418
+ title,
419
+ description,
420
+ content='work_items',
421
+ content_rowid='rowid'
422
+ )
423
+ `);
424
+ db.exec(`
425
+ CREATE TRIGGER IF NOT EXISTS work_fts_ai AFTER INSERT ON work_items BEGIN
426
+ INSERT INTO work_items_fts(rowid, title, description) VALUES (new.rowid, new.title, new.description);
427
+ END
428
+ `);
429
+ db.exec(`
430
+ CREATE TRIGGER IF NOT EXISTS work_fts_ad AFTER DELETE ON work_items BEGIN
431
+ INSERT INTO work_items_fts(work_items_fts, rowid, title, description) VALUES('delete', old.rowid, old.title, old.description);
432
+ END
433
+ `);
434
+ db.exec(`
435
+ CREATE TRIGGER IF NOT EXISTS work_fts_au AFTER UPDATE ON work_items BEGIN
436
+ INSERT INTO work_items_fts(work_items_fts, rowid, title, description) VALUES('delete', old.rowid, old.title, old.description);
437
+ INSERT INTO work_items_fts(rowid, title, description) VALUES (new.rowid, new.title, new.description);
438
+ END
439
+ `);
440
+ }
441
+ // ── V4: Agent capabilities ────────────────────────────────────
442
+ // Skills, tools, MCP servers registered per agent for fleet-wide discoverability.
443
+ function applyV4Capabilities(db) {
444
+ // Add capabilities column to fleet_agents
445
+ db.exec('ALTER TABLE fleet_agents ADD COLUMN capabilities TEXT');
446
+ // Structured capabilities table for queryable skill/tool lookups
447
+ db.exec(`
448
+ CREATE TABLE IF NOT EXISTS agent_capabilities (
449
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
450
+ agent_id TEXT NOT NULL REFERENCES fleet_agents(id),
451
+ cap_type TEXT NOT NULL,
452
+ name TEXT NOT NULL,
453
+ version TEXT,
454
+ source TEXT,
455
+ config TEXT,
456
+ status TEXT DEFAULT 'active',
457
+ last_verified TEXT,
458
+ created_at TEXT NOT NULL,
459
+ updated_at TEXT NOT NULL,
460
+ UNIQUE(agent_id, cap_type, name)
461
+ )
462
+ `);
463
+ db.exec('CREATE INDEX IF NOT EXISTS idx_agent_caps_agent ON agent_capabilities(agent_id, cap_type)');
464
+ db.exec('CREATE INDEX IF NOT EXISTS idx_agent_caps_type ON agent_capabilities(cap_type, name)');
465
+ db.exec('CREATE INDEX IF NOT EXISTS idx_agent_caps_status ON agent_capabilities(status, cap_type)');
466
+ }
467
+ // ── V5: Agent desired state ───────────────────────────────────
468
+ // Stores intended configuration for each agent: model, thinking, provider, etc.
469
+ // Enables drift detection (desired vs actual) and fleet-wide config visibility.
470
+ function applyV5DesiredState(db) {
471
+ db.exec(`
472
+ CREATE TABLE IF NOT EXISTS agent_desired_state (
473
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
474
+ agent_id TEXT NOT NULL,
475
+ config_key TEXT NOT NULL,
476
+ desired_value TEXT NOT NULL,
477
+ actual_value TEXT,
478
+ source TEXT NOT NULL DEFAULT 'operator',
479
+ set_by TEXT,
480
+ drift_status TEXT DEFAULT 'unknown',
481
+ last_checked TEXT,
482
+ created_at TEXT NOT NULL,
483
+ updated_at TEXT NOT NULL,
484
+ notes TEXT,
485
+ UNIQUE(agent_id, config_key)
486
+ )
487
+ `);
488
+ db.exec('CREATE INDEX IF NOT EXISTS idx_desired_agent ON agent_desired_state(agent_id)');
489
+ db.exec('CREATE INDEX IF NOT EXISTS idx_desired_drift ON agent_desired_state(drift_status)');
490
+ db.exec('CREATE INDEX IF NOT EXISTS idx_desired_key ON agent_desired_state(config_key)');
491
+ // Change log for desired state modifications
492
+ db.exec(`
493
+ CREATE TABLE IF NOT EXISTS agent_config_events (
494
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
495
+ agent_id TEXT NOT NULL,
496
+ config_key TEXT NOT NULL,
497
+ event_type TEXT NOT NULL,
498
+ old_value TEXT,
499
+ new_value TEXT,
500
+ changed_by TEXT,
501
+ created_at TEXT NOT NULL
502
+ )
503
+ `);
504
+ db.exec('CREATE INDEX IF NOT EXISTS idx_config_events_agent ON agent_config_events(agent_id, config_key, created_at DESC)');
505
+ }
506
+ // ── V6: Document chunks ───────────────────────────────────────
507
+ // Stores chunked ACA workspace documents for semantic retrieval.
508
+ // Enables ACA offload: governance docs, identity files, memory → demand-loaded.
509
+ //
510
+ // Key design:
511
+ // - Each chunk has a source_hash — atomic re-indexing via hash-based swap
512
+ // - collection path mirrors ACA_COLLECTIONS (governance/policy, etc.)
513
+ // - scope: shared-fleet | per-tier | per-agent
514
+ // - FTS5 virtual table for keyword fallback when no embedder configured
515
+ function applyV6DocChunks(db) {
516
+ db.exec(`
517
+ CREATE TABLE IF NOT EXISTS doc_chunks (
518
+ id TEXT PRIMARY KEY,
519
+ collection TEXT NOT NULL,
520
+ section_path TEXT NOT NULL,
521
+ depth INTEGER NOT NULL DEFAULT 2,
522
+ content TEXT NOT NULL,
523
+ token_estimate INTEGER NOT NULL DEFAULT 0,
524
+ source_hash TEXT NOT NULL,
525
+ source_path TEXT NOT NULL,
526
+ scope TEXT NOT NULL DEFAULT 'shared-fleet',
527
+ tier TEXT,
528
+ agent_id TEXT,
529
+ parent_path TEXT,
530
+ created_at TEXT NOT NULL,
531
+ updated_at TEXT NOT NULL
532
+ )
533
+ `);
534
+ db.exec('CREATE INDEX IF NOT EXISTS idx_doc_chunks_collection ON doc_chunks(collection, scope)');
535
+ db.exec('CREATE INDEX IF NOT EXISTS idx_doc_chunks_agent ON doc_chunks(agent_id, collection)');
536
+ db.exec('CREATE INDEX IF NOT EXISTS idx_doc_chunks_hash ON doc_chunks(source_hash)');
537
+ db.exec('CREATE INDEX IF NOT EXISTS idx_doc_chunks_source ON doc_chunks(source_path)');
538
+ // Source file tracking: one row per indexed file
539
+ // Used to detect when a file has changed and needs re-indexing
540
+ db.exec(`
541
+ CREATE TABLE IF NOT EXISTS doc_sources (
542
+ source_path TEXT NOT NULL,
543
+ collection TEXT NOT NULL,
544
+ scope TEXT NOT NULL DEFAULT 'shared-fleet',
545
+ agent_id TEXT,
546
+ source_hash TEXT NOT NULL,
547
+ chunk_count INTEGER NOT NULL DEFAULT 0,
548
+ indexed_at TEXT NOT NULL,
549
+ PRIMARY KEY (source_path, collection)
550
+ )
551
+ `);
552
+ db.exec('CREATE INDEX IF NOT EXISTS idx_doc_sources_collection ON doc_sources(collection)');
553
+ db.exec('CREATE INDEX IF NOT EXISTS idx_doc_sources_agent ON doc_sources(agent_id)');
554
+ // FTS5 for keyword-based fallback retrieval
555
+ db.exec(`
556
+ CREATE VIRTUAL TABLE IF NOT EXISTS doc_chunks_fts USING fts5(
557
+ content,
558
+ section_path,
559
+ collection,
560
+ content='doc_chunks',
561
+ content_rowid='rowid'
562
+ )
563
+ `);
564
+ db.exec(`
565
+ CREATE TRIGGER IF NOT EXISTS doc_chunks_fts_ai AFTER INSERT ON doc_chunks BEGIN
566
+ INSERT INTO doc_chunks_fts(rowid, content, section_path, collection)
567
+ VALUES (new.rowid, new.content, new.section_path, new.collection);
568
+ END
569
+ `);
570
+ db.exec(`
571
+ CREATE TRIGGER IF NOT EXISTS doc_chunks_fts_ad AFTER DELETE ON doc_chunks BEGIN
572
+ INSERT INTO doc_chunks_fts(doc_chunks_fts, rowid, content, section_path, collection)
573
+ VALUES ('delete', old.rowid, old.content, old.section_path, old.collection);
574
+ END
575
+ `);
576
+ db.exec(`
577
+ CREATE TRIGGER IF NOT EXISTS doc_chunks_fts_au AFTER UPDATE ON doc_chunks BEGIN
578
+ INSERT INTO doc_chunks_fts(doc_chunks_fts, rowid, content, section_path, collection)
579
+ VALUES ('delete', old.rowid, old.content, old.section_path, old.collection);
580
+ INSERT INTO doc_chunks_fts(rowid, content, section_path, collection)
581
+ VALUES (new.rowid, new.content, new.section_path, new.collection);
582
+ END
583
+ `);
584
+ }
585
+ // ── V7: Fix knowledge versioning ─────────────────────────────
586
+ // The V1 knowledge table had UNIQUE(agent_id, domain, key) which prevented
587
+ // true versioning — upsert would overwrite in-place, creating self-superseding rows.
588
+ //
589
+ // V7 recreates the knowledge table with:
590
+ // - version INTEGER NOT NULL DEFAULT 1
591
+ // - UNIQUE(agent_id, domain, key, version) — allows multiple versions per key
592
+ // - Preserves existing data (current rows become version 1)
593
+ function applyV7KnowledgeVersioning(db) {
594
+ // Rename existing table
595
+ db.exec('ALTER TABLE knowledge RENAME TO knowledge_v6');
596
+ // Create new table with versioned unique constraint
597
+ db.exec(`
598
+ CREATE TABLE knowledge (
599
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
600
+ agent_id TEXT NOT NULL,
601
+ domain TEXT NOT NULL,
602
+ key TEXT NOT NULL,
603
+ version INTEGER NOT NULL DEFAULT 1,
604
+ content TEXT NOT NULL,
605
+ confidence REAL NOT NULL DEFAULT 1.0,
606
+ visibility TEXT NOT NULL DEFAULT 'private',
607
+ source_type TEXT NOT NULL DEFAULT 'manual',
608
+ source_ref TEXT,
609
+ created_at TEXT NOT NULL,
610
+ updated_at TEXT NOT NULL,
611
+ expires_at TEXT,
612
+ superseded_by INTEGER,
613
+ UNIQUE(agent_id, domain, key, version)
614
+ )
615
+ `);
616
+ // Recreate indexes
617
+ db.exec('CREATE INDEX IF NOT EXISTS idx_knowledge_agent ON knowledge(agent_id, domain, key)');
618
+ db.exec('CREATE INDEX IF NOT EXISTS idx_knowledge_active ON knowledge(agent_id, superseded_by)');
619
+ // Migrate existing data (all become version 1, preserve visibility)
620
+ db.exec(`
621
+ INSERT INTO knowledge (id, agent_id, domain, key, version, content, confidence, visibility,
622
+ source_type, source_ref, created_at, updated_at, expires_at, superseded_by)
623
+ SELECT id, agent_id, domain, key, 1, content, confidence,
624
+ COALESCE(visibility, 'private'),
625
+ source_type, source_ref, created_at, updated_at, expires_at, superseded_by
626
+ FROM knowledge_v6
627
+ `);
628
+ // Drop old table
629
+ db.exec('DROP TABLE knowledge_v6');
630
+ // Recreate FTS5 virtual table (was created in V3 but references knowledge)
631
+ // FTS tables can't be migrated — drop and recreate
632
+ try {
633
+ db.exec('DROP TABLE IF EXISTS knowledge_fts');
634
+ }
635
+ catch { /* ignore */ }
636
+ db.exec(`
637
+ CREATE VIRTUAL TABLE IF NOT EXISTS knowledge_fts USING fts5(
638
+ content,
639
+ domain,
640
+ key,
641
+ content='knowledge',
642
+ content_rowid='id'
643
+ )
644
+ `);
645
+ // Repopulate FTS index from migrated data
646
+ db.exec(`INSERT INTO knowledge_fts(rowid, content, domain, key) SELECT id, content, domain, key FROM knowledge`);
647
+ // Recreate triggers
648
+ db.exec(`
649
+ CREATE TRIGGER IF NOT EXISTS knowledge_fts_ai AFTER INSERT ON knowledge BEGIN
650
+ INSERT INTO knowledge_fts(rowid, content, domain, key) VALUES (new.id, new.content, new.domain, new.key);
651
+ END
652
+ `);
653
+ db.exec(`
654
+ CREATE TRIGGER IF NOT EXISTS knowledge_fts_au AFTER UPDATE ON knowledge BEGIN
655
+ INSERT INTO knowledge_fts(knowledge_fts, rowid, content, domain, key) VALUES('delete', old.id, old.content, old.domain, old.key);
656
+ INSERT INTO knowledge_fts(rowid, content, domain, key) VALUES (new.id, new.content, new.domain, new.key);
657
+ END
658
+ `);
659
+ db.exec(`
660
+ CREATE TRIGGER IF NOT EXISTS knowledge_fts_ad AFTER DELETE ON knowledge BEGIN
661
+ INSERT INTO knowledge_fts(knowledge_fts, rowid, content, domain, key) VALUES('delete', old.id, old.content, old.domain, old.key);
662
+ END
663
+ `);
664
+ }
665
+ // ── V9: Add session_key to doc_chunks ───────────────────────
666
+ // Enables ephemeral session-scoped doc chunks for subagent context inheritance.
667
+ // Chunks stored with a session_key are transient — clearSessionChunks() removes them.
668
+ function applyV9DocChunkSessionKey(db) {
669
+ const cols = db.prepare('PRAGMA table_info(doc_chunks)').all()
670
+ .map(r => r.name);
671
+ if (!cols.includes('session_key')) {
672
+ db.exec('ALTER TABLE doc_chunks ADD COLUMN session_key TEXT');
673
+ }
674
+ db.exec('CREATE INDEX IF NOT EXISTS idx_doc_chunks_session ON doc_chunks(session_key) WHERE session_key IS NOT NULL');
675
+ }
676
+ // ── V8: Add source_message_id to episodes ───────────────────
677
+ function applyV8EpisodeSourceMessageId(db) {
678
+ // ALTER TABLE ADD COLUMN is safe — existing rows get NULL for new column
679
+ const cols = db.prepare('PRAGMA table_info(episodes)').all()
680
+ .map(r => r.name);
681
+ if (!cols.includes('source_message_id')) {
682
+ db.exec('ALTER TABLE episodes ADD COLUMN source_message_id INTEGER');
683
+ }
684
+ db.exec('CREATE INDEX IF NOT EXISTS idx_episodes_source_msg ON episodes(agent_id, source_message_id)');
685
+ }
686
+ // ── V12: FOS / MOD tables + builtin seed data ──────────────
687
+ function applyV12FosMod(db) {
688
+ // fleet_output_standard: fleet-wide output formatting standards
689
+ db.exec(`
690
+ CREATE TABLE IF NOT EXISTS fleet_output_standard (
691
+ id TEXT PRIMARY KEY,
692
+ name TEXT NOT NULL,
693
+ directives TEXT NOT NULL,
694
+ task_variants TEXT DEFAULT '{}',
695
+ token_budget INTEGER DEFAULT 250,
696
+ active INTEGER DEFAULT 0,
697
+ source TEXT DEFAULT 'builtin',
698
+ version INTEGER DEFAULT 1,
699
+ last_validated_at TEXT,
700
+ created_at TEXT NOT NULL,
701
+ updated_at TEXT NOT NULL
702
+ )
703
+ `);
704
+ // model_output_directives: per-model corrections and calibration
705
+ db.exec(`
706
+ CREATE TABLE IF NOT EXISTS model_output_directives (
707
+ id TEXT PRIMARY KEY,
708
+ match_pattern TEXT NOT NULL,
709
+ priority INTEGER DEFAULT 0,
710
+ corrections TEXT NOT NULL,
711
+ calibration TEXT NOT NULL,
712
+ task_overrides TEXT DEFAULT '{}',
713
+ token_budget INTEGER DEFAULT 150,
714
+ version INTEGER DEFAULT 1,
715
+ source TEXT DEFAULT 'builtin',
716
+ enabled INTEGER DEFAULT 1,
717
+ last_validated_at TEXT,
718
+ created_at TEXT NOT NULL,
719
+ updated_at TEXT NOT NULL
720
+ )
721
+ `);
722
+ // output_metrics: per-request telemetry for drift analytics
723
+ db.exec(`
724
+ CREATE TABLE IF NOT EXISTS output_metrics (
725
+ id TEXT PRIMARY KEY,
726
+ timestamp TEXT NOT NULL,
727
+ agent_id TEXT NOT NULL,
728
+ session_key TEXT NOT NULL,
729
+ model_id TEXT NOT NULL,
730
+ provider TEXT NOT NULL,
731
+ fos_version INTEGER,
732
+ mod_version INTEGER,
733
+ mod_id TEXT,
734
+ task_type TEXT,
735
+ output_tokens INTEGER NOT NULL,
736
+ input_tokens INTEGER,
737
+ cache_read_tokens INTEGER,
738
+ corrections_fired TEXT DEFAULT '[]',
739
+ latency_ms INTEGER,
740
+ created_at TEXT NOT NULL
741
+ )
742
+ `);
743
+ db.exec('CREATE INDEX IF NOT EXISTS idx_output_metrics_model ON output_metrics(model_id, timestamp)');
744
+ db.exec('CREATE INDEX IF NOT EXISTS idx_output_metrics_agent ON output_metrics(agent_id, timestamp)');
745
+ // ── Seed builtin FOS profile ──
746
+ const now = nowIso();
747
+ const fosDirectives = JSON.stringify({
748
+ structural: [
749
+ 'Lead with the answer. Conclusion first, reasoning after.',
750
+ 'Headers earn their place. Under 200 words: no headers.',
751
+ 'Lists cap at 7 items. Technical enumerations exempt.',
752
+ 'One metaphor lands. Two is the limit.',
753
+ ],
754
+ anti_patterns: [
755
+ 'No sycophantic openings: Great question, Certainly, Absolutely, Of course',
756
+ 'No em dashes',
757
+ 'No preamble restating the question',
758
+ 'No: Let me know if you need anything else',
759
+ 'No AI vocabulary: delve, tapestry, pivotal, fostering, garner, underscore, vibrant, leverage, noteworthy, realm',
760
+ 'No unverifiable references — don\'t cite "you mentioned earlier" or "as discussed" without a direct quote',
761
+ 'No claiming actions completed without tool results to back them up',
762
+ 'No attributing statements to people without quoting the actual message',
763
+ ],
764
+ density_targets: {
765
+ simple: '1-3 sentences',
766
+ analysis: '200-500 words',
767
+ code: 'code first, explain only non-obvious parts',
768
+ },
769
+ voice: [
770
+ 'Every sentence states a fact, makes a decision, or advances an argument',
771
+ 'Numbers over adjectives',
772
+ 'Vary sentence length deliberately',
773
+ 'Match confidence to evidence: facts zero hedges, inference one hedge max',
774
+ ],
775
+ });
776
+ const fosVariants = JSON.stringify({
777
+ 'council-deliberation': {
778
+ density_target: '400-800 words. Depth over brevity.',
779
+ structure: 'Headers required. Position statement, risk assessment, confidence, action.',
780
+ },
781
+ 'code-generation': {
782
+ density_target: 'Minimize prose. Code is the deliverable.',
783
+ list_cap: 'DISABLED',
784
+ },
785
+ 'quick-answer': {
786
+ density_target: '1-3 sentences.',
787
+ structure: 'No headers. No lists unless the answer is genuinely a list.',
788
+ },
789
+ });
790
+ const existingFos = db.prepare("SELECT id FROM fleet_output_standard WHERE id = 'psiclawops-default'").get();
791
+ if (!existingFos) {
792
+ db.prepare(`
793
+ INSERT INTO fleet_output_standard (id, name, directives, task_variants, token_budget, active, source, version, created_at, updated_at)
794
+ VALUES ('psiclawops-default', 'PsiClawOps Default', ?, ?, 250, 1, 'builtin', 1, ?, ?)
795
+ `).run(fosDirectives, fosVariants, now, now);
796
+ }
797
+ // ── Seed builtin MOD profiles ──
798
+ const mods = [
799
+ {
800
+ id: 'gpt-5.4',
801
+ match_pattern: 'gpt-5.4*',
802
+ priority: 10,
803
+ corrections: JSON.stringify([
804
+ { id: 'plan-loop', rule: 'If 2+ responses without concrete output, execute immediately. Ship partial.', severity: 'hard' },
805
+ { id: 'first-person-opening', rule: 'Do not open with I.', severity: 'medium' },
806
+ { id: 'throat-clearing', rule: 'No preamble before the answer.', severity: 'medium' },
807
+ { id: 'conditional-hedging', rule: 'Decision questions: answer + 1-2 reasons. No if-X-then-Y branching.', severity: 'medium' },
808
+ ]),
809
+ calibration: JSON.stringify([
810
+ { id: 'verbosity-offset', fos_target: 'analysis: 200-500 words', model_tendency: '~600 words vs Opus baseline', adjustment: 'Actively compress. Your natural output is ~2x the target. Cut first drafts in half.' },
811
+ { id: 'list-length-offset', fos_target: '7 items max', model_tendency: 'defaults to 12-15 items', adjustment: 'After drafting a list, cut the bottom half.' },
812
+ ]),
813
+ },
814
+ {
815
+ id: 'claude-opus-4.6',
816
+ match_pattern: 'claude-opus-4*',
817
+ priority: 10,
818
+ corrections: JSON.stringify([
819
+ { id: 'over-structuring', rule: 'Resist adding headers and sections to short answers.', severity: 'medium' },
820
+ { id: 'premature-enumeration', rule: "Don't list when prose works. Lists require 3+ genuinely distinct items.", severity: 'medium' },
821
+ ]),
822
+ calibration: JSON.stringify([
823
+ { id: 'verbosity-offset', fos_target: 'analysis: 200-500 words', model_tendency: '1.1x target', adjustment: 'Near target. Minor compression on detailed analysis.' },
824
+ ]),
825
+ },
826
+ {
827
+ id: 'claude-sonnet-4.6',
828
+ match_pattern: 'claude-sonnet-4*',
829
+ priority: 10,
830
+ corrections: JSON.stringify([
831
+ { id: 'caveat-frontloading', rule: "Don't open with caveats. State the answer, then caveats if needed.", severity: 'medium' },
832
+ { id: 'safety-hedging', rule: 'Minimize safety qualifiers on unambiguous requests.', severity: 'medium' },
833
+ ]),
834
+ calibration: JSON.stringify([
835
+ { id: 'verbosity-offset', fos_target: 'analysis: 200-500 words', model_tendency: '1.3x target', adjustment: 'Compress by ~25%. Cut qualifications and restatements.' },
836
+ ]),
837
+ },
838
+ {
839
+ id: 'gemini-3.1',
840
+ match_pattern: 'gemini-3.1*',
841
+ priority: 10,
842
+ corrections: JSON.stringify([
843
+ { id: 'numbered-list-default', rule: "Don't default to numbered lists. Use prose unless order matters.", severity: 'hard' },
844
+ { id: 'source-attribution-noise', rule: 'Skip attribution boilerplate unless sourcing is specifically requested.', severity: 'medium' },
845
+ ]),
846
+ calibration: JSON.stringify([
847
+ { id: 'list-length-offset', fos_target: '7 items max', model_tendency: '1.5x target', adjustment: 'Cut lists to 7 items. Merge or drop the rest.' },
848
+ ]),
849
+ },
850
+ {
851
+ id: 'default',
852
+ match_pattern: '*',
853
+ priority: 0,
854
+ corrections: JSON.stringify([]),
855
+ calibration: JSON.stringify([]),
856
+ },
857
+ ];
858
+ for (const mod of mods) {
859
+ const existing = db.prepare('SELECT id FROM model_output_directives WHERE id = ?').get(mod.id);
860
+ if (!existing) {
861
+ db.prepare(`
862
+ INSERT INTO model_output_directives (id, match_pattern, priority, corrections, calibration, task_overrides, token_budget, version, source, enabled, created_at, updated_at)
863
+ VALUES (?, ?, ?, ?, ?, '{}', 150, 1, 'builtin', 1, ?, ?)
864
+ `).run(mod.id, mod.match_pattern, mod.priority, mod.corrections, mod.calibration, now, now);
865
+ }
866
+ }
867
+ }
868
+ // ── Migration runner ──────────────────────────────────────────
869
+ export function migrateLibrary(db, engineVersion) {
870
+ db.exec(`
871
+ CREATE TABLE IF NOT EXISTS schema_version (
872
+ version INTEGER PRIMARY KEY,
873
+ applied_at TEXT NOT NULL
874
+ )
875
+ `);
876
+ const row = db
877
+ .prepare('SELECT MAX(version) AS version FROM schema_version')
878
+ .get();
879
+ const currentVersion = typeof row?.version === 'number' ? row.version : 0;
880
+ if (currentVersion > LIBRARY_SCHEMA_VERSION) {
881
+ console.warn(`[hypermem-library] Database schema version (${currentVersion}) is newer than this engine (${LIBRARY_SCHEMA_VERSION}).`);
882
+ return;
883
+ }
884
+ if (currentVersion < 1) {
885
+ applyV1Schema(db);
886
+ db.prepare('INSERT INTO schema_version (version, applied_at) VALUES (?, ?)')
887
+ .run(1, nowIso());
888
+ }
889
+ if (currentVersion < 2) {
890
+ applyV2SessionRegistry(db);
891
+ db.prepare('INSERT INTO schema_version (version, applied_at) VALUES (?, ?)')
892
+ .run(2, nowIso());
893
+ }
894
+ if (currentVersion < 3) {
895
+ applyV3Collections(db);
896
+ db.prepare('INSERT INTO schema_version (version, applied_at) VALUES (?, ?)')
897
+ .run(3, nowIso());
898
+ }
899
+ if (currentVersion < 4) {
900
+ applyV4Capabilities(db);
901
+ db.prepare('INSERT INTO schema_version (version, applied_at) VALUES (?, ?)')
902
+ .run(4, nowIso());
903
+ }
904
+ if (currentVersion < 5) {
905
+ applyV5DesiredState(db);
906
+ db.prepare('INSERT INTO schema_version (version, applied_at) VALUES (?, ?)')
907
+ .run(5, nowIso());
908
+ }
909
+ if (currentVersion < 6) {
910
+ applyV6DocChunks(db);
911
+ db.prepare('INSERT INTO schema_version (version, applied_at) VALUES (?, ?)')
912
+ .run(6, nowIso());
913
+ }
914
+ if (currentVersion < 7) {
915
+ applyV7KnowledgeVersioning(db);
916
+ db.prepare('INSERT INTO schema_version (version, applied_at) VALUES (?, ?)')
917
+ .run(7, nowIso());
918
+ }
919
+ if (currentVersion < 8) {
920
+ applyV8EpisodeSourceMessageId(db);
921
+ db.prepare('INSERT INTO schema_version (version, applied_at) VALUES (?, ?)')
922
+ .run(8, nowIso());
923
+ }
924
+ if (currentVersion < 9) {
925
+ applyV9DocChunkSessionKey(db);
926
+ db.prepare('INSERT INTO schema_version (version, applied_at) VALUES (?, ?)')
927
+ .run(9, nowIso());
928
+ }
929
+ if (currentVersion < 10) {
930
+ db.exec(`
931
+ CREATE TABLE IF NOT EXISTS meta (
932
+ key TEXT PRIMARY KEY,
933
+ value TEXT NOT NULL,
934
+ updated_at TEXT NOT NULL
935
+ )
936
+ `);
937
+ db.prepare('INSERT INTO schema_version (version, applied_at) VALUES (?, ?)')
938
+ .run(10, nowIso());
939
+ }
940
+ // ── V11: Topics FTS + indexer watermarks ──────────────────
941
+ // topics_fts was missing from V3 (topics table was created without FTS).
942
+ // indexer_watermarks tracks per-agent indexer progress for resumable indexing.
943
+ if (currentVersion < 11) {
944
+ db.exec(`
945
+ CREATE VIRTUAL TABLE IF NOT EXISTS topics_fts USING fts5(
946
+ name,
947
+ description,
948
+ content='topics',
949
+ content_rowid='id'
950
+ )
951
+ `);
952
+ db.exec(`
953
+ CREATE TRIGGER IF NOT EXISTS topics_fts_ai AFTER INSERT ON topics BEGIN
954
+ INSERT INTO topics_fts(rowid, name, description) VALUES (new.id, new.name, new.description);
955
+ END
956
+ `);
957
+ db.exec(`
958
+ CREATE TRIGGER IF NOT EXISTS topics_fts_ad AFTER DELETE ON topics BEGIN
959
+ INSERT INTO topics_fts(topics_fts, rowid, name, description) VALUES('delete', old.id, old.name, old.description);
960
+ END
961
+ `);
962
+ db.exec(`
963
+ CREATE TRIGGER IF NOT EXISTS topics_fts_au AFTER UPDATE ON topics BEGIN
964
+ INSERT INTO topics_fts(topics_fts, rowid, name, description) VALUES('delete', old.id, old.name, old.description);
965
+ INSERT INTO topics_fts(rowid, name, description) VALUES (new.id, new.name, new.description);
966
+ END
967
+ `);
968
+ db.exec(`
969
+ CREATE TABLE IF NOT EXISTS indexer_watermarks (
970
+ agent_id TEXT PRIMARY KEY,
971
+ last_message_id INTEGER NOT NULL DEFAULT 0,
972
+ last_run_at TEXT NOT NULL
973
+ )
974
+ `);
975
+ db.prepare('INSERT INTO schema_version (version, applied_at) VALUES (?, ?)')
976
+ .run(11, nowIso());
977
+ }
978
+ // ── V12: FOS/MOD tables + builtin seed data ──────────────
979
+ // fleet_output_standard: fleet-wide output standards
980
+ // model_output_directives: per-model correction & calibration profiles
981
+ // output_metrics: per-request telemetry for drift analytics
982
+ if (currentVersion < 12) {
983
+ applyV12FosMod(db);
984
+ db.prepare('INSERT INTO schema_version (version, applied_at) VALUES (?, ?)')
985
+ .run(12, nowIso());
986
+ }
987
+ // ── V13: Temporal index ──────────────────────────────────────────────────
988
+ // Maps fact_id → occurred_at (unix ms). Initially backfilled from created_at
989
+ // (ingest time as proxy). Enables time-range retrieval for LoCoMo temporal
990
+ // questions without vector similarity.
991
+ if (currentVersion < 13) {
992
+ db.exec(`
993
+ CREATE TABLE IF NOT EXISTS temporal_index (
994
+ fact_id INTEGER PRIMARY KEY REFERENCES facts(id) ON DELETE CASCADE,
995
+ agent_id TEXT NOT NULL,
996
+ occurred_at INTEGER NOT NULL,
997
+ ingest_at INTEGER NOT NULL,
998
+ time_ref TEXT,
999
+ confidence REAL NOT NULL DEFAULT 0.5
1000
+ )
1001
+ `);
1002
+ db.exec('CREATE INDEX IF NOT EXISTS idx_temporal_agent_time ON temporal_index(agent_id, occurred_at DESC)');
1003
+ db.exec('CREATE INDEX IF NOT EXISTS idx_temporal_occurred ON temporal_index(occurred_at DESC)');
1004
+ // Backfill existing facts using created_at as occurred_at proxy
1005
+ db.exec(`
1006
+ INSERT OR IGNORE INTO temporal_index (fact_id, agent_id, occurred_at, ingest_at, confidence)
1007
+ SELECT
1008
+ id,
1009
+ agent_id,
1010
+ CAST((julianday(created_at) - 2440587.5) * 86400000 AS INTEGER),
1011
+ CAST((julianday(created_at) - 2440587.5) * 86400000 AS INTEGER),
1012
+ 0.5
1013
+ FROM facts
1014
+ WHERE superseded_by IS NULL
1015
+ `);
1016
+ db.prepare('INSERT INTO schema_version (version, applied_at) VALUES (?, ?)')
1017
+ .run(13, nowIso());
1018
+ }
1019
+ // Always ensure meta exists before stamping the running engine version.
1020
+ // Some legacy/stale DBs reached schema >=10 without the V10 migration having
1021
+ // actually created the table, which would make startup fail with
1022
+ // "no such table: meta" during an otherwise unrelated init path.
1023
+ db.exec(`
1024
+ CREATE TABLE IF NOT EXISTS meta (
1025
+ key TEXT PRIMARY KEY,
1026
+ value TEXT NOT NULL,
1027
+ updated_at TEXT NOT NULL
1028
+ )
1029
+ `);
1030
+ // Always stamp the running engine version so any query can surface it.
1031
+ if (engineVersion) {
1032
+ db.prepare(`
1033
+ INSERT INTO meta (key, value, updated_at) VALUES ('engine_version', ?, ?)
1034
+ ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at
1035
+ `).run(engineVersion, nowIso());
1036
+ }
1037
+ }
1038
+ //# sourceMappingURL=library-schema.js.map