@psiclawops/hypermem 0.1.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 (94) hide show
  1. package/ARCHITECTURE.md +296 -0
  2. package/LICENSE +190 -0
  3. package/README.md +243 -0
  4. package/dist/background-indexer.d.ts +117 -0
  5. package/dist/background-indexer.d.ts.map +1 -0
  6. package/dist/background-indexer.js +732 -0
  7. package/dist/compaction-fence.d.ts +89 -0
  8. package/dist/compaction-fence.d.ts.map +1 -0
  9. package/dist/compaction-fence.js +153 -0
  10. package/dist/compositor.d.ts +139 -0
  11. package/dist/compositor.d.ts.map +1 -0
  12. package/dist/compositor.js +1109 -0
  13. package/dist/cross-agent.d.ts +57 -0
  14. package/dist/cross-agent.d.ts.map +1 -0
  15. package/dist/cross-agent.js +254 -0
  16. package/dist/db.d.ts +131 -0
  17. package/dist/db.d.ts.map +1 -0
  18. package/dist/db.js +398 -0
  19. package/dist/desired-state-store.d.ts +100 -0
  20. package/dist/desired-state-store.d.ts.map +1 -0
  21. package/dist/desired-state-store.js +212 -0
  22. package/dist/doc-chunk-store.d.ts +115 -0
  23. package/dist/doc-chunk-store.d.ts.map +1 -0
  24. package/dist/doc-chunk-store.js +278 -0
  25. package/dist/doc-chunker.d.ts +99 -0
  26. package/dist/doc-chunker.d.ts.map +1 -0
  27. package/dist/doc-chunker.js +324 -0
  28. package/dist/episode-store.d.ts +48 -0
  29. package/dist/episode-store.d.ts.map +1 -0
  30. package/dist/episode-store.js +135 -0
  31. package/dist/fact-store.d.ts +57 -0
  32. package/dist/fact-store.d.ts.map +1 -0
  33. package/dist/fact-store.js +175 -0
  34. package/dist/fleet-store.d.ts +144 -0
  35. package/dist/fleet-store.d.ts.map +1 -0
  36. package/dist/fleet-store.js +276 -0
  37. package/dist/hybrid-retrieval.d.ts +60 -0
  38. package/dist/hybrid-retrieval.d.ts.map +1 -0
  39. package/dist/hybrid-retrieval.js +340 -0
  40. package/dist/index.d.ts +611 -0
  41. package/dist/index.d.ts.map +1 -0
  42. package/dist/index.js +1042 -0
  43. package/dist/knowledge-graph.d.ts +110 -0
  44. package/dist/knowledge-graph.d.ts.map +1 -0
  45. package/dist/knowledge-graph.js +305 -0
  46. package/dist/knowledge-store.d.ts +72 -0
  47. package/dist/knowledge-store.d.ts.map +1 -0
  48. package/dist/knowledge-store.js +241 -0
  49. package/dist/library-schema.d.ts +22 -0
  50. package/dist/library-schema.d.ts.map +1 -0
  51. package/dist/library-schema.js +717 -0
  52. package/dist/message-store.d.ts +76 -0
  53. package/dist/message-store.d.ts.map +1 -0
  54. package/dist/message-store.js +273 -0
  55. package/dist/preference-store.d.ts +54 -0
  56. package/dist/preference-store.d.ts.map +1 -0
  57. package/dist/preference-store.js +109 -0
  58. package/dist/preservation-gate.d.ts +82 -0
  59. package/dist/preservation-gate.d.ts.map +1 -0
  60. package/dist/preservation-gate.js +150 -0
  61. package/dist/provider-translator.d.ts +40 -0
  62. package/dist/provider-translator.d.ts.map +1 -0
  63. package/dist/provider-translator.js +349 -0
  64. package/dist/rate-limiter.d.ts +76 -0
  65. package/dist/rate-limiter.d.ts.map +1 -0
  66. package/dist/rate-limiter.js +179 -0
  67. package/dist/redis.d.ts +188 -0
  68. package/dist/redis.d.ts.map +1 -0
  69. package/dist/redis.js +534 -0
  70. package/dist/schema.d.ts +15 -0
  71. package/dist/schema.d.ts.map +1 -0
  72. package/dist/schema.js +203 -0
  73. package/dist/secret-scanner.d.ts +51 -0
  74. package/dist/secret-scanner.d.ts.map +1 -0
  75. package/dist/secret-scanner.js +248 -0
  76. package/dist/seed.d.ts +108 -0
  77. package/dist/seed.d.ts.map +1 -0
  78. package/dist/seed.js +177 -0
  79. package/dist/system-store.d.ts +73 -0
  80. package/dist/system-store.d.ts.map +1 -0
  81. package/dist/system-store.js +182 -0
  82. package/dist/topic-store.d.ts +45 -0
  83. package/dist/topic-store.d.ts.map +1 -0
  84. package/dist/topic-store.js +136 -0
  85. package/dist/types.d.ts +329 -0
  86. package/dist/types.d.ts.map +1 -0
  87. package/dist/types.js +9 -0
  88. package/dist/vector-store.d.ts +132 -0
  89. package/dist/vector-store.d.ts.map +1 -0
  90. package/dist/vector-store.js +498 -0
  91. package/dist/work-store.d.ts +112 -0
  92. package/dist/work-store.d.ts.map +1 -0
  93. package/dist/work-store.js +273 -0
  94. package/package.json +57 -0
