@psiclawops/hypermem 0.1.0 → 0.5.1

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 (153) hide show
  1. package/ARCHITECTURE.md +4 -3
  2. package/README.md +457 -174
  3. package/dist/background-indexer.d.ts +19 -4
  4. package/dist/background-indexer.d.ts.map +1 -1
  5. package/dist/background-indexer.js +329 -17
  6. package/dist/cache.d.ts +110 -0
  7. package/dist/cache.d.ts.map +1 -0
  8. package/dist/cache.js +495 -0
  9. package/dist/compaction-fence.d.ts +1 -1
  10. package/dist/compaction-fence.js +1 -1
  11. package/dist/compositor.d.ts +114 -27
  12. package/dist/compositor.d.ts.map +1 -1
  13. package/dist/compositor.js +1678 -229
  14. package/dist/content-type-classifier.d.ts +41 -0
  15. package/dist/content-type-classifier.d.ts.map +1 -0
  16. package/dist/content-type-classifier.js +181 -0
  17. package/dist/cross-agent.d.ts +5 -0
  18. package/dist/cross-agent.d.ts.map +1 -1
  19. package/dist/cross-agent.js +5 -0
  20. package/dist/db.d.ts +1 -1
  21. package/dist/db.d.ts.map +1 -1
  22. package/dist/db.js +6 -2
  23. package/dist/desired-state-store.d.ts +1 -1
  24. package/dist/desired-state-store.d.ts.map +1 -1
  25. package/dist/desired-state-store.js +15 -5
  26. package/dist/doc-chunk-store.d.ts +26 -1
  27. package/dist/doc-chunk-store.d.ts.map +1 -1
  28. package/dist/doc-chunk-store.js +114 -1
  29. package/dist/doc-chunker.d.ts +1 -1
  30. package/dist/doc-chunker.js +1 -1
  31. package/dist/dreaming-promoter.d.ts +86 -0
  32. package/dist/dreaming-promoter.d.ts.map +1 -0
  33. package/dist/dreaming-promoter.js +381 -0
  34. package/dist/episode-store.d.ts +2 -1
  35. package/dist/episode-store.d.ts.map +1 -1
  36. package/dist/episode-store.js +4 -4
  37. package/dist/fact-store.d.ts +19 -1
  38. package/dist/fact-store.d.ts.map +1 -1
  39. package/dist/fact-store.js +64 -3
  40. package/dist/fleet-store.d.ts +1 -1
  41. package/dist/fleet-store.js +1 -1
  42. package/dist/fos-mod.d.ts +178 -0
  43. package/dist/fos-mod.d.ts.map +1 -0
  44. package/dist/fos-mod.js +416 -0
  45. package/dist/hybrid-retrieval.d.ts +5 -1
  46. package/dist/hybrid-retrieval.d.ts.map +1 -1
  47. package/dist/hybrid-retrieval.js +7 -3
  48. package/dist/image-eviction.d.ts +49 -0
  49. package/dist/image-eviction.d.ts.map +1 -0
  50. package/dist/image-eviction.js +251 -0
  51. package/dist/index.d.ts +50 -11
  52. package/dist/index.d.ts.map +1 -1
  53. package/dist/index.js +73 -43
  54. package/dist/keystone-scorer.d.ts +51 -0
  55. package/dist/keystone-scorer.d.ts.map +1 -0
  56. package/dist/keystone-scorer.js +52 -0
  57. package/dist/knowledge-graph.d.ts +1 -1
  58. package/dist/knowledge-graph.js +1 -1
  59. package/dist/knowledge-lint.d.ts +29 -0
  60. package/dist/knowledge-lint.d.ts.map +1 -0
  61. package/dist/knowledge-lint.js +116 -0
  62. package/dist/knowledge-store.d.ts +1 -1
  63. package/dist/knowledge-store.d.ts.map +1 -1
  64. package/dist/knowledge-store.js +8 -2
  65. package/dist/library-schema.d.ts +3 -3
  66. package/dist/library-schema.d.ts.map +1 -1
  67. package/dist/library-schema.js +324 -3
  68. package/dist/message-store.d.ts +15 -2
  69. package/dist/message-store.d.ts.map +1 -1
  70. package/dist/message-store.js +51 -1
  71. package/dist/metrics-dashboard.d.ts +114 -0
  72. package/dist/metrics-dashboard.d.ts.map +1 -0
  73. package/dist/metrics-dashboard.js +260 -0
  74. package/dist/obsidian-exporter.d.ts +57 -0
  75. package/dist/obsidian-exporter.d.ts.map +1 -0
  76. package/dist/obsidian-exporter.js +274 -0
  77. package/dist/obsidian-watcher.d.ts +147 -0
  78. package/dist/obsidian-watcher.d.ts.map +1 -0
  79. package/dist/obsidian-watcher.js +403 -0
  80. package/dist/open-domain.d.ts +46 -0
  81. package/dist/open-domain.d.ts.map +1 -0
  82. package/dist/open-domain.js +125 -0
  83. package/dist/preference-store.d.ts +1 -1
  84. package/dist/preference-store.js +1 -1
  85. package/dist/preservation-gate.d.ts +1 -1
  86. package/dist/preservation-gate.js +1 -1
  87. package/dist/proactive-pass.d.ts +63 -0
  88. package/dist/proactive-pass.d.ts.map +1 -0
  89. package/dist/proactive-pass.js +239 -0
  90. package/dist/profiles.d.ts +44 -0
  91. package/dist/profiles.d.ts.map +1 -0
  92. package/dist/profiles.js +227 -0
  93. package/dist/provider-translator.d.ts +13 -3
  94. package/dist/provider-translator.d.ts.map +1 -1
  95. package/dist/provider-translator.js +63 -9
  96. package/dist/rate-limiter.d.ts +1 -1
  97. package/dist/rate-limiter.js +1 -1
  98. package/dist/repair-tool-pairs.d.ts +38 -0
  99. package/dist/repair-tool-pairs.d.ts.map +1 -0
  100. package/dist/repair-tool-pairs.js +138 -0
  101. package/dist/retrieval-policy.d.ts +51 -0
  102. package/dist/retrieval-policy.d.ts.map +1 -0
  103. package/dist/retrieval-policy.js +77 -0
  104. package/dist/schema.d.ts +2 -2
  105. package/dist/schema.d.ts.map +1 -1
  106. package/dist/schema.js +28 -2
  107. package/dist/secret-scanner.d.ts +1 -1
  108. package/dist/secret-scanner.js +1 -1
  109. package/dist/seed.d.ts +2 -2
  110. package/dist/seed.js +2 -2
  111. package/dist/session-flusher.d.ts +53 -0
  112. package/dist/session-flusher.d.ts.map +1 -0
  113. package/dist/session-flusher.js +69 -0
  114. package/dist/session-topic-map.d.ts +41 -0
  115. package/dist/session-topic-map.d.ts.map +1 -0
  116. package/dist/session-topic-map.js +77 -0
  117. package/dist/spawn-context.d.ts +54 -0
  118. package/dist/spawn-context.d.ts.map +1 -0
  119. package/dist/spawn-context.js +159 -0
  120. package/dist/system-store.d.ts +1 -1
  121. package/dist/system-store.js +1 -1
  122. package/dist/temporal-store.d.ts +80 -0
  123. package/dist/temporal-store.d.ts.map +1 -0
  124. package/dist/temporal-store.js +149 -0
  125. package/dist/topic-detector.d.ts +35 -0
  126. package/dist/topic-detector.d.ts.map +1 -0
  127. package/dist/topic-detector.js +249 -0
  128. package/dist/topic-store.d.ts +1 -1
  129. package/dist/topic-store.js +1 -1
  130. package/dist/topic-synthesizer.d.ts +51 -0
  131. package/dist/topic-synthesizer.d.ts.map +1 -0
  132. package/dist/topic-synthesizer.js +315 -0
  133. package/dist/trigger-registry.d.ts +63 -0
  134. package/dist/trigger-registry.d.ts.map +1 -0
  135. package/dist/trigger-registry.js +163 -0
  136. package/dist/types.d.ts +214 -10
  137. package/dist/types.d.ts.map +1 -1
  138. package/dist/types.js +1 -1
  139. package/dist/vector-store.d.ts +43 -5
  140. package/dist/vector-store.d.ts.map +1 -1
  141. package/dist/vector-store.js +189 -10
  142. package/dist/version.d.ts +34 -0
  143. package/dist/version.d.ts.map +1 -0
  144. package/dist/version.js +34 -0
  145. package/dist/wiki-page-emitter.d.ts +65 -0
  146. package/dist/wiki-page-emitter.d.ts.map +1 -0
  147. package/dist/wiki-page-emitter.js +258 -0
  148. package/dist/work-store.d.ts +1 -1
  149. package/dist/work-store.js +1 -1
  150. package/package.json +15 -5
  151. package/dist/redis.d.ts +0 -188
  152. package/dist/redis.d.ts.map +0 -1
  153. package/dist/redis.js +0 -534
