moflo 4.10.25-rc.1 → 4.10.25

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.
@@ -87,11 +87,11 @@ mcp__moflo__memory_store {
87
87
  namespace: "learnings",
88
88
  key: "<stable descriptive slug, e.g. pattern:daemon-port-resolver or gotcha:windows-spell-path>",
89
89
  value: "<the lesson> — Why: <why it matters>. How to apply: <what to do next time>.",
90
- tags: ["<topic>", "<area>"]
90
+ tags: ["<topic>", "<area>", "source:meditate-manual"]
91
91
  }
92
92
  ```
93
93
 
94
- Keep keys stable and descriptive so the next `/meditate` updates rather than re-adds. In `--preview` mode, **stop here** — print the candidates and their would-be keys/dedup verdicts, write nothing.
94
+ Always include the `source:meditate-manual` tag — it lets the Luminarium "Learnings" panel attribute each lesson to `/meditate` vs the automatic auto-meditate distill. Keep keys stable and descriptive so the next `/meditate` updates rather than re-adds. In `--preview` mode, **stop here** — print the candidates and their would-be keys/dedup verdicts, write nothing.
95
95
 
96
96
  ## Step 4 — Report
97
97
 
@@ -488,6 +488,7 @@ export function buildDistillPrompt(entries) {
488
488
  `1. For EACH candidate, call mcp__moflo__memory_search { namespace: "learnings", query: <bare keywords>, threshold: 0.6, limit: 5 }.`,
489
489
  `2. If the top hit is the SAME fact at similarity >= 0.80, call mcp__moflo__memory_store with that SAME key (upsert), merging any new nuance — do NOT create a near-duplicate.`,
490
490
  `3. Otherwise call mcp__moflo__memory_store { namespace: "learnings", key: <stable descriptive slug>, value: "<lesson> — Why: <why it matters>. How to apply: <what to do next time>.", tags: [<topic>, <area>] }.`,
491
+ ` PROVENANCE (#1203): every memory_store you make — both the step-2 upsert and the step-3 new write — MUST include the tag "source:auto-meditate" in its tags array, so the Luminarium "Learnings" panel can attribute these to the automatic distill pass.`,
491
492
  `4. Skip any candidate that does not clear the durability bar below, or that merely restates an existing entry.`,
492
493
  ``,
493
494
  DURABILITY_BAR,
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Lazy AgentDB v3 bridge loader (ADR-053).
3
+ *
4
+ * Extracted from `memory-initializer.ts` (#1203 decomposition) so the
5
+ * HNSW, embedding, entry-CRUD, and init modules share ONE process-wide
6
+ * bridge singleton instead of each holding their own. Behaviour is
7
+ * identical to the pre-split inline `getBridge()`: first call attempts the
8
+ * dynamic import; a failure caches `null` so subsequent calls short-circuit.
9
+ *
10
+ * @module memory/bridge-loader
11
+ */
12
+ // ADR-053: Lazy import of AgentDB v3 bridge.
13
+ // `undefined` = not yet attempted; `null` = attempted and failed (cached).
14
+ let _bridge;
15
+ /**
16
+ * Return the lazily-loaded memory-bridge module, or `null` if it (or its
17
+ * native deps) can't be loaded. Result is memoized — a failed load caches
18
+ * `null` so we don't retry the import on every call.
19
+ */
20
+ export async function getBridge() {
21
+ if (_bridge === null)
22
+ return null;
23
+ if (_bridge)
24
+ return _bridge;
25
+ try {
26
+ _bridge = await import('./memory-bridge.js');
27
+ return _bridge;
28
+ }
29
+ catch {
30
+ _bridge = null;
31
+ return null;
32
+ }
33
+ }
34
+ /**
35
+ * True once the bridge module has been successfully loaded. Lets callers
36
+ * (e.g. `getHNSWStatus`) report AgentDB-v3 availability without forcing a
37
+ * load. Mirrors the pre-split `_bridge && _bridge !== null` check.
38
+ */
39
+ export function isBridgeLoaded() {
40
+ return !!_bridge;
41
+ }
42
+ //# sourceMappingURL=bridge-loader.js.map
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Neural embedding model manager.
3
+ *
4
+ * Extracted from `memory-initializer.ts` (#1203 decomposition). Lazily loads
5
+ * the fastembed-backed embedding service (ONNX all-MiniLM-L6-v2) and generates
6
+ * embeddings. There is NO hash fallback — a failed model load throws (hash
7
+ * embeddings were removed in epic #527).
8
+ *
9
+ * @module memory/embedding-model
10
+ */
11
+ import { formatEmbeddingError } from './embedding-errors.js';
12
+ import { getBridge } from './bridge-loader.js';
13
+ let embeddingModelState = null;
14
+ /**
15
+ * Lazy-load the neural embedding service.
16
+ *
17
+ * Delegates to the local fastembed-backed service. Returns a diagnostic
18
+ * result for callers that want to report status; if model loading fails later
19
+ * on first `embed()`, that throws from `generateEmbedding`.
20
+ */
21
+ export async function loadEmbeddingModel(options) {
22
+ const { verbose = false } = options || {};
23
+ const startTime = Date.now();
24
+ if (embeddingModelState?.loaded) {
25
+ return {
26
+ success: true,
27
+ dimensions: embeddingModelState.dimensions,
28
+ modelName: 'cached',
29
+ loadTime: 0,
30
+ };
31
+ }
32
+ // ADR-053: Try AgentDB v3 bridge first
33
+ const bridge = await getBridge();
34
+ if (bridge) {
35
+ const bridgeResult = await bridge.bridgeLoadEmbeddingModel();
36
+ if (bridgeResult && bridgeResult.success) {
37
+ embeddingModelState = {
38
+ loaded: true,
39
+ service: null, // Bridge handles embedding
40
+ dimensions: bridgeResult.dimensions,
41
+ modelName: bridgeResult.modelName || 'bridge',
42
+ };
43
+ return bridgeResult;
44
+ }
45
+ }
46
+ if (verbose) {
47
+ console.log('Preparing neural embedding runtime (fastembed / all-MiniLM-L6-v2)...');
48
+ }
49
+ const { createEmbeddingService } = await import('../embeddings/embedding-service.js');
50
+ const service = createEmbeddingService({
51
+ provider: 'fastembed',
52
+ dimensions: 384,
53
+ });
54
+ embeddingModelState = {
55
+ loaded: true,
56
+ service,
57
+ dimensions: 384,
58
+ modelName: 'fastembed/all-MiniLM-L6-v2',
59
+ };
60
+ return {
61
+ success: true,
62
+ dimensions: 384,
63
+ modelName: 'fastembed/all-MiniLM-L6-v2',
64
+ loadTime: Date.now() - startTime,
65
+ };
66
+ }
67
+ /**
68
+ * Generate a neural embedding for text.
69
+ *
70
+ * Uses the fastembed-backed service from cli's embeddings module. Throws on model
71
+ * load / inference failure — there is no hash fallback.
72
+ */
73
+ export async function generateEmbedding(text) {
74
+ // ADR-053: Try AgentDB v3 bridge first
75
+ const bridge = await getBridge();
76
+ if (bridge) {
77
+ const bridgeResult = await bridge.bridgeGenerateEmbedding(text);
78
+ if (bridgeResult)
79
+ return bridgeResult;
80
+ }
81
+ if (!embeddingModelState?.loaded) {
82
+ const load = await loadEmbeddingModel();
83
+ if (!load.success) {
84
+ throw new Error(formatEmbeddingError(load.error ?? 'unknown error'));
85
+ }
86
+ }
87
+ const state = embeddingModelState;
88
+ if (!state.service) {
89
+ throw new Error(`Embedding model state has no active service. Bridge-backed state requires ` +
90
+ `bridge.bridgeGenerateEmbedding(); hash fallback was removed in epic #527.`);
91
+ }
92
+ let result;
93
+ try {
94
+ result = await state.service.embed(text);
95
+ }
96
+ catch (err) {
97
+ throw new Error(formatEmbeddingError(err));
98
+ }
99
+ const embedding = Array.from(result.embedding);
100
+ return {
101
+ embedding,
102
+ dimensions: embedding.length,
103
+ model: state.modelName,
104
+ };
105
+ }
106
+ /**
107
+ * Generate embeddings for multiple texts
108
+ * Uses parallel execution for API-based providers (2-4x faster)
109
+ * Note: Local ONNX inference is CPU-bound, so parallelism has limited benefit
110
+ *
111
+ * @param texts - Array of texts to embed
112
+ * @param options - Batch options
113
+ * @returns Array of embedding results with timing info
114
+ */
115
+ export async function generateBatchEmbeddings(texts, options) {
116
+ const { concurrency = texts.length, onProgress } = options || {};
117
+ const startTime = Date.now();
118
+ // Ensure model is loaded first (prevents cold start in parallel)
119
+ if (!embeddingModelState?.loaded) {
120
+ await loadEmbeddingModel();
121
+ }
122
+ // Process in parallel with optional concurrency limit
123
+ if (concurrency >= texts.length) {
124
+ // Full parallelism
125
+ const embeddings = await Promise.all(texts.map(async (text, i) => {
126
+ const result = await generateEmbedding(text);
127
+ onProgress?.(i + 1, texts.length);
128
+ return { text, ...result };
129
+ }));
130
+ const totalTime = Date.now() - startTime;
131
+ return {
132
+ results: embeddings,
133
+ totalTime,
134
+ avgTime: totalTime / texts.length
135
+ };
136
+ }
137
+ // Limited concurrency using chunking
138
+ const results = [];
139
+ let completed = 0;
140
+ for (let i = 0; i < texts.length; i += concurrency) {
141
+ const chunk = texts.slice(i, i + concurrency);
142
+ const chunkResults = await Promise.all(chunk.map(async (text) => {
143
+ const result = await generateEmbedding(text);
144
+ completed++;
145
+ onProgress?.(completed, texts.length);
146
+ return { text, ...result };
147
+ }));
148
+ results.push(...chunkResults);
149
+ }
150
+ const totalTime = Date.now() - startTime;
151
+ return {
152
+ results,
153
+ totalTime,
154
+ avgTime: totalTime / texts.length
155
+ };
156
+ }
157
+ //# sourceMappingURL=embedding-model.js.map
@@ -0,0 +1,380 @@
1
+ /**
2
+ * Memory entry read path: search / list / get / namespace counts.
3
+ *
4
+ * Extracted from `memory-initializer.ts` (#1203 decomposition). All reads
5
+ * follow the #1058 read-side routing preamble (route through the daemon's
6
+ * HTTP RPC when reachable so callers see its authoritative post-write state),
7
+ * then the AgentDB v3 bridge, then a direct node:sqlite query.
8
+ *
9
+ * @module memory/entries-read
10
+ */
11
+ import * as fs from 'fs';
12
+ import { errorDetail } from '../shared/utils/error-detail.js';
13
+ import { memoryDbPath } from '../services/moflo-paths.js';
14
+ import { openDaemonDatabase } from './daemon-backend.js';
15
+ import { ensureSchemaColumns } from './schema.js';
16
+ import { generateEmbedding } from './embedding-model.js';
17
+ import { searchHNSWIndex } from './hnsw-singleton.js';
18
+ import { getBridge } from './bridge-loader.js';
19
+ import { tryDaemonGet, tryDaemonSearch, tryDaemonList } from './daemon-write-client.js';
20
+ import { searchCandidateCap } from './bridge-core.js';
21
+ import { cosineSim, logRoutingFault } from './entries-shared.js';
22
+ /**
23
+ * Search entries via node:sqlite with vector similarity.
24
+ * Uses HNSW index for 150x faster search when available.
25
+ */
26
+ export async function searchEntries(options) {
27
+ // #1058 — read-side routing preamble. When a daemon is reachable AND we're
28
+ // not the daemon ourselves AND no custom dbPath was supplied, route the
29
+ // search through the daemon's HTTP RPC so callers see its authoritative,
30
+ // up-to-the-write state. Without this, a non-daemon process queries its
31
+ // own bridge's sql.js snapshot loaded at process-start and never sees
32
+ // anything the daemon has written since (epic #1054 silent-drop).
33
+ if (!options.dbPath
34
+ && process.env.MOFLO_IS_DAEMON !== '1'
35
+ && process.env.MOFLO_DISABLE_DAEMON_ROUTING !== '1') {
36
+ try {
37
+ const routed = await tryDaemonSearch({
38
+ query: options.query,
39
+ namespace: options.namespace,
40
+ limit: options.limit,
41
+ threshold: options.threshold,
42
+ });
43
+ if (routed.routed && routed.data) {
44
+ return {
45
+ success: true,
46
+ results: routed.data.results,
47
+ searchTime: routed.data.searchTime ?? 0,
48
+ };
49
+ }
50
+ // #1101 — daemon rejected query (4xx); propagate instead of falling back.
51
+ if (routed.routed && routed.error) {
52
+ return { success: false, results: [], searchTime: 0, error: routed.error };
53
+ }
54
+ }
55
+ catch (err) {
56
+ logRoutingFault(err);
57
+ }
58
+ }
59
+ // ADR-053: Try AgentDB v3 bridge first
60
+ const bridge = await getBridge();
61
+ if (bridge) {
62
+ const bridgeResult = await bridge.bridgeSearchEntries(options);
63
+ if (bridgeResult)
64
+ return bridgeResult;
65
+ }
66
+ // Fallback: direct node:sqlite write via the unified factory.
67
+ const { query, namespace = 'default', limit = 10, threshold = 0.3, dbPath: customPath } = options;
68
+ const dbPath = customPath || memoryDbPath(process.cwd());
69
+ const startTime = Date.now();
70
+ try {
71
+ if (!fs.existsSync(dbPath)) {
72
+ return { success: false, results: [], searchTime: 0, error: 'Database not found' };
73
+ }
74
+ // Ensure schema has all required columns (migration for older DBs)
75
+ await ensureSchemaColumns(dbPath);
76
+ // Generate query embedding
77
+ const queryEmb = await generateEmbedding(query);
78
+ const queryEmbedding = queryEmb.embedding;
79
+ // Try HNSW search first (150x faster)
80
+ const hnswResults = await searchHNSWIndex(queryEmbedding, { k: limit, namespace });
81
+ if (hnswResults && hnswResults.length > 0) {
82
+ // Filter by threshold
83
+ const filtered = hnswResults.filter(r => r.score >= threshold);
84
+ return {
85
+ success: true,
86
+ results: filtered,
87
+ searchTime: Date.now() - startTime
88
+ };
89
+ }
90
+ // Fall back to brute-force SQLite search via the unified factory.
91
+ const db = openDaemonDatabase(dbPath);
92
+ // Get entries with embeddings
93
+ // #1201 — recency-ordered candidate cap (see searchCandidateCap). A bare
94
+ // LIMIT truncated by rowid, hiding recent non-code-map namespaces from a
95
+ // no-namespace search.
96
+ const entries = db.exec(`
97
+ SELECT id, key, namespace, content, metadata, embedding
98
+ FROM memory_entries
99
+ WHERE status = 'active'
100
+ ${namespace !== 'all' ? `AND namespace = '${namespace.replace(/'/g, "''")}'` : ''}
101
+ ORDER BY created_at DESC
102
+ LIMIT ${searchCandidateCap()}
103
+ `);
104
+ const results = [];
105
+ if (entries[0]?.values) {
106
+ for (const row of entries[0].values) {
107
+ const [id, key, ns, content, metadataJson, embeddingJson] = row;
108
+ let score = 0;
109
+ if (embeddingJson) {
110
+ try {
111
+ const embedding = JSON.parse(embeddingJson);
112
+ score = cosineSim(queryEmbedding, embedding);
113
+ }
114
+ catch {
115
+ // Invalid embedding, use keyword score
116
+ }
117
+ }
118
+ // Skip entries without valid semantic embeddings — keyword fallback
119
+ // produces misleading 0.500 scores that degrade search quality.
120
+ // Entries must have real vector embeddings to participate in semantic search.
121
+ if (score < threshold) {
122
+ continue;
123
+ }
124
+ if (score >= threshold) {
125
+ results.push({
126
+ id: id.substring(0, 12),
127
+ key: key || id.substring(0, 15),
128
+ content: (content || '').substring(0, 60) + ((content || '').length > 60 ? '...' : ''),
129
+ score,
130
+ namespace: ns || 'default',
131
+ metadata: metadataJson || undefined
132
+ });
133
+ }
134
+ }
135
+ }
136
+ db.close();
137
+ // Sort by score
138
+ results.sort((a, b) => b.score - a.score);
139
+ return {
140
+ success: true,
141
+ results: results.slice(0, limit),
142
+ searchTime: Date.now() - startTime
143
+ };
144
+ }
145
+ catch (error) {
146
+ return {
147
+ success: false,
148
+ results: [],
149
+ searchTime: Date.now() - startTime,
150
+ error: errorDetail(error)
151
+ };
152
+ }
153
+ }
154
+ /**
155
+ * List all entries from the memory database
156
+ */
157
+ export async function listEntries(options) {
158
+ // #1058 — read-side routing preamble (mirrors searchEntries/getEntry).
159
+ if (!options.dbPath
160
+ && process.env.MOFLO_IS_DAEMON !== '1'
161
+ && process.env.MOFLO_DISABLE_DAEMON_ROUTING !== '1') {
162
+ try {
163
+ const routed = await tryDaemonList({
164
+ namespace: options.namespace,
165
+ limit: options.limit,
166
+ offset: options.offset,
167
+ });
168
+ if (routed.routed && routed.data) {
169
+ return { success: true, entries: routed.data.entries, total: routed.data.total };
170
+ }
171
+ // #1101 — daemon rejected list args (4xx); propagate.
172
+ if (routed.routed && routed.error) {
173
+ return { success: false, entries: [], total: 0, error: routed.error };
174
+ }
175
+ }
176
+ catch (err) {
177
+ logRoutingFault(err);
178
+ }
179
+ }
180
+ // ADR-053: Try AgentDB v3 bridge first
181
+ const bridge = await getBridge();
182
+ if (bridge) {
183
+ const bridgeResult = await bridge.bridgeListEntries(options);
184
+ if (bridgeResult)
185
+ return bridgeResult;
186
+ }
187
+ // Fallback: direct node:sqlite write via the unified factory.
188
+ const { namespace, limit = 20, offset = 0, dbPath: customPath } = options;
189
+ const dbPath = customPath || memoryDbPath(process.cwd());
190
+ try {
191
+ if (!fs.existsSync(dbPath)) {
192
+ return { success: false, entries: [], total: 0, error: 'Database not found' };
193
+ }
194
+ // Ensure schema has all required columns (migration for older DBs)
195
+ await ensureSchemaColumns(dbPath);
196
+ const db = openDaemonDatabase(dbPath);
197
+ // Get total count
198
+ const countQuery = namespace
199
+ ? `SELECT COUNT(*) as cnt FROM memory_entries WHERE status = 'active' AND namespace = '${namespace.replace(/'/g, "''")}'`
200
+ : `SELECT COUNT(*) as cnt FROM memory_entries WHERE status = 'active'`;
201
+ const countResult = db.exec(countQuery);
202
+ const total = countResult[0]?.values?.[0]?.[0] || 0;
203
+ // Get entries
204
+ const listQuery = `
205
+ SELECT id, key, namespace, content, embedding, access_count, created_at, updated_at
206
+ FROM memory_entries
207
+ WHERE status = 'active'
208
+ ${namespace ? `AND namespace = '${namespace.replace(/'/g, "''")}'` : ''}
209
+ ORDER BY updated_at DESC
210
+ LIMIT ${limit} OFFSET ${offset}
211
+ `;
212
+ const result = db.exec(listQuery);
213
+ const entries = [];
214
+ if (result[0]?.values) {
215
+ for (const row of result[0].values) {
216
+ const [id, key, ns, content, embedding, accessCount, createdAt, updatedAt] = row;
217
+ entries.push({
218
+ id: String(id).substring(0, 20),
219
+ key: key || String(id).substring(0, 15),
220
+ namespace: ns || 'default',
221
+ size: (content || '').length,
222
+ accessCount: accessCount || 0,
223
+ createdAt: createdAt || new Date().toISOString(),
224
+ updatedAt: updatedAt || new Date().toISOString(),
225
+ hasEmbedding: !!embedding && embedding.length > 10
226
+ });
227
+ }
228
+ }
229
+ db.close();
230
+ return { success: true, entries, total };
231
+ }
232
+ catch (error) {
233
+ return {
234
+ success: false,
235
+ entries: [],
236
+ total: 0,
237
+ error: errorDetail(error)
238
+ };
239
+ }
240
+ }
241
+ /**
242
+ * Get a specific entry from the memory database
243
+ */
244
+ export async function getEntry(options) {
245
+ // #1058 — read-side routing preamble (mirrors searchEntries/listEntries).
246
+ if (!options.dbPath
247
+ && process.env.MOFLO_IS_DAEMON !== '1'
248
+ && process.env.MOFLO_DISABLE_DAEMON_ROUTING !== '1') {
249
+ try {
250
+ const routed = await tryDaemonGet({
251
+ namespace: options.namespace ?? 'default',
252
+ key: options.key,
253
+ });
254
+ if (routed.routed && routed.data) {
255
+ return { success: true, found: routed.data.found, entry: routed.data.entry };
256
+ }
257
+ // #1101 — daemon rejected get args (4xx); propagate.
258
+ if (routed.routed && routed.error) {
259
+ return { success: false, found: false, error: routed.error };
260
+ }
261
+ }
262
+ catch (err) {
263
+ logRoutingFault(err);
264
+ }
265
+ }
266
+ // ADR-053: Try AgentDB v3 bridge first
267
+ const bridge = await getBridge();
268
+ if (bridge) {
269
+ const bridgeResult = await bridge.bridgeGetEntry(options);
270
+ if (bridgeResult)
271
+ return bridgeResult;
272
+ }
273
+ // Fallback: direct node:sqlite write via the unified factory.
274
+ const { key, namespace = 'default', dbPath: customPath } = options;
275
+ const dbPath = customPath || memoryDbPath(process.cwd());
276
+ try {
277
+ if (!fs.existsSync(dbPath)) {
278
+ return { success: false, found: false, error: 'Database not found' };
279
+ }
280
+ // Ensure schema has all required columns (migration for older DBs)
281
+ await ensureSchemaColumns(dbPath);
282
+ const db = openDaemonDatabase(dbPath);
283
+ // Find entry by key
284
+ const result = db.exec(`
285
+ SELECT id, key, namespace, content, embedding, access_count, created_at, updated_at, tags, metadata
286
+ FROM memory_entries
287
+ WHERE status = 'active'
288
+ AND key = '${key.replace(/'/g, "''")}'
289
+ AND namespace = '${namespace.replace(/'/g, "''")}'
290
+ LIMIT 1
291
+ `);
292
+ if (!result[0]?.values?.[0]) {
293
+ db.close();
294
+ return { success: true, found: false };
295
+ }
296
+ const [id, entryKey, ns, content, embedding, accessCount, createdAt, updatedAt, tagsJson, metadataJson] = result[0].values[0];
297
+ // #1058: previously this path issued `UPDATE memory_entries SET access_count = ...`
298
+ // followed by `atomicWriteFileSync(dbPath, db.export())` — a read that
299
+ // dumped the entire DB snapshot back to disk just to bump access_count.
300
+ // Any write by another process between this function's readFileSync and
301
+ // the writeback was clobbered (read-side writeback-clobber). Access_count
302
+ // is observability, not correctness — drop the writeback. The caller's
303
+ // return value reports the in-memory incremented count so existing
304
+ // surfaces aren't disturbed; persistence of the counter is deferred to a
305
+ // future controller-table refactor (out of scope).
306
+ db.close();
307
+ let tags = [];
308
+ if (tagsJson) {
309
+ try {
310
+ tags = JSON.parse(tagsJson);
311
+ }
312
+ catch {
313
+ // Invalid JSON
314
+ }
315
+ }
316
+ return {
317
+ success: true,
318
+ found: true,
319
+ entry: {
320
+ id: String(id),
321
+ key: entryKey || String(id),
322
+ namespace: ns || 'default',
323
+ content: content || '',
324
+ accessCount: (accessCount || 0) + 1,
325
+ createdAt: createdAt || new Date().toISOString(),
326
+ updatedAt: updatedAt || new Date().toISOString(),
327
+ hasEmbedding: !!embedding && embedding.length > 10,
328
+ tags,
329
+ metadata: metadataJson || undefined
330
+ }
331
+ };
332
+ }
333
+ catch (error) {
334
+ return {
335
+ success: false,
336
+ found: false,
337
+ error: errorDetail(error)
338
+ };
339
+ }
340
+ }
341
+ /**
342
+ * Get memory stats via a single GROUP BY query — namespace counts plus the
343
+ * number of rows that carry a non-null embedding. One trip to disk; the
344
+ * server-side aggregation replaces a pre-#1149 client iteration that
345
+ * fetched 100 000 rows just to count them.
346
+ *
347
+ * Throws on DB read errors. Returns a zero shape ONLY when the DB file
348
+ * doesn't exist yet (the real "empty project" signal) — never swallows a
349
+ * locked/corrupt-DB error into a fake zero, since that's the exact silent
350
+ * wrong-answer this fix is for.
351
+ */
352
+ export async function getNamespaceCounts(dbPath) {
353
+ const resolvedPath = dbPath || memoryDbPath(process.cwd());
354
+ if (!fs.existsSync(resolvedPath)) {
355
+ return { namespaces: {}, total: 0, withEmbeddings: 0 };
356
+ }
357
+ const db = openDaemonDatabase(resolvedPath);
358
+ try {
359
+ const result = db.exec("SELECT namespace, COUNT(*) AS cnt, SUM(CASE WHEN embedding IS NOT NULL THEN 1 ELSE 0 END) AS emb_cnt " +
360
+ "FROM memory_entries WHERE status = 'active' GROUP BY namespace ORDER BY cnt DESC");
361
+ const namespaces = {};
362
+ let total = 0;
363
+ let withEmbeddings = 0;
364
+ if (result[0]?.values) {
365
+ for (const row of result[0].values) {
366
+ const ns = String(row[0]);
367
+ const count = Number(row[1]);
368
+ const embCount = Number(row[2] ?? 0);
369
+ namespaces[ns] = count;
370
+ total += count;
371
+ withEmbeddings += embCount;
372
+ }
373
+ }
374
+ return { namespaces, total, withEmbeddings };
375
+ }
376
+ finally {
377
+ db.close();
378
+ }
379
+ }
380
+ //# sourceMappingURL=entries-read.js.map
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Internal helpers shared by the memory entry read + write paths.
3
+ *
4
+ * Extracted from `memory-initializer.ts` (#1203 decomposition). These were
5
+ * file-private helpers in the monolith; they live here so `entries-read.ts`
6
+ * and `entries-write.ts` share ONE copy (and one `_routingFaultLogged`
7
+ * latch) without re-exporting them from the public barrel.
8
+ *
9
+ * @module memory/entries-shared
10
+ */
11
+ import * as fs from 'fs';
12
+ import * as path from 'path';
13
+ import { errorDetail } from '../shared/utils/error-detail.js';
14
+ import { hnswIndexPath } from '../services/moflo-paths.js';
15
+ import { writeVectorStatsJson } from './bridge-core.js';
16
+ // #981 — daemon-write-client throws are a contract violation (it's documented
17
+ // as never-throw). When a throw escapes anyway, log to stderr ONCE per process
18
+ // and fall through to the direct-write path. Silent swallow would hide bugs;
19
+ // per-call logging would spam.
20
+ let _routingFaultLogged = false;
21
+ export function logRoutingFault(err) {
22
+ if (_routingFaultLogged)
23
+ return;
24
+ _routingFaultLogged = true;
25
+ process.stderr.write(`moflo: daemon-write-client routing fault (#981, falling back to direct write): ${errorDetail(err)}\n`);
26
+ }
27
+ /**
28
+ * Write vector-stats.json cache for the statusline (no subprocess needed).
29
+ * Called after memory store in the direct-write fallback path. The bridge
30
+ * path goes through refreshVectorStatsCache() in bridge-core.ts instead.
31
+ * @param dbPath - path to the SQLite database file
32
+ * @param stats - exact counts from a db query already in progress (required —
33
+ * making this optional caused issue #639 by silently writing 0)
34
+ */
35
+ export function writeVectorStatsCache(dbPath, stats) {
36
+ try {
37
+ const fileStat = fs.statSync(dbPath);
38
+ const dbSizeKB = Math.floor(fileStat.size / 1024);
39
+ const { vectorCount, namespaces, missing = 0 } = stats;
40
+ const dbDir = path.dirname(dbPath);
41
+ const projectDir = path.dirname(dbDir); // .moflo (or legacy .swarm) -> project root
42
+ let hasHnsw = false;
43
+ try {
44
+ fs.statSync(hnswIndexPath(projectDir));
45
+ hasHnsw = true;
46
+ }
47
+ catch { /* nope */ }
48
+ writeVectorStatsJson(projectDir, { vectorCount, missing, dbSizeKB, namespaces, hasHnsw });
49
+ }
50
+ catch { /* Non-fatal */ }
51
+ }
52
+ /**
53
+ * Optimized cosine similarity
54
+ * V8 JIT-friendly - avoids manual unrolling which can hurt performance
55
+ * ~0.5μs per 384-dim vector comparison
56
+ */
57
+ export function cosineSim(a, b) {
58
+ if (!a || !b || a.length === 0 || b.length === 0)
59
+ return 0;
60
+ const len = Math.min(a.length, b.length);
61
+ let dot = 0, normA = 0, normB = 0;
62
+ // Simple loop - V8 optimizes this well
63
+ for (let i = 0; i < len; i++) {
64
+ const ai = a[i], bi = b[i];
65
+ dot += ai * bi;
66
+ normA += ai * ai;
67
+ normB += bi * bi;
68
+ }
69
+ // Combined sqrt for slightly better performance
70
+ const mag = Math.sqrt(normA * normB);
71
+ return mag === 0 ? 0 : dot / mag;
72
+ }
73
+ //# sourceMappingURL=entries-shared.js.map