moflo 4.10.24 → 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.
- package/.claude/guidance/shipped/moflo-yaml-reference.md +5 -5
- package/.claude/skills/meditate/SKILL.md +2 -2
- package/bin/cli-hooks/statusline.js +12 -8
- package/bin/cli.js +1 -1
- package/bin/hooks.mjs +1 -1
- package/bin/lib/meditate.mjs +1 -0
- package/bin/lib/pii-scrub.mjs +2 -5
- package/dist/src/cli/commands/daemon.js +4 -3
- package/dist/src/cli/commands/doctor-checks-deep.js +2 -2
- package/dist/src/cli/commands/doctor-checks-swarm.js +122 -13
- package/dist/src/cli/commands/hooks.js +7 -23
- package/dist/src/cli/commands/init.js +1 -1
- package/dist/src/cli/commands/mcp.js +2 -1
- package/dist/src/cli/commands/session.js +1 -1
- package/dist/src/cli/commands/start.js +1 -1
- package/dist/src/cli/commands/status.js +1 -1
- package/dist/src/cli/commands/task.js +1 -1
- package/dist/src/cli/commands/update.js +12 -12
- package/dist/src/cli/guidance/analyzer.js +3 -3
- package/dist/src/cli/guidance/gates.js +1 -1
- package/dist/src/cli/guidance/hooks.js +1 -1
- package/dist/src/cli/guidance/meta-governance.js +1 -1
- package/dist/src/cli/hooks/index.js +1 -1
- package/dist/src/cli/hooks/reasoningbank/guidance-provider.js +1 -1
- package/dist/src/cli/hooks/workers/index.js +1 -1
- package/dist/src/cli/hooks/workers/session-hook.js +0 -40
- package/dist/src/cli/index.js +2 -2
- package/dist/src/cli/init/executor.js +36 -20
- package/dist/src/cli/init/mcp-generator.js +10 -8
- package/dist/src/cli/init/settings-generator.js +10 -7
- package/dist/src/cli/init/types.js +2 -2
- package/dist/src/cli/mcp-server.js +2 -1
- package/dist/src/cli/memory/bridge-loader.js +42 -0
- package/dist/src/cli/memory/embedding-model.js +157 -0
- package/dist/src/cli/memory/entries-read.js +380 -0
- package/dist/src/cli/memory/entries-shared.js +73 -0
- package/dist/src/cli/memory/entries-write.js +384 -0
- package/dist/src/cli/memory/hnsw-singleton.js +242 -0
- package/dist/src/cli/memory/init.js +367 -0
- package/dist/src/cli/memory/learnings-overview.js +156 -0
- package/dist/src/cli/memory/memory-initializer.js +37 -2257
- package/dist/src/cli/memory/quantization.js +221 -0
- package/dist/src/cli/memory/schema.js +382 -0
- package/dist/src/cli/memory/verify.js +178 -0
- package/dist/src/cli/movector/index.js +1 -1
- package/dist/src/cli/plugins/store/discovery.js +9 -9
- package/dist/src/cli/{transfer/ipfs/client.js → plugins/store/ipfs-client.js} +4 -1
- package/dist/src/cli/plugins/tests/demo-plugin-store.js +1 -1
- package/dist/src/cli/plugins/tests/standalone-test.js +1 -1
- package/dist/src/cli/runtime/headless.js +5 -4
- package/dist/src/cli/scripts/publish-registry.js +6 -6
- package/dist/src/cli/services/daemon-dashboard.js +108 -7
- package/dist/src/cli/services/daemon-readiness.js +1 -1
- package/dist/src/cli/services/daemon-service.js +1 -1
- package/dist/src/cli/services/env-compat.js +29 -0
- package/dist/src/cli/services/hook-block-hash.js +5 -6
- package/dist/src/cli/services/registry-api.js +1 -1
- package/dist/src/cli/shared/core/config/loader.js +19 -11
- package/dist/src/cli/shared/events/example-usage.js +2 -2
- package/dist/src/cli/shared/events/index.js +1 -1
- package/dist/src/cli/shared/index.js +1 -1
- package/dist/src/cli/shared/mcp/index.js +1 -1
- package/dist/src/cli/shared/mcp/server.js +3 -3
- package/dist/src/cli/shared/plugin-interface.js +1 -1
- package/dist/src/cli/shared/plugins/index.js +1 -1
- package/dist/src/cli/shared/plugins/official/index.js +1 -1
- package/dist/src/cli/shared/security/index.js +1 -1
- package/dist/src/cli/shared/services/v3-progress.service.js +40 -29
- package/dist/src/cli/shared/types.js +1 -1
- package/dist/src/cli/swarm/coordination/swarm-hub.js +1 -1
- package/dist/src/cli/update/index.js +1 -1
- package/dist/src/cli/update/rate-limiter.js +3 -2
- package/dist/src/cli/version.js +1 -1
- package/package.json +2 -2
- package/dist/src/cli/commands/transfer-store.js +0 -428
- package/dist/src/cli/transfer/anonymization/index.js +0 -281
- package/dist/src/cli/transfer/deploy-seraphine.js +0 -205
- package/dist/src/cli/transfer/export.js +0 -113
- package/dist/src/cli/transfer/index.js +0 -31
- package/dist/src/cli/transfer/ipfs/upload.js +0 -411
- package/dist/src/cli/transfer/models/seraphine.js +0 -373
- package/dist/src/cli/transfer/serialization/cfp.js +0 -184
- package/dist/src/cli/transfer/storage/gcs.js +0 -242
- package/dist/src/cli/transfer/storage/index.js +0 -6
- package/dist/src/cli/transfer/store/discovery.js +0 -382
- package/dist/src/cli/transfer/store/download.js +0 -334
- package/dist/src/cli/transfer/store/index.js +0 -153
- package/dist/src/cli/transfer/store/publish.js +0 -294
- package/dist/src/cli/transfer/store/registry.js +0 -285
- package/dist/src/cli/transfer/store/search.js +0 -232
- package/dist/src/cli/transfer/store/tests/standalone-test.js +0 -190
- package/dist/src/cli/transfer/store/types.js +0 -6
- package/dist/src/cli/transfer/test-seraphine.js +0 -105
- package/dist/src/cli/transfer/tests/test-store.js +0 -214
- package/dist/src/cli/transfer/types.js +0 -6
|
@@ -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
|