@@ -1,5 +1,5 @@
1
1
  /**
2
- * HyperMem Vector Store — Semantic Search via sqlite-vec
2
+ * hypermem Vector Store — Semantic Search via sqlite-vec
3
3
  *
4
4
  * Provides embedding-backed KNN search over facts, knowledge, episodes,
5
5
  * and session registry entries. Uses Ollama (local) for embeddings,
@@ -9,28 +9,156 @@
9
9
  * - One vec0 virtual table per indexed content type
10
10
  * - Embeddings generated via local Ollama (nomic-embed-text, 768d)
11
11
  * - Vectors stored alongside content in the same agent DB
12
- * - Embedding cache to avoid redundant API calls
12
+ * - LRU embedding cache (module-level, per-process) to avoid redundant Ollama calls
13
+ * - Precomputed embedding passthrough: callers can supply an embedding to skip Ollama
13
14
  * - Batch embedding support for bulk indexing
14
15
  */
15
16
  import { createHash } from 'node:crypto';
16
17
  const DEFAULT_EMBEDDING_CONFIG = {
18
+ provider: 'ollama',
17
19
  ollamaUrl: 'http://localhost:11434',
20
+ openaiBaseUrl: 'https://api.openai.com/v1',
18
21
  model: 'nomic-embed-text',
19
22
  dimensions: 768,
20
23
  timeout: 10000,
21
24
  batchSize: 32,
25
+ cacheSize: 128,
22
26
  };
