@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.
- package/ARCHITECTURE.md +4 -3
- package/README.md +457 -174
- package/dist/background-indexer.d.ts +19 -4
- package/dist/background-indexer.d.ts.map +1 -1
- package/dist/background-indexer.js +329 -17
- package/dist/cache.d.ts +110 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/cache.js +495 -0
- package/dist/compaction-fence.d.ts +1 -1
- package/dist/compaction-fence.js +1 -1
- package/dist/compositor.d.ts +114 -27
- package/dist/compositor.d.ts.map +1 -1
- package/dist/compositor.js +1678 -229
- package/dist/content-type-classifier.d.ts +41 -0
- package/dist/content-type-classifier.d.ts.map +1 -0
- package/dist/content-type-classifier.js +181 -0
- package/dist/cross-agent.d.ts +5 -0
- package/dist/cross-agent.d.ts.map +1 -1
- package/dist/cross-agent.js +5 -0
- package/dist/db.d.ts +1 -1
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +6 -2
- package/dist/desired-state-store.d.ts +1 -1
- package/dist/desired-state-store.d.ts.map +1 -1
- package/dist/desired-state-store.js +15 -5
- package/dist/doc-chunk-store.d.ts +26 -1
- package/dist/doc-chunk-store.d.ts.map +1 -1
- package/dist/doc-chunk-store.js +114 -1
- package/dist/doc-chunker.d.ts +1 -1
- package/dist/doc-chunker.js +1 -1
- package/dist/dreaming-promoter.d.ts +86 -0
- package/dist/dreaming-promoter.d.ts.map +1 -0
- package/dist/dreaming-promoter.js +381 -0
- package/dist/episode-store.d.ts +2 -1
- package/dist/episode-store.d.ts.map +1 -1
- package/dist/episode-store.js +4 -4
- package/dist/fact-store.d.ts +19 -1
- package/dist/fact-store.d.ts.map +1 -1
- package/dist/fact-store.js +64 -3
- package/dist/fleet-store.d.ts +1 -1
- package/dist/fleet-store.js +1 -1
- package/dist/fos-mod.d.ts +178 -0
- package/dist/fos-mod.d.ts.map +1 -0
- package/dist/fos-mod.js +416 -0
- package/dist/hybrid-retrieval.d.ts +5 -1
- package/dist/hybrid-retrieval.d.ts.map +1 -1
- package/dist/hybrid-retrieval.js +7 -3
- package/dist/image-eviction.d.ts +49 -0
- package/dist/image-eviction.d.ts.map +1 -0
- package/dist/image-eviction.js +251 -0
- package/dist/index.d.ts +50 -11
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +73 -43
- package/dist/keystone-scorer.d.ts +51 -0
- package/dist/keystone-scorer.d.ts.map +1 -0
- package/dist/keystone-scorer.js +52 -0
- package/dist/knowledge-graph.d.ts +1 -1
- package/dist/knowledge-graph.js +1 -1
- package/dist/knowledge-lint.d.ts +29 -0
- package/dist/knowledge-lint.d.ts.map +1 -0
- package/dist/knowledge-lint.js +116 -0
- package/dist/knowledge-store.d.ts +1 -1
- package/dist/knowledge-store.d.ts.map +1 -1
- package/dist/knowledge-store.js +8 -2
- package/dist/library-schema.d.ts +3 -3
- package/dist/library-schema.d.ts.map +1 -1
- package/dist/library-schema.js +324 -3
- package/dist/message-store.d.ts +15 -2
- package/dist/message-store.d.ts.map +1 -1
- package/dist/message-store.js +51 -1
- package/dist/metrics-dashboard.d.ts +114 -0
- package/dist/metrics-dashboard.d.ts.map +1 -0
- package/dist/metrics-dashboard.js +260 -0
- package/dist/obsidian-exporter.d.ts +57 -0
- package/dist/obsidian-exporter.d.ts.map +1 -0
- package/dist/obsidian-exporter.js +274 -0
- package/dist/obsidian-watcher.d.ts +147 -0
- package/dist/obsidian-watcher.d.ts.map +1 -0
- package/dist/obsidian-watcher.js +403 -0
- package/dist/open-domain.d.ts +46 -0
- package/dist/open-domain.d.ts.map +1 -0
- package/dist/open-domain.js +125 -0
- package/dist/preference-store.d.ts +1 -1
- package/dist/preference-store.js +1 -1
- package/dist/preservation-gate.d.ts +1 -1
- package/dist/preservation-gate.js +1 -1
- package/dist/proactive-pass.d.ts +63 -0
- package/dist/proactive-pass.d.ts.map +1 -0
- package/dist/proactive-pass.js +239 -0
- package/dist/profiles.d.ts +44 -0
- package/dist/profiles.d.ts.map +1 -0
- package/dist/profiles.js +227 -0
- package/dist/provider-translator.d.ts +13 -3
- package/dist/provider-translator.d.ts.map +1 -1
- package/dist/provider-translator.js +63 -9
- package/dist/rate-limiter.d.ts +1 -1
- package/dist/rate-limiter.js +1 -1
- package/dist/repair-tool-pairs.d.ts +38 -0
- package/dist/repair-tool-pairs.d.ts.map +1 -0
- package/dist/repair-tool-pairs.js +138 -0
- package/dist/retrieval-policy.d.ts +51 -0
- package/dist/retrieval-policy.d.ts.map +1 -0
- package/dist/retrieval-policy.js +77 -0
- package/dist/schema.d.ts +2 -2
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +28 -2
- package/dist/secret-scanner.d.ts +1 -1
- package/dist/secret-scanner.js +1 -1
- package/dist/seed.d.ts +2 -2
- package/dist/seed.js +2 -2
- package/dist/session-flusher.d.ts +53 -0
- package/dist/session-flusher.d.ts.map +1 -0
- package/dist/session-flusher.js +69 -0
- package/dist/session-topic-map.d.ts +41 -0
- package/dist/session-topic-map.d.ts.map +1 -0
- package/dist/session-topic-map.js +77 -0
- package/dist/spawn-context.d.ts +54 -0
- package/dist/spawn-context.d.ts.map +1 -0
- package/dist/spawn-context.js +159 -0
- package/dist/system-store.d.ts +1 -1
- package/dist/system-store.js +1 -1
- package/dist/temporal-store.d.ts +80 -0
- package/dist/temporal-store.d.ts.map +1 -0
- package/dist/temporal-store.js +149 -0
- package/dist/topic-detector.d.ts +35 -0
- package/dist/topic-detector.d.ts.map +1 -0
- package/dist/topic-detector.js +249 -0
- package/dist/topic-store.d.ts +1 -1
- package/dist/topic-store.js +1 -1
- package/dist/topic-synthesizer.d.ts +51 -0
- package/dist/topic-synthesizer.d.ts.map +1 -0
- package/dist/topic-synthesizer.js +315 -0
- package/dist/trigger-registry.d.ts +63 -0
- package/dist/trigger-registry.d.ts.map +1 -0
- package/dist/trigger-registry.js +163 -0
- package/dist/types.d.ts +214 -10
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +1 -1
- package/dist/vector-store.d.ts +43 -5
- package/dist/vector-store.d.ts.map +1 -1
- package/dist/vector-store.js +189 -10
- package/dist/version.d.ts +34 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +34 -0
- package/dist/wiki-page-emitter.d.ts +65 -0
- package/dist/wiki-page-emitter.d.ts.map +1 -0
- package/dist/wiki-page-emitter.js +258 -0
- package/dist/work-store.d.ts +1 -1
- package/dist/work-store.js +1 -1
- package/package.json +15 -5
- package/dist/redis.d.ts +0 -188
- package/dist/redis.d.ts.map +0 -1
- package/dist/redis.js +0 -534
package/dist/vector-store.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
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
|
-
* -
|
|
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
|
|
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 <
|
|
33
|
-
const batch =
|
|
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
|
-
|
|
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
|
-
//
|
|
245
|
-
|
|
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"}
|
package/dist/version.js
ADDED
|
@@ -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
|
package/dist/work-store.d.ts
CHANGED