@@ -0,0 +1,717 @@
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 = 7;
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
+ // ── Migration runner ──────────────────────────────────────────
666
+ export function migrateLibrary(db) {
667
+ db.exec(`
668
+ CREATE TABLE IF NOT EXISTS schema_version (
669
+ version INTEGER PRIMARY KEY,
670
+ applied_at TEXT NOT NULL
671
+ )
672
+ `);
673
+ const row = db
674
+ .prepare('SELECT MAX(version) AS version FROM schema_version')
675
+ .get();
676
+ const currentVersion = typeof row?.version === 'number' ? row.version : 0;
677
+ if (currentVersion > LIBRARY_SCHEMA_VERSION) {
678
+ console.warn(`[hypermem-library] Database schema version (${currentVersion}) is newer than this engine (${LIBRARY_SCHEMA_VERSION}).`);
679
+ return;
680
+ }
681
+ if (currentVersion < 1) {
682
+ applyV1Schema(db);
683
+ db.prepare('INSERT INTO schema_version (version, applied_at) VALUES (?, ?)')
684
+ .run(1, nowIso());
685
+ }
686
+ if (currentVersion < 2) {
687
+ applyV2SessionRegistry(db);
688
+ db.prepare('INSERT INTO schema_version (version, applied_at) VALUES (?, ?)')
689
+ .run(2, nowIso());
690
+ }
691
+ if (currentVersion < 3) {
692
+ applyV3Collections(db);
693
+ db.prepare('INSERT INTO schema_version (version, applied_at) VALUES (?, ?)')
694
+ .run(3, nowIso());
695
+ }
696
+ if (currentVersion < 4) {
697
+ applyV4Capabilities(db);
698
+ db.prepare('INSERT INTO schema_version (version, applied_at) VALUES (?, ?)')
699
+ .run(4, nowIso());
700
+ }
701
+ if (currentVersion < 5) {
702
+ applyV5DesiredState(db);
703
+ db.prepare('INSERT INTO schema_version (version, applied_at) VALUES (?, ?)')
704
+ .run(5, nowIso());
705
+ }
706
+ if (currentVersion < 6) {
707
+ applyV6DocChunks(db);
708
+ db.prepare('INSERT INTO schema_version (version, applied_at) VALUES (?, ?)')
709
+ .run(6, nowIso());
710
+ }
711
+ if (currentVersion < 7) {
712
+ applyV7KnowledgeVersioning(db);
713
+ db.prepare('INSERT INTO schema_version (version, applied_at) VALUES (?, ?)')
714
+ .run(7, nowIso());
715
+ }
716
+ }
717
+ //# sourceMappingURL=library-schema.js.map