27
+ /** Provider-specific defaults applied when provider is 'openai' and fields are not set. */
28
+ const OPENAI_DEFAULTS = {
29
+ model: 'text-embedding-3-small',
30
+ dimensions: 1536,
31
+ batchSize: 128,
32
+ };
33
+ const _embeddingCache = new Map();
34
+ /**
35
+ * Insert an entry into the LRU cache, evicting the oldest if over capacity.
36
+ */
37
+ function cachePut(key, embedding, maxSize) {
38
+ if (_embeddingCache.has(key)) {
39
+ // Update existing entry (refresh timestamp)
40
+ _embeddingCache.delete(key);
41
+ }
42
+ else if (_embeddingCache.size >= maxSize) {
43
+ // Evict oldest entry by timestamp
44
+ let oldestKey;
45
+ let oldestTime = Infinity;
46
+ for (const [k, v] of _embeddingCache) {
47
+ if (v.timestamp < oldestTime) {
48
+ oldestTime = v.timestamp;
49
+ oldestKey = k;
50
+ }
51
+ }
52
+ if (oldestKey !== undefined) {
53
+ _embeddingCache.delete(oldestKey);
54
+ }
55
+ }
56
+ _embeddingCache.set(key, { embedding, timestamp: Date.now() });
57
+ }
58
+ /**
59
+ * Clear the embedding cache. Primarily for testing.
60
+ */
61
+ export function clearEmbeddingCache() {
62
+ _embeddingCache.clear();
63
+ }
64
+ /**
65
+ * Generate embeddings via OpenAI Embeddings API.
66
+ * Batches up to batchSize inputs per request.
67
+ */
68
+ async function generateOpenAIEmbeddings(texts, config) {
69
+ // Resolve API key: config > environment
70
+ const apiKey = config.openaiApiKey
71
+ ?? process.env.OPENROUTER_API_KEY
72
+ ?? process.env.OPENAI_API_KEY
73
+ ?? null;
74
+ if (!apiKey) {
75
+ throw new Error('[hypermem] OpenAI embedding provider requires an API key. ' +
76
+ 'Set openaiApiKey in hypermem config, or set OPENROUTER_API_KEY / OPENAI_API_KEY env var.');
77
+ }
78
+ const baseUrl = config.openaiBaseUrl ?? 'https://api.openai.com/v1';
79
+ const model = config.model;
80
+ const results = [];
81
+ for (let i = 0; i < texts.length; i += config.batchSize) {
82
+ const batch = texts.slice(i, i + config.batchSize);
83
+ const controller = new AbortController();
84
+ const timer = setTimeout(() => controller.abort(), config.timeout);
85
+ try {
86
+ const response = await fetch(`${baseUrl}/embeddings`, {
87
+ method: 'POST',
88
+ headers: {
89
+ 'Content-Type': 'application/json',
90
+ 'Authorization': `Bearer ${apiKey}`,
91
+ },
92
+ body: JSON.stringify({ model, input: batch }),
93
+ signal: controller.signal,
94
+ });
95
+ if (!response.ok) {
96
+ const body = await response.text().catch(() => '');
97
+ throw new Error(`OpenAI embedding failed: ${response.status} ${response.statusText} — ${body}`);
98
+ }
99
+ const data = await response.json();
100
+ // OpenAI returns results in order by default but may not guarantee it — sort by index.
101
+ const sorted = data.data.sort((a, b) => a.index - b.index);
102
+ for (const item of sorted) {
103
+ if (item.embedding.length !== config.dimensions) {
104
+ throw new Error(`OpenAI embedding dimension mismatch: expected ${config.dimensions}, got ${item.embedding.length}. ` +
105
+ 'If you changed models, re-index via hypermem reindex.');
106
+ }
107
+ results.push(new Float32Array(item.embedding));
108
+ }
109
+ }
110
+ finally {
111
+ clearTimeout(timer);
112
+ }
113
+ }
114
+ return results;
115
+ }
23
116
  /**
24
117
  * Generate embeddings via Ollama API.
25
118
  * Supports single and batch embedding.
119
+ * Results are cached per text hash — cache hits skip the Ollama call entirely.
26
120
  */
