@hanhnd/agent-kit 1.0.38 → 1.0.40
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/dist/cli.js +286 -140
- package/dist/entrypoints/server.js +3 -3
- package/dist/mcp/memory.d.ts +4 -9
- package/dist/mcp/memory.js +43 -19
- package/dist/server.js +445 -212
- package/dist/services/digest/digest-processor.test.js +1 -3
- package/dist/services/memory/index.d.ts +3 -1
- package/dist/services/memory/index.js +1 -0
- package/dist/services/memory/indexer.d.ts +5 -6
- package/dist/services/memory/indexer.js +71 -101
- package/dist/services/memory/indexer.test.js +347 -572
- package/dist/services/memory/memory.test.js +58 -0
- package/dist/services/memory/store.d.ts +4 -11
- package/dist/services/memory/store.js +200 -66
- package/dist/services/memory/store.test.js +359 -108
- package/dist/services/memory/subsystem.d.ts +19 -0
- package/dist/services/memory/subsystem.js +67 -0
- package/dist/services/memory/types.d.ts +48 -0
- package/dist/utils/async.d.ts +1 -0
- package/dist/utils/async.js +17 -0
- package/dist/utils/async.test.d.ts +1 -0
- package/dist/utils/async.test.js +48 -0
- package/package.json +1 -1
|
@@ -45,6 +45,7 @@ function makeIndexerStub(overrides = {}) {
|
|
|
45
45
|
return {
|
|
46
46
|
search: overrides.search ?? (async () => []),
|
|
47
47
|
save: overrides.save ?? (async () => ({ indexed: 1, deleted: 0, skipped: 0 })),
|
|
48
|
+
startupIndex: overrides.startupIndex ?? (async () => undefined),
|
|
48
49
|
};
|
|
49
50
|
}
|
|
50
51
|
function makeStoreStub(vecAvailable, overrides = {}) {
|
|
@@ -176,6 +177,44 @@ describe('kit_memory_search', () => {
|
|
|
176
177
|
await tools.get('kit_memory_search')({ query: 'test', top_k: 3 });
|
|
177
178
|
assert.equal(capturedTopK, 3);
|
|
178
179
|
});
|
|
180
|
+
test('returns a warming note while subsystem warmup has not completed', async () => {
|
|
181
|
+
const indexer = makeIndexerStub({ search: async () => [] });
|
|
182
|
+
const store = makeStoreStub(true);
|
|
183
|
+
const { server, tools } = makeMockServer();
|
|
184
|
+
registerMemoryTools(server, '/tmp', {
|
|
185
|
+
settings: { memory: { enabled: true } },
|
|
186
|
+
indexer,
|
|
187
|
+
store,
|
|
188
|
+
config: BASE_CONFIG,
|
|
189
|
+
});
|
|
190
|
+
const result = await tools.get('kit_memory_search')({ query: 'test' });
|
|
191
|
+
const text = extractText(result);
|
|
192
|
+
assert.ok(text.includes('Memory index is warming'), `Expected warming note, got: ${text}`);
|
|
193
|
+
});
|
|
194
|
+
test('returns degraded note after warmup failure while preserving search results', async () => {
|
|
195
|
+
const chunk = makeChunk({ content: 'Degraded result', source: 'compiled/entities/degraded.md' });
|
|
196
|
+
const indexer = makeIndexerStub({
|
|
197
|
+
search: async () => [{ chunk, score: 0.7, retriever: 'bm25', contentSource: 'file' }],
|
|
198
|
+
startupIndex: async () => {
|
|
199
|
+
throw new Error('warmup failed');
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
const store = makeStoreStub(true);
|
|
203
|
+
const { server, tools } = makeMockServer();
|
|
204
|
+
const subsystem = registerMemoryTools(server, '/tmp', {
|
|
205
|
+
settings: { memory: { enabled: true } },
|
|
206
|
+
indexer,
|
|
207
|
+
store,
|
|
208
|
+
config: BASE_CONFIG,
|
|
209
|
+
});
|
|
210
|
+
assert.ok(subsystem);
|
|
211
|
+
await subsystem.startWarmup();
|
|
212
|
+
const result = await tools.get('kit_memory_search')({ query: 'test' });
|
|
213
|
+
const text = extractText(result);
|
|
214
|
+
assert.ok(text.includes('Memory index is degraded'), `Expected degraded note, got: ${text}`);
|
|
215
|
+
assert.ok(text.includes('warmup failed'), `Expected warmup error, got: ${text}`);
|
|
216
|
+
assert.ok(text.includes('Degraded result'), 'Result must still be included during degraded state');
|
|
217
|
+
});
|
|
179
218
|
test('falls back to config.topK when top_k is not provided', async () => {
|
|
180
219
|
let capturedTopK;
|
|
181
220
|
const indexer = makeIndexerStub({
|
|
@@ -214,6 +253,25 @@ describe('kit_memory_search', () => {
|
|
|
214
253
|
assert.ok(text.includes('kit_memory_search failed'), `Expected error prefix, got: ${text}`);
|
|
215
254
|
assert.ok(text.includes('search exploded'), `Expected error message, got: ${text}`);
|
|
216
255
|
});
|
|
256
|
+
test('returns initialization failure when store construction fails', async () => {
|
|
257
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'memory-init-failure-'));
|
|
258
|
+
const occupiedWikiPath = path.join(tmpDir, 'wiki-file');
|
|
259
|
+
fs.writeFileSync(occupiedWikiPath, 'not a directory', 'utf8');
|
|
260
|
+
try {
|
|
261
|
+
const { server, tools } = makeMockServer();
|
|
262
|
+
const subsystem = registerMemoryTools(server, tmpDir, {
|
|
263
|
+
settings: { memory: { enabled: true } },
|
|
264
|
+
config: { ...BASE_CONFIG, wikiDir: occupiedWikiPath },
|
|
265
|
+
});
|
|
266
|
+
assert.ok(subsystem);
|
|
267
|
+
const result = await tools.get('kit_memory_search')({ query: 'test' });
|
|
268
|
+
const text = extractText(result);
|
|
269
|
+
assert.ok(text.includes('Memory initialization failed'), `Expected initialization failure, got: ${text}`);
|
|
270
|
+
}
|
|
271
|
+
finally {
|
|
272
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
273
|
+
}
|
|
274
|
+
});
|
|
217
275
|
test('description routes factual and temporal queries distinctly', () => {
|
|
218
276
|
const indexer = makeIndexerStub();
|
|
219
277
|
const store = makeStoreStub(true);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { MemoryChunk, MemoryConfig, RecentSource, SourceType } from './types.js';
|
|
1
|
+
import type { HybridSearchOptions, HybridSearchRow, MemoryChunk, MemoryConfig, RecentSource, SourceType } from './types.js';
|
|
2
2
|
export declare const SCHEMA_VERSION = 4;
|
|
3
3
|
export declare class StoreError extends Error {
|
|
4
4
|
constructor(message: string, cause?: unknown);
|
|
@@ -13,7 +13,6 @@ export declare class MemoryStore {
|
|
|
13
13
|
private readUserVersion;
|
|
14
14
|
upsert(chunks: MemoryChunk[], embeddings: Float32Array[]): void;
|
|
15
15
|
private toVectorBinding;
|
|
16
|
-
private normalizeVectorDistance;
|
|
17
16
|
hashesBySource(source: string): Set<string>;
|
|
18
17
|
indexedSources(): string[];
|
|
19
18
|
getRecentSources(options: {
|
|
@@ -22,15 +21,9 @@ export declare class MemoryStore {
|
|
|
22
21
|
}): RecentSource[];
|
|
23
22
|
deleteBySource(source: string): void;
|
|
24
23
|
deleteByIds(ids: string[]): void;
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
}>;
|
|
29
|
-
searchBm25(query: string, limit: number): Array<{
|
|
30
|
-
id: string;
|
|
31
|
-
score: number;
|
|
32
|
-
}>;
|
|
33
|
-
getChunksByIds(ids: string[]): MemoryChunk[];
|
|
24
|
+
private buildFtsQuery;
|
|
25
|
+
private mapMemoryChunkRow;
|
|
26
|
+
searchHybrid(options: HybridSearchOptions): HybridSearchRow[];
|
|
34
27
|
get vecAvailable(): boolean;
|
|
35
28
|
close(): void;
|
|
36
29
|
}
|
|
@@ -186,11 +186,6 @@ export class MemoryStore {
|
|
|
186
186
|
}
|
|
187
187
|
return Buffer.from(embedding.buffer, embedding.byteOffset, embedding.byteLength);
|
|
188
188
|
}
|
|
189
|
-
normalizeVectorDistance(distance) {
|
|
190
|
-
if (distance <= 0)
|
|
191
|
-
return 1;
|
|
192
|
-
return Math.max(0, Math.min(1, 1 - distance));
|
|
193
|
-
}
|
|
194
189
|
hashesBySource(source) {
|
|
195
190
|
const rows = this.db.prepare(`SELECT id FROM memory_chunks WHERE source = ?`).all(source);
|
|
196
191
|
return new Set(rows.map((r) => r.id));
|
|
@@ -229,74 +224,18 @@ export class MemoryStore {
|
|
|
229
224
|
});
|
|
230
225
|
deleteAll();
|
|
231
226
|
}
|
|
232
|
-
|
|
233
|
-
try {
|
|
234
|
-
const queryVector = this.toVectorBinding(embedding);
|
|
235
|
-
const rows = this.db
|
|
236
|
-
.prepare(`SELECT mc.id, vector_distance_cos(mc.embedding, vector32(?)) AS distance
|
|
237
|
-
FROM vector_top_k('idx_memory_chunks_embedding', vector32(?), ?) AS vector_matches
|
|
238
|
-
JOIN memory_chunks mc ON mc.rowid = vector_matches.id
|
|
239
|
-
ORDER BY distance
|
|
240
|
-
LIMIT ?`)
|
|
241
|
-
.all(queryVector, queryVector, limit, limit);
|
|
242
|
-
return rows.map((r) => ({
|
|
243
|
-
id: r.id,
|
|
244
|
-
score: this.normalizeVectorDistance(r.distance),
|
|
245
|
-
}));
|
|
246
|
-
}
|
|
247
|
-
catch (err) {
|
|
248
|
-
console.warn('[memory-store] Dense search failed:', err);
|
|
249
|
-
return [];
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
searchBm25(query, limit) {
|
|
253
|
-
if (!query.trim())
|
|
254
|
-
return [];
|
|
227
|
+
buildFtsQuery(query) {
|
|
255
228
|
const tokens = query
|
|
256
229
|
.trim()
|
|
257
230
|
.split(/\s+/)
|
|
258
231
|
.map((t) => t.replace(/[^a-zA-Z0-9_]/g, '').toLowerCase())
|
|
259
232
|
.filter((t) => t.length > 1);
|
|
260
|
-
|
|
233
|
+
return removeStopwords(tokens, eng)
|
|
261
234
|
.map((t) => `${t.replace(/s$/, '')}*`)
|
|
262
235
|
.join(' OR ');
|
|
263
|
-
if (!ftsQuery)
|
|
264
|
-
return [];
|
|
265
|
-
try {
|
|
266
|
-
const rows = this.db
|
|
267
|
-
.prepare(`SELECT mc.id, bm25(memory_fts, 0.2, 2.0, 5.0) AS rank
|
|
268
|
-
FROM memory_fts
|
|
269
|
-
JOIN memory_chunks mc ON mc.rowid = memory_fts.rowid
|
|
270
|
-
WHERE memory_fts MATCH ?
|
|
271
|
-
ORDER BY rank
|
|
272
|
-
LIMIT ?`)
|
|
273
|
-
.all(ftsQuery, limit);
|
|
274
|
-
if (rows.length === 0)
|
|
275
|
-
return [];
|
|
276
|
-
// BM25 rank is negative in SQLite FTS5 (lower = more relevant)
|
|
277
|
-
const ranks = rows.map((r) => r.rank);
|
|
278
|
-
const minRank = Math.min(...ranks);
|
|
279
|
-
const maxRank = Math.max(...ranks);
|
|
280
|
-
const range = maxRank - minRank;
|
|
281
|
-
return rows.map((r) => ({
|
|
282
|
-
id: r.id,
|
|
283
|
-
score: range > 0 ? (maxRank - r.rank) / range : 1,
|
|
284
|
-
}));
|
|
285
|
-
}
|
|
286
|
-
catch (err) {
|
|
287
|
-
console.warn('[memory-store] BM25 search failed:', err);
|
|
288
|
-
return [];
|
|
289
|
-
}
|
|
290
236
|
}
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
return [];
|
|
294
|
-
const placeholders = ids.map(() => '?').join(', ');
|
|
295
|
-
const rows = this.db
|
|
296
|
-
.prepare(`SELECT id, source, source_type, heading, heading_level, content, line_start, line_end, file_mtime_at
|
|
297
|
-
FROM memory_chunks WHERE id IN (${placeholders})`)
|
|
298
|
-
.all(...ids);
|
|
299
|
-
return rows.map((r) => ({
|
|
237
|
+
mapMemoryChunkRow(r) {
|
|
238
|
+
return {
|
|
300
239
|
id: r.id,
|
|
301
240
|
source: r.source,
|
|
302
241
|
sourceType: r.source_type,
|
|
@@ -306,7 +245,202 @@ export class MemoryStore {
|
|
|
306
245
|
lineStart: r.line_start,
|
|
307
246
|
lineEnd: r.line_end,
|
|
308
247
|
fileMtimeAt: r.file_mtime_at,
|
|
309
|
-
}
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
searchHybrid(options) {
|
|
251
|
+
const { query, embedding, topK, fetchLimit, denseScoreFloor, recencyWeight, rrfK, sourceType, sinceMtimeAt, includeDebug, } = options;
|
|
252
|
+
const ftsQuery = this.buildFtsQuery(query);
|
|
253
|
+
const hasDense = embedding !== undefined;
|
|
254
|
+
const hasBm25 = ftsQuery.length > 0;
|
|
255
|
+
if (!hasDense && !hasBm25)
|
|
256
|
+
return [];
|
|
257
|
+
const hasFilters = sourceType !== undefined || sinceMtimeAt !== undefined;
|
|
258
|
+
try {
|
|
259
|
+
const params = [];
|
|
260
|
+
let denseCte = '';
|
|
261
|
+
let denseRankedCte = '';
|
|
262
|
+
if (hasDense) {
|
|
263
|
+
const queryBuf = this.toVectorBinding(embedding);
|
|
264
|
+
if (hasFilters) {
|
|
265
|
+
// Exact filtered scan — avoids losing candidates due to ANN post-filtering
|
|
266
|
+
const filterClauses = [];
|
|
267
|
+
if (sourceType) {
|
|
268
|
+
filterClauses.push('mc.source_type = ?');
|
|
269
|
+
params.push(sourceType);
|
|
270
|
+
}
|
|
271
|
+
if (sinceMtimeAt !== undefined) {
|
|
272
|
+
filterClauses.push('mc.file_mtime_at >= ?');
|
|
273
|
+
params.push(sinceMtimeAt);
|
|
274
|
+
}
|
|
275
|
+
const whereClause = filterClauses.length > 0 ? `WHERE ${filterClauses.join(' AND ')}` : '';
|
|
276
|
+
params.push(queryBuf); // vector_distance_cos arg
|
|
277
|
+
params.push(fetchLimit);
|
|
278
|
+
denseCte = `dense_data AS (
|
|
279
|
+
SELECT mc.id, mc.source, mc.source_type, mc.file_mtime_at,
|
|
280
|
+
mc.heading, mc.heading_level, mc.content, mc.line_start, mc.line_end,
|
|
281
|
+
vector_distance_cos(mc.embedding, vector32(?)) AS dense_dist
|
|
282
|
+
FROM memory_chunks mc
|
|
283
|
+
${whereClause}
|
|
284
|
+
ORDER BY dense_dist
|
|
285
|
+
LIMIT ?
|
|
286
|
+
)`;
|
|
287
|
+
}
|
|
288
|
+
else {
|
|
289
|
+
params.push(queryBuf); // vector_distance_cos arg
|
|
290
|
+
params.push(queryBuf); // vector_top_k arg
|
|
291
|
+
params.push(fetchLimit);
|
|
292
|
+
denseCte = `dense_data AS (
|
|
293
|
+
SELECT mc.id, mc.source, mc.source_type, mc.file_mtime_at,
|
|
294
|
+
mc.heading, mc.heading_level, mc.content, mc.line_start, mc.line_end,
|
|
295
|
+
vector_distance_cos(mc.embedding, vector32(?)) AS dense_dist
|
|
296
|
+
FROM vector_top_k('idx_memory_chunks_embedding', vector32(?), ?) AS vtk
|
|
297
|
+
JOIN memory_chunks mc ON mc.rowid = vtk.id
|
|
298
|
+
)`;
|
|
299
|
+
}
|
|
300
|
+
denseRankedCte = `dense_ranked AS (
|
|
301
|
+
SELECT *, ROW_NUMBER() OVER (ORDER BY dense_dist, id) AS dense_rank
|
|
302
|
+
FROM dense_data
|
|
303
|
+
)`;
|
|
304
|
+
}
|
|
305
|
+
let bm25Cte = '';
|
|
306
|
+
let bm25RankedCte = '';
|
|
307
|
+
if (hasBm25) {
|
|
308
|
+
const bm25FilterClauses = [];
|
|
309
|
+
if (sourceType)
|
|
310
|
+
bm25FilterClauses.push('mc.source_type = ?');
|
|
311
|
+
if (sinceMtimeAt !== undefined)
|
|
312
|
+
bm25FilterClauses.push('mc.file_mtime_at >= ?');
|
|
313
|
+
const bm25WhereExtra = bm25FilterClauses.length > 0 ? ` AND ${bm25FilterClauses.join(' AND ')}` : '';
|
|
314
|
+
params.push(ftsQuery);
|
|
315
|
+
if (sourceType)
|
|
316
|
+
params.push(sourceType);
|
|
317
|
+
if (sinceMtimeAt !== undefined)
|
|
318
|
+
params.push(sinceMtimeAt);
|
|
319
|
+
params.push(fetchLimit);
|
|
320
|
+
bm25Cte = `bm25_data AS (
|
|
321
|
+
SELECT mc.id, mc.source, mc.source_type, mc.file_mtime_at,
|
|
322
|
+
mc.heading, mc.heading_level, mc.content, mc.line_start, mc.line_end,
|
|
323
|
+
bm25(memory_fts, 0.2, 2.0, 5.0) AS bm25_neg
|
|
324
|
+
FROM memory_fts
|
|
325
|
+
JOIN memory_chunks mc ON mc.rowid = memory_fts.rowid
|
|
326
|
+
WHERE memory_fts MATCH ?${bm25WhereExtra}
|
|
327
|
+
LIMIT ?
|
|
328
|
+
)`;
|
|
329
|
+
bm25RankedCte = `bm25_ranked AS (
|
|
330
|
+
SELECT *, ROW_NUMBER() OVER (ORDER BY bm25_neg, id) AS bm25_rank
|
|
331
|
+
FROM bm25_data
|
|
332
|
+
)`;
|
|
333
|
+
}
|
|
334
|
+
let candidatesCte = '';
|
|
335
|
+
if (hasDense && hasBm25) {
|
|
336
|
+
candidatesCte = `all_candidates AS (
|
|
337
|
+
SELECT d.id, d.source, d.source_type, d.file_mtime_at,
|
|
338
|
+
d.heading, d.heading_level, d.content, d.line_start, d.line_end,
|
|
339
|
+
CAST(d.dense_rank AS INTEGER) AS dense_rank, d.dense_dist,
|
|
340
|
+
b.bm25_rank,
|
|
341
|
+
CASE WHEN b.id IS NOT NULL THEN 'both' ELSE 'dense' END AS retriever
|
|
342
|
+
FROM dense_ranked d
|
|
343
|
+
LEFT JOIN bm25_ranked b ON b.id = d.id
|
|
344
|
+
UNION ALL
|
|
345
|
+
SELECT b.id, b.source, b.source_type, b.file_mtime_at,
|
|
346
|
+
b.heading, b.heading_level, b.content, b.line_start, b.line_end,
|
|
347
|
+
NULL AS dense_rank, NULL AS dense_dist,
|
|
348
|
+
CAST(b.bm25_rank AS INTEGER) AS bm25_rank,
|
|
349
|
+
'bm25' AS retriever
|
|
350
|
+
FROM bm25_ranked b
|
|
351
|
+
LEFT JOIN dense_ranked d ON d.id = b.id
|
|
352
|
+
WHERE d.id IS NULL
|
|
353
|
+
)`;
|
|
354
|
+
}
|
|
355
|
+
else if (hasDense) {
|
|
356
|
+
candidatesCte = `all_candidates AS (
|
|
357
|
+
SELECT id, source, source_type, file_mtime_at,
|
|
358
|
+
heading, heading_level, content, line_start, line_end,
|
|
359
|
+
CAST(dense_rank AS INTEGER) AS dense_rank, dense_dist,
|
|
360
|
+
NULL AS bm25_rank, 'dense' AS retriever
|
|
361
|
+
FROM dense_ranked
|
|
362
|
+
)`;
|
|
363
|
+
}
|
|
364
|
+
else {
|
|
365
|
+
candidatesCte = `all_candidates AS (
|
|
366
|
+
SELECT id, source, source_type, file_mtime_at,
|
|
367
|
+
heading, heading_level, content, line_start, line_end,
|
|
368
|
+
NULL AS dense_rank, NULL AS dense_dist,
|
|
369
|
+
CAST(bm25_rank AS INTEGER) AS bm25_rank, 'bm25' AS retriever
|
|
370
|
+
FROM bm25_ranked
|
|
371
|
+
)`;
|
|
372
|
+
}
|
|
373
|
+
// Scoring: RRF over active channels + recency, dense floor, source dedup
|
|
374
|
+
params.push(rrfK, rrfK, recencyWeight, rrfK, denseScoreFloor, topK);
|
|
375
|
+
const sql = `
|
|
376
|
+
WITH
|
|
377
|
+
${[denseCte, denseRankedCte, bm25Cte, bm25RankedCte, candidatesCte].filter(Boolean).join(',\n')}
|
|
378
|
+
,
|
|
379
|
+
recency_ranked AS (
|
|
380
|
+
SELECT id, ROW_NUMBER() OVER (ORDER BY file_mtime_at DESC, source, id) AS recency_rank
|
|
381
|
+
FROM all_candidates
|
|
382
|
+
),
|
|
383
|
+
scored AS (
|
|
384
|
+
SELECT c.id, c.source, c.source_type, c.file_mtime_at,
|
|
385
|
+
c.heading, c.heading_level, c.content, c.line_start, c.line_end,
|
|
386
|
+
c.dense_rank, c.dense_dist, c.bm25_rank, r.recency_rank,
|
|
387
|
+
c.retriever,
|
|
388
|
+
COALESCE(1.0 / (? + c.dense_rank), 0.0) +
|
|
389
|
+
COALESCE(1.0 / (? + c.bm25_rank), 0.0) +
|
|
390
|
+
? * (1.0 / (? + r.recency_rank)) AS total_score
|
|
391
|
+
FROM all_candidates c
|
|
392
|
+
JOIN recency_ranked r ON r.id = c.id
|
|
393
|
+
WHERE c.retriever != 'dense' OR (1.0 - c.dense_dist) >= ?
|
|
394
|
+
),
|
|
395
|
+
best_per_source AS (
|
|
396
|
+
SELECT *,
|
|
397
|
+
ROW_NUMBER() OVER (PARTITION BY source ORDER BY total_score DESC, source, id) AS source_rank
|
|
398
|
+
FROM scored
|
|
399
|
+
)
|
|
400
|
+
SELECT id, source, source_type, file_mtime_at, heading, heading_level, content, line_start, line_end,
|
|
401
|
+
dense_rank, dense_dist, bm25_rank, recency_rank, total_score, retriever
|
|
402
|
+
FROM best_per_source
|
|
403
|
+
WHERE source_rank = 1
|
|
404
|
+
ORDER BY total_score DESC, source, id
|
|
405
|
+
LIMIT ?
|
|
406
|
+
`;
|
|
407
|
+
const rows = this.db.prepare(sql).all(...params);
|
|
408
|
+
if (rows.length === 0)
|
|
409
|
+
return [];
|
|
410
|
+
const denseActive = hasDense;
|
|
411
|
+
const bm25Active = hasBm25;
|
|
412
|
+
const recencyActive = rows.length > 0;
|
|
413
|
+
const maxScore = ((denseActive ? 1 : 0) + (bm25Active ? 1 : 0) + (recencyActive ? recencyWeight : 0)) / (rrfK + 1);
|
|
414
|
+
return rows.map((r) => {
|
|
415
|
+
const chunk = this.mapMemoryChunkRow(r);
|
|
416
|
+
const normalizedScore = maxScore > 0 ? r.total_score / maxScore : 0;
|
|
417
|
+
const row = {
|
|
418
|
+
chunk,
|
|
419
|
+
score: normalizedScore,
|
|
420
|
+
retriever: r.retriever,
|
|
421
|
+
};
|
|
422
|
+
if (includeDebug) {
|
|
423
|
+
const denseScore = r.dense_dist !== null ? Math.max(0, Math.min(1, 1 - r.dense_dist)) : undefined;
|
|
424
|
+
const bm25Score = r.bm25_rank !== null ? 1 / (rrfK + r.bm25_rank) : undefined;
|
|
425
|
+
const recencyScore = recencyWeight * (1 / (rrfK + r.recency_rank));
|
|
426
|
+
const debug = {
|
|
427
|
+
denseRank: r.dense_rank ?? undefined,
|
|
428
|
+
denseScore,
|
|
429
|
+
bm25Rank: r.bm25_rank ?? undefined,
|
|
430
|
+
bm25Score,
|
|
431
|
+
recencyRank: r.recency_rank,
|
|
432
|
+
recencyScore,
|
|
433
|
+
totalScore: r.total_score,
|
|
434
|
+
};
|
|
435
|
+
row.debug = debug;
|
|
436
|
+
}
|
|
437
|
+
return row;
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
catch (err) {
|
|
441
|
+
console.warn('[memory-store] Hybrid search failed:', err);
|
|
442
|
+
return [];
|
|
443
|
+
}
|
|
310
444
|
}
|
|
311
445
|
get vecAvailable() {
|
|
312
446
|
return true;
|