27
121
  export async function generateEmbeddings(texts, config = DEFAULT_EMBEDDING_CONFIG) {
122
+ // Apply provider-specific defaults when provider is 'openai' and fields are at Ollama defaults
123
+ if (config.provider === 'openai') {
124
+ // Merge: OpenAI defaults fill in any unset fields, user-supplied values always win
125
+ config = {
126
+ ...DEFAULT_EMBEDDING_CONFIG,
127
+ ...config,
128
+ model: config.model !== DEFAULT_EMBEDDING_CONFIG.model ? config.model : OPENAI_DEFAULTS.model,
129
+ dimensions: config.dimensions !== DEFAULT_EMBEDDING_CONFIG.dimensions ? config.dimensions : OPENAI_DEFAULTS.dimensions,
130
+ batchSize: config.batchSize !== DEFAULT_EMBEDDING_CONFIG.batchSize ? config.batchSize : OPENAI_DEFAULTS.batchSize,
131
+ };
132
+ // OpenAI path — no LRU cache (responses are billed; caching at this layer
133
+ // adds complexity without proportional benefit given async background use).
134
+ return generateOpenAIEmbeddings(texts, config);
135
+ }
28
136
  if (texts.length === 0)
29
137
  return [];
30
- const results = [];
138
+ const maxSize = Math.min(config.cacheSize ?? DEFAULT_EMBEDDING_CONFIG.cacheSize ?? 128, 10_000 // Hard cap: prevent unbounded memory growth from operator misconfiguration
139
+ );
140
+ const results = new Array(texts.length).fill(null);
141
+ // Check cache first — build list of texts that need Ollama calls
142
+ const uncachedIndices = [];
143
+ for (let i = 0; i < texts.length; i++) {
144
+ const key = simpleHash(texts[i]);
145
+ const cached = _embeddingCache.get(key);
146
+ if (cached) {
147
+ results[i] = cached.embedding;
148
+ }
149
+ else {
150
+ uncachedIndices.push(i);
151
+ }
152
+ }
153
+ if (uncachedIndices.length === 0) {
154
+ return results;
155
+ }
156
+ // Fetch uncached texts from Ollama in batches
157
+ const uncachedTexts = uncachedIndices.map(i => texts[i]);
158
+ const ollamaResults = [];
31
159
  // Ollama /api/embed supports batch via `input` array
32
- for (let i = 0; i < texts.length; i += config.batchSize) {
33
- const batch = texts.slice(i, i + config.batchSize);
160
+ for (let i = 0; i < uncachedTexts.length; i += config.batchSize) {
161
+ const batch = uncachedTexts.slice(i, i + config.batchSize);
34
162
  const controller = new AbortController();
35
163
  const timer = setTimeout(() => controller.abort(), config.timeout);
36
164
  try {
@@ -51,13 +179,20 @@ export async function generateEmbeddings(texts, config = DEFAULT_EMBEDDING_CONFI
51
179
  if (embedding.length !== config.dimensions) {
52
180
  throw new Error(`Embedding dimension mismatch: expected ${config.dimensions}, got ${embedding.length}`);
53
181
  }
54
- results.push(new Float32Array(embedding));
182
+ ollamaResults.push(new Float32Array(embedding));
55
183
  }
56
184
  }
57
185
  finally {
58
186
  clearTimeout(timer);
59
187
  }
60
188
  }
189
+ // Populate cache and fill results array
190
+ for (let j = 0; j < uncachedIndices.length; j++) {
191
+ const origIdx = uncachedIndices[j];
192
+ const embedding = ollamaResults[j];
193
+ results[origIdx] = embedding;
194
+ cachePut(simpleHash(texts[origIdx]), embedding, maxSize);
195
+ }
61
196
  return results;
62
197
  }
63
198
  /**
@@ -233,6 +368,10 @@ export class VectorStore {
233
368
  }
234
369
  /**
235
370
  * Semantic KNN search across one or all vector tables.
371
+ *
372
+ * @param precomputedEmbedding — optional pre-computed embedding for the query.
373
+ * When provided, skips the Ollama call entirely. The precomputed embedding
374
+ * is still inserted into the LRU cache so subsequent identical queries hit.
236
375
  */
237
376
  async search(query, opts) {
238
377
  const limit = opts?.limit || 10;
@@ -241,8 +380,17 @@ export class VectorStore {
241
380
  for (const table of tables) {
242
381
  this.validateSourceTable(table);
243
382
  }
244
- // Generate query embedding
245
- const [queryEmbedding] = await generateEmbeddings([query], this.config);
383
+ // Use precomputed embedding if provided, otherwise call Ollama
384
+ let queryEmbedding;
385
+ if (opts?.precomputedEmbedding) {
386
+ queryEmbedding = opts.precomputedEmbedding;
387
+ // Populate LRU cache so subsequent queries for the same text hit
388
+ const maxSize = this.config.cacheSize ?? 128;
389
+ cachePut(simpleHash(query), queryEmbedding, maxSize);
390
+ }
391
+ else {
392
+ [queryEmbedding] = await generateEmbeddings([query], this.config);
393
+ }
246
394
  const queryBytes = vecToBytes(queryEmbedding);
247
395
  const results = [];
248
396
  for (const table of tables) {
@@ -300,13 +448,13 @@ export class VectorStore {
300
448
  switch (table) {
301
449
  case 'facts': {
302
450
  const row = sourceDb
303
- .prepare('SELECT content, domain, agent_id FROM facts WHERE id = ?')
451
+ .prepare('SELECT content, domain, agent_id FROM facts WHERE id = ? AND superseded_by IS NULL')
304
452
  .get(id);
305
453
  return row ? { content: row.content, domain: row.domain, agentId: row.agent_id } : null;
306
454
  }
307
455
  case 'knowledge': {
308
456
  const row = sourceDb
309
- .prepare('SELECT content, domain, agent_id, key FROM knowledge WHERE id = ?')
457
+ .prepare('SELECT content, domain, agent_id, key FROM knowledge WHERE id = ? AND superseded_by IS NULL')
310
458
  .get(id);
311
459
  return row
312
460
  ? { content: row.content, domain: row.domain, agentId: row.agent_id, metadata: row.key }
@@ -409,6 +557,37 @@ export class VectorStore {
409
557
  }
410
558
  return pruned;
411
559
  }
560
+ /**
561
+ * Remove the vector index entry for a single source item.
562
+ *
563
+ * Deletes both the vec table row and the vec_index_map entry for the given
564
+ * (sourceTable, sourceId) pair. Used by the background indexer for immediate
565
+ * point-in-time removal when a supersedes relationship is detected.
566
+ *
567
+ * @returns true if an entry was found and removed, false if nothing was indexed.
568
+ */
569
+ removeItem(sourceTable, sourceId) {
570
+ this.validateSourceTable(sourceTable);
571
+ const entry = this.db
572
+ .prepare('SELECT id, vec_table FROM vec_index_map WHERE source_table = ? AND source_id = ?')
573
+ .get(sourceTable, sourceId);
574
+ if (!entry)
575
+ return false;
576
+ this.db.prepare(`DELETE FROM ${entry.vec_table} WHERE rowid = CAST(? AS INTEGER)`).run(entry.id);
577
+ this.db.prepare('DELETE FROM vec_index_map WHERE id = ?').run(entry.id);
578
+ return true;
579
+ }
580
+ /**
581
+ * Check whether a source item already has a vector in the index.
582
+ * Used by the episode backfill to skip already-vectorized entries.
583
+ */
584
+ hasItem(sourceTable, sourceId) {
585
+ this.validateSourceTable(sourceTable);
586
+ const row = this.db
587
+ .prepare('SELECT 1 FROM vec_index_map WHERE source_table = ? AND source_id = ? LIMIT 1')
588
+ .get(sourceTable, sourceId);
589
+ return row !== undefined;
590
+ }
412
591
  /**
413
592
  * Tombstone vector entries for superseded facts and knowledge.
414
593
  *
@@ -0,0 +1,34 @@
1
+ /** Release version — matches package.json and is stamped into library.db on every startup. */
2
+ export declare const ENGINE_VERSION = "0.5.0";
3
+ /** Minimum Node.js version required — matches package.json engines field. */
4
+ export declare const MIN_NODE_VERSION = "22.0.0";
5
+ /** @deprecated No longer used — Redis was replaced with SQLite :memory: CacheLayer. */
6
+ export declare const MIN_REDIS_VERSION = "7.0.0";
7
+ /** sqlite-vec version bundled with this release. */
8
+ export declare const SQLITE_VEC_VERSION = "0.1.9";
9
+ /**
10
+ * Main DB (hypermem.db) schema version.
11
+ * Re-exported here for convenience; authoritative value lives in schema.ts.
12
+ */
13
+ export declare const MAIN_SCHEMA_VERSION = 6;
14
+ /**
15
+ * Library DB (library.db) schema version.
16
+ * Re-exported here for convenience; authoritative value lives in library-schema.ts.
17
+ */
18
+ export declare const LIBRARY_SCHEMA_VERSION_EXPORT = 12;
19
+ /**
20
+ * Compatibility version — the single number operators and consumers check.
21
+ * Maps to: main schema v6, library schema v12.
22
+ * Matches ENGINE_VERSION for the 0.5.0 release.
23
+ */
24
+ export declare const HYPERMEM_COMPAT_VERSION = "0.5.0";
25
+ /**
26
+ * Schema compatibility map — machine-readable version requirements.
27
+ * Use this to verify DB schemas match the running engine.
28
+ */
29
+ export declare const SCHEMA_COMPAT: {
30
+ readonly compatVersion: "0.5.0";
31
+ readonly mainSchema: 6;
32
+ readonly librarySchema: 12;
33
+ };
34
+ //# sourceMappingURL=version.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"version.d.ts","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":"AAAA,8FAA8F;AAC9F,eAAO,MAAM,cAAc,UAAU,CAAC;AAEtC,6EAA6E;AAC7E,eAAO,MAAM,gBAAgB,WAAW,CAAC;AAEzC,uFAAuF;AACvF,eAAO,MAAM,iBAAiB,UAAU,CAAC;AAEzC,oDAAoD;AACpD,eAAO,MAAM,kBAAkB,UAAU,CAAC;AAE1C;;;GAGG;AACH,eAAO,MAAM,mBAAmB,IAAI,CAAC;AAErC;;;GAGG;AACH,eAAO,MAAM,6BAA6B,KAAK,CAAC;AAEhD;;;;GAIG;AACH,eAAO,MAAM,uBAAuB,UAAU,CAAC;AAE/C;;;GAGG;AACH,eAAO,MAAM,aAAa;;;;CAIhB,CAAC"}
@@ -0,0 +1,34 @@
1
+ /** Release version — matches package.json and is stamped into library.db on every startup. */
2
+ export const ENGINE_VERSION = '0.5.0';
3
+ /** Minimum Node.js version required — matches package.json engines field. */
4
+ export const MIN_NODE_VERSION = '22.0.0';
5
+ /** @deprecated No longer used — Redis was replaced with SQLite :memory: CacheLayer. */
6
+ export const MIN_REDIS_VERSION = '7.0.0';
7
+ /** sqlite-vec version bundled with this release. */
8
+ export const SQLITE_VEC_VERSION = '0.1.9';
9
+ /**
10
+ * Main DB (hypermem.db) schema version.
11
+ * Re-exported here for convenience; authoritative value lives in schema.ts.
12
+ */
13
+ export const MAIN_SCHEMA_VERSION = 6;
14
+ /**
15
+ * Library DB (library.db) schema version.
16
+ * Re-exported here for convenience; authoritative value lives in library-schema.ts.
17
+ */
18
+ export const LIBRARY_SCHEMA_VERSION_EXPORT = 12;
19
+ /**
20
+ * Compatibility version — the single number operators and consumers check.
21
+ * Maps to: main schema v6, library schema v12.
22
+ * Matches ENGINE_VERSION for the 0.5.0 release.
23
+ */
24
+ export const HYPERMEM_COMPAT_VERSION = '0.5.0';
25
+ /**
26
+ * Schema compatibility map — machine-readable version requirements.
27
+ * Use this to verify DB schemas match the running engine.
28
+ */
29
+ export const SCHEMA_COMPAT = {
30
+ compatVersion: '0.5.0',
31
+ mainSchema: 6,
32
+ librarySchema: 12,
33
+ };
34
+ //# sourceMappingURL=version.js.map
@@ -0,0 +1,65 @@
1
+ /**
2
+ * wiki-page-emitter.ts
3
+ *
4
+ * Query-time API for the hypermem wiki layer.
5
+ * Retrieves synthesized topic pages, resolves cross-links,
6
+ * and triggers on-demand synthesis when pages are stale/missing.
7
+ */
8
+ import type { DatabaseSync } from 'node:sqlite';
9
+ import { type SynthesisConfig } from './topic-synthesizer.js';
10
+ export interface WikiPage {
11
+ topicName: string;
12
+ content: string;
13
+ version: number;
14
+ updatedAt: string;
15
+ crossLinks: WikiLink[];
16
+ }
17
+ export interface WikiLink {
18
+ topicName: string;
19
+ linkType: string;
20
+ direction: 'from' | 'to';
21
+ }
22
+ export interface WikiPageSummary {
23
+ topicName: string;
24
+ updatedAt: string;
25
+ messageCount: number;
26
+ version: number;
27
+ }
28
+ export declare class WikiPageEmitter {
29
+ private readonly libraryDb;
30
+ private readonly getMessageDb;
31
+ private readonly synthConfig?;
32
+ private readonly knowledgeStore;
33
+ private readonly synthesizer;
34
+ private readonly regrowthThreshold;
35
+ constructor(libraryDb: DatabaseSync, getMessageDb: (agentId: string) => DatabaseSync | null, synthConfig?: Partial<SynthesisConfig> | undefined);
36
+ /**
37
+ * Fetch the version number for an active knowledge entry.
38
+ */
39
+ private getVersion;
40
+ /**
41
+ * Get a wiki page for a topic.
42
+ * If no page exists, or page is stale (topic has grown by >= regrowthThreshold
43
+ * since last synthesis), trigger a synthesis pass first.
44
+ * Returns null if topic has no messages or doesn't exist.
45
+ */
46
+ getPage(agentId: string, topicName: string): WikiPage | null;
47
+ /**
48
+ * List all synthesized pages for an agent — the table of contents.
49
+ */
50
+ listPages(agentId: string, opts?: {
51
+ limit?: number;
52
+ domain?: string;
53
+ }): WikiPageSummary[];
54
+ /**
55
+ * Get a page's cross-links from knowledge_links table.
56
+ * Resolves both directions (from and to).
57
+ */
58
+ private resolveLinks;
59
+ /**
60
+ * Force re-synthesis of a specific topic regardless of staleness.
61
+ * Returns the new page or null if topic not found.
62
+ */
63
+ forceSynthesize(agentId: string, topicName: string): WikiPage | null;
64
+ }
65
+ //# sourceMappingURL=wiki-page-emitter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wiki-page-emitter.d.ts","sourceRoot":"","sources":["../src/wiki-page-emitter.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD,OAAO,EAAoB,KAAK,eAAe,EAAgC,MAAM,wBAAwB,CAAC;AAI9G,MAAM,WAAW,QAAQ;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,QAAQ,EAAE,CAAC;CACxB;AAED,MAAM,WAAW,QAAQ;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAED,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC;CACjB;AAeD,qBAAa,eAAe;IAMxB,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,YAAY;IAC7B,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAC;IAP/B,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAiB;IAChD,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAmB;IAC/C,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAS;gBAGxB,SAAS,EAAE,YAAY,EACvB,YAAY,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,YAAY,GAAG,IAAI,EACtD,WAAW,CAAC,EAAE,OAAO,CAAC,eAAe,CAAC,YAAA;IAOzD;;OAEG;IACH,OAAO,CAAC,UAAU;IAclB;;;;;OAKG;IACH,OAAO,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,QAAQ,GAAG,IAAI;IAgE5D;;OAEG;IACH,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,eAAe,EAAE;IAgCzF;;;OAGG;IACH,OAAO,CAAC,YAAY;IAiEpB;;;OAGG;IACH,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,QAAQ,GAAG,IAAI;CAyCrE"}
@@ -0,0 +1,258 @@
1
+ /**
2
+ * wiki-page-emitter.ts
3
+ *
4
+ * Query-time API for the hypermem wiki layer.
5
+ * Retrieves synthesized topic pages, resolves cross-links,
6
+ * and triggers on-demand synthesis when pages are stale/missing.
7
+ */
8
+ import { KnowledgeStore } from './knowledge-store.js';
9
+ import { TopicSynthesizer, SYNTHESIS_REGROWTH_THRESHOLD } from './topic-synthesizer.js';
10
+ // ─── Helpers ────────────────────────────────────────────────────
11
+ /**
12
+ * Parse message_count stored in source_ref ("topic:<id>:mc:<count>").
13
+ */
14
+ function parseStoredMessageCount(sourceRef) {
15
+ if (!sourceRef)
16
+ return 0;
17
+ const match = sourceRef.match(/:mc:(\d+)$/);
18
+ return match ? parseInt(match[1], 10) : 0;
19
+ }
20
+ // ─── WikiPageEmitter ────────────────────────────────────────────
21
+ export class WikiPageEmitter {
22
+ libraryDb;
23
+ getMessageDb;
24
+ synthConfig;
25
+ knowledgeStore;
26
+ synthesizer;
27
+ regrowthThreshold;
28
+ constructor(libraryDb, getMessageDb, synthConfig) {
29
+ this.libraryDb = libraryDb;
30
+ this.getMessageDb = getMessageDb;
31
+ this.synthConfig = synthConfig;
32
+ this.knowledgeStore = new KnowledgeStore(libraryDb);
33
+ this.synthesizer = new TopicSynthesizer(libraryDb, getMessageDb, synthConfig);
34
+ this.regrowthThreshold = synthConfig?.SYNTHESIS_REGROWTH_THRESHOLD ?? SYNTHESIS_REGROWTH_THRESHOLD;
35
+ }
36
+ /**
37
+ * Fetch the version number for an active knowledge entry.
38
+ */
39
+ getVersion(agentId, topicName) {
40
+ try {
41
+ const row = this.libraryDb.prepare(`
42
+ SELECT version FROM knowledge
43
+ WHERE agent_id = ? AND domain = 'topic-synthesis' AND key = ?
44
+ AND superseded_by IS NULL
45
+ LIMIT 1
46
+ `).get(agentId, topicName);
47
+ return row?.version ?? 1;
48
+ }
49
+ catch {
50
+ return 1;
51
+ }
52
+ }
53
+ /**
54
+ * Get a wiki page for a topic.
55
+ * If no page exists, or page is stale (topic has grown by >= regrowthThreshold
56
+ * since last synthesis), trigger a synthesis pass first.
57
+ * Returns null if topic has no messages or doesn't exist.
58
+ */
59
+ getPage(agentId, topicName) {
60
+ // Look up topic to check current message_count and existence
61
+ let topicRow;
62
+ try {
63
+ topicRow = this.libraryDb.prepare(`
64
+ SELECT id, message_count FROM topics
65
+ WHERE agent_id = ? AND name = ?
66
+ LIMIT 1
67
+ `).get(agentId, topicName);
68
+ }
69
+ catch {
70
+ // Topics table may not exist or topic not found
71
+ }
72
+ // Check existing synthesis
73
+ const existing = this.knowledgeStore.get(agentId, 'topic-synthesis', topicName);
74
+ if (existing) {
75
+ // Check staleness if we have topic data
76
+ if (topicRow) {
77
+ const storedMc = parseStoredMessageCount(existing.sourceRef);
78
+ const growth = topicRow.message_count - storedMc;
79
+ if (growth >= this.regrowthThreshold) {
80
+ // Stale — re-synthesize by running a targeted tick
81
+ // TopicSynthesizer.tick() picks up topics that have grown enough
82
+ this.synthesizer.tick(agentId);
83
+ }
84
+ }
85
+ }
86
+ else {
87
+ // No page at all — trigger synthesis regardless of staleness threshold
88
+ if (!topicRow)
89
+ return null;
90
+ const messageDb = this.getMessageDb(agentId);
91
+ if (!messageDb)
92
+ return null;
93
+ // Check if there are actually messages for this topic before attempting synthesis
94
+ let msgCount = 0;
95
+ try {
96
+ const row = messageDb.prepare('SELECT COUNT(*) AS cnt FROM messages WHERE topic_id = ?').get(String(topicRow.id));
97
+ msgCount = row?.cnt ?? 0;
98
+ }
99
+ catch {
100
+ // messages table query failed
101
+ }
102
+ if (msgCount === 0)
103
+ return null;
104
+ this.synthesizer.tick(agentId);
105
+ }
106
+ // Re-fetch after possible synthesis
107
+ const knowledge = this.knowledgeStore.get(agentId, 'topic-synthesis', topicName);
108
+ if (!knowledge)
109
+ return null;
110
+ const crossLinks = this.resolveLinks(agentId, knowledge.id);
111
+ return {
112
+ topicName,
113
+ content: knowledge.content,
114
+ version: this.getVersion(agentId, topicName),
115
+ updatedAt: knowledge.updatedAt,
116
+ crossLinks,
117
+ };
118
+ }
119
+ /**
120
+ * List all synthesized pages for an agent — the table of contents.
121
+ */
122
+ listPages(agentId, opts) {
123
+ const domain = opts?.domain ?? 'topic-synthesis';
124
+ const limit = opts?.limit ?? 100;
125
+ let rows;
126
+ try {
127
+ rows = this.libraryDb.prepare(`
128
+ SELECT key, updated_at, version, source_ref
129
+ FROM knowledge
130
+ WHERE agent_id = ?
131
+ AND domain = ?
132
+ AND superseded_by IS NULL
133
+ ORDER BY updated_at DESC
134
+ LIMIT ?
135
+ `).all(agentId, domain, limit);
136
+ }
137
+ catch {
138
+ return [];
139
+ }
140
+ return rows.map(row => ({
141
+ topicName: row.key,
142
+ updatedAt: row.updated_at,
143
+ messageCount: parseStoredMessageCount(row.source_ref),
144
+ version: row.version,
145
+ }));
146
+ }
147
+ /**
148
+ * Get a page's cross-links from knowledge_links table.
149
+ * Resolves both directions (from and to).
150
+ */
151
+ resolveLinks(agentId, knowledgeId) {
152
+ const links = [];
153
+ // Outgoing links (from this knowledge entry to another)
154
+ let fromRows = [];
155
+ try {
156
+ fromRows = this.libraryDb.prepare(`
157
+ SELECT kl.to_id, kl.link_type
158
+ FROM knowledge_links kl
159
+ WHERE kl.from_type = 'knowledge' AND kl.from_id = ?
160
+ AND kl.to_type = 'knowledge'
161
+ `).all(knowledgeId);
162
+ }
163
+ catch {
164
+ // knowledge_links may not exist
165
+ }
166
+ for (const row of fromRows) {
167
+ // Look up the topic name from the target knowledge row
168
+ let targetKey = null;
169
+ try {
170
+ const targetRow = this.libraryDb.prepare(`
171
+ SELECT key FROM knowledge
172
+ WHERE id = ? AND agent_id = ? AND domain = 'topic-synthesis'
173
+ `).get(row.to_id, agentId);
174
+ targetKey = targetRow?.key ?? null;
175
+ }
176
+ catch {
177
+ // Ignore
178
+ }
179
+ if (targetKey) {
180
+ links.push({ topicName: targetKey, linkType: row.link_type, direction: 'from' });
181
+ }
182
+ }
183
+ // Incoming links (from other knowledge entries to this one)
184
+ let toRows = [];
185
+ try {
186
+ toRows = this.libraryDb.prepare(`
187
+ SELECT kl.from_id, kl.link_type
188
+ FROM knowledge_links kl
189
+ WHERE kl.to_type = 'knowledge' AND kl.to_id = ?
190
+ AND kl.from_type = 'knowledge'
191
+ `).all(knowledgeId);
192
+ }
193
+ catch {
194
+ // Ignore
195
+ }
196
+ for (const row of toRows) {
197
+ let sourceKey = null;
198
+ try {
199
+ const sourceRow = this.libraryDb.prepare(`
200
+ SELECT key FROM knowledge
201
+ WHERE id = ? AND agent_id = ? AND domain = 'topic-synthesis'
202
+ `).get(row.from_id, agentId);
203
+ sourceKey = sourceRow?.key ?? null;
204
+ }
205
+ catch {
206
+ // Ignore
207
+ }
208
+ if (sourceKey) {
209
+ links.push({ topicName: sourceKey, linkType: row.link_type, direction: 'to' });
210
+ }
211
+ }
212
+ return links;
213
+ }
214
+ /**
215
+ * Force re-synthesis of a specific topic regardless of staleness.
216
+ * Returns the new page or null if topic not found.
217
+ */
218
+ forceSynthesize(agentId, topicName) {
219
+ // Verify topic exists
220
+ let topicId;
221
+ try {
222
+ const row = this.libraryDb.prepare(`
223
+ SELECT id FROM topics WHERE agent_id = ? AND name = ? LIMIT 1
224
+ `).get(agentId, topicName);
225
+ topicId = row?.id;
226
+ }
227
+ catch {
228
+ // Topics table may not exist
229
+ }
230
+ if (topicId === undefined)
231
+ return null;
232
+ // Invalidate existing synthesis by setting superseded_by so tick() will re-synthesize
233
+ // Strategy: temporarily lower the stored message_count to force re-synthesis.
234
+ // Actually, simplest approach: call tick() after removing the existing knowledge entry.
235
+ // But KnowledgeStore doesn't have a delete method. Instead, we can use the upsert
236
+ // with forced content change to invalidate, then call tick().
237
+ //
238
+ // Better approach: use a fresh synthesizer with regrowthThreshold=0 so any growth triggers.
239
+ const forceSynth = new TopicSynthesizer(this.libraryDb, this.getMessageDb, {
240
+ ...this.synthConfig,
241
+ SYNTHESIS_REGROWTH_THRESHOLD: 0,
242
+ SYNTHESIS_STALE_MINUTES: 0, // bypass staleness time gate too
243
+ });
244
+ forceSynth.tick(agentId);
245
+ const knowledge = this.knowledgeStore.get(agentId, 'topic-synthesis', topicName);
246
+ if (!knowledge)
247
+ return null;
248
+ const crossLinks = this.resolveLinks(agentId, knowledge.id);
249
+ return {
250
+ topicName,
251
+ content: knowledge.content,
252
+ version: this.getVersion(agentId, topicName),
253
+ updatedAt: knowledge.updatedAt,
254
+ crossLinks,
255
+ };
256
+ }
257
+ }
258
+ //# sourceMappingURL=wiki-page-emitter.js.map
@@ -1,5 +1,5 @@
1
1
  /**
2
- * HyperMem Work Item Store
2
+ * hypermem Work Item Store
3
3
  *
4
4
  * Fleet kanban board in SQL. Replaces WORKQUEUE.md.
5
5
  * Lives in the central library DB.