@hanhnd/agent-kit 1.0.37 → 1.0.39

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/server.js CHANGED
@@ -25930,8 +25930,8 @@ function getElementAtPath(obj, path15) {
25930
25930
  }
25931
25931
  function promiseAllObject(promisesObj) {
25932
25932
  const keys = Object.keys(promisesObj);
25933
- const promises = keys.map((key) => promisesObj[key]);
25934
- return Promise.all(promises).then((results) => {
25933
+ const promises3 = keys.map((key) => promisesObj[key]);
25934
+ return Promise.all(promises3).then((results) => {
25935
25935
  const resolvedObj = {};
25936
25936
  for (let i = 0; i < keys.length; i++) {
25937
25937
  resolvedObj[keys[i]] = results[i];
@@ -35920,6 +35920,9 @@ var DEFAULT_MEMORY_CONFIG = {
35920
35920
  var LOCK_RETRY_MS = 50;
35921
35921
  var LOCK_TIMEOUT_MS = 500;
35922
35922
  var RRF_K = 60;
35923
+ var FETCH_MULTIPLIER = 4;
35924
+ var RECENCY_WEIGHT = 0.5;
35925
+ var DENSE_SCORE_FLOOR = 0.2;
35923
35926
 
35924
35927
  // src/utils/files.ts
35925
35928
  import * as fs from "node:fs";
@@ -37013,14 +37016,14 @@ var MemoryIndexer = class {
37013
37016
  const existingChunkIds = this.store.hashesBySource(source);
37014
37017
  let text;
37015
37018
  try {
37016
- text = fs6.readFileSync(absolutePath, "utf8");
37019
+ text = await fs6.promises.readFile(absolutePath, "utf8");
37017
37020
  } catch (err) {
37018
37021
  console.warn(`[memory-indexer] Cannot read file ${absolutePath}:`, err);
37019
37022
  return { indexed: 0, deleted: 0, skipped: existingChunkIds.size };
37020
37023
  }
37021
37024
  let fileMtimeAt;
37022
37025
  try {
37023
- fileMtimeAt = fs6.statSync(absolutePath).mtimeMs;
37026
+ fileMtimeAt = (await fs6.promises.stat(absolutePath)).mtimeMs;
37024
37027
  } catch (err) {
37025
37028
  console.warn(`[memory-indexer] Cannot stat file ${absolutePath}:`, err);
37026
37029
  fileMtimeAt = Date.now();
@@ -37053,22 +37056,30 @@ var MemoryIndexer = class {
37053
37056
  }
37054
37057
  async indexDirectory(rootDir, opts = {}) {
37055
37058
  const totals = { indexed: 0, deleted: 0, skipped: 0 };
37056
- if (!fs6.existsSync(rootDir)) return totals;
37059
+ let rootExists;
37060
+ try {
37061
+ await fs6.promises.stat(rootDir);
37062
+ rootExists = true;
37063
+ } catch {
37064
+ rootExists = false;
37065
+ }
37066
+ if (!rootExists) return totals;
37057
37067
  const relativeBase = opts.relativeBase ?? this.config.wikiDir;
37058
37068
  const excludeFiles = new Set(opts.excludeFiles ?? []);
37059
37069
  const files = [];
37060
- const walk = (dirPath) => {
37070
+ const walk = async (dirPath) => {
37061
37071
  let entries;
37062
37072
  try {
37063
- entries = fs6.readdirSync(dirPath, { withFileTypes: true });
37073
+ entries = await fs6.promises.readdir(dirPath, { withFileTypes: true });
37064
37074
  } catch (err) {
37065
37075
  console.warn(`[memory-indexer] Cannot scan directory ${dirPath}:`, err);
37066
37076
  return;
37067
37077
  }
37078
+ entries.sort((a, b) => a.name.localeCompare(b.name));
37068
37079
  for (const entry of entries) {
37069
37080
  const entryPath = path8.join(dirPath, entry.name);
37070
37081
  if (entry.isDirectory()) {
37071
- walk(entryPath);
37082
+ await walk(entryPath);
37072
37083
  continue;
37073
37084
  }
37074
37085
  if (entry.isFile() && /\.md$/i.test(entry.name) && !excludeFiles.has(entry.name)) {
@@ -37076,7 +37087,7 @@ var MemoryIndexer = class {
37076
37087
  }
37077
37088
  }
37078
37089
  };
37079
- walk(rootDir);
37090
+ await walk(rootDir);
37080
37091
  const currentSources = new Set(files.map((file) => path8.relative(relativeBase, file)));
37081
37092
  for (const stale of this.store.indexedSources()) {
37082
37093
  if (!currentSources.has(stale)) {
@@ -37093,58 +37104,35 @@ var MemoryIndexer = class {
37093
37104
  return totals;
37094
37105
  }
37095
37106
  async search(query, topK) {
37096
- const fetchLimit = topK * 2;
37097
- let denseResults = [];
37107
+ const fetchLimit = topK * FETCH_MULTIPLIER;
37108
+ let embedding;
37098
37109
  if (this.store.vecAvailable) {
37099
37110
  try {
37100
- const queryEmbedding = await this.embedder.embed([query]);
37101
- denseResults = this.store.searchDense(queryEmbedding[0], fetchLimit);
37111
+ const embeddings = await this.embedder.embed([query]);
37112
+ embedding = embeddings[0];
37102
37113
  } catch (err) {
37103
37114
  console.warn("[memory-indexer] Dense search embedding failed:", err);
37104
37115
  }
37105
37116
  }
37106
- const bm25Results = this.store.searchBm25(query, fetchLimit);
37107
- const bm25Ids = new Set(bm25Results.map((result) => result.id));
37108
- if (bm25Ids.size > 0) {
37109
- denseResults = denseResults.filter((result) => bm25Ids.has(result.id));
37110
- }
37111
- const scoreMap = /* @__PURE__ */ new Map();
37112
- denseResults.forEach((r, rank) => {
37113
- scoreMap.set(r.id, { dense: 1 / (RRF_K + rank + 1), bm25: 0 });
37114
- });
37115
- bm25Results.forEach((r, rank) => {
37116
- const entry = scoreMap.get(r.id) ?? { dense: 0, bm25: 0 };
37117
- entry.bm25 = 1 / (RRF_K + rank + 1);
37118
- scoreMap.set(r.id, entry);
37117
+ const rows = this.store.searchHybrid({
37118
+ query,
37119
+ embedding,
37120
+ topK,
37121
+ fetchLimit,
37122
+ denseScoreFloor: DENSE_SCORE_FLOOR,
37123
+ recencyWeight: RECENCY_WEIGHT,
37124
+ rrfK: RRF_K
37119
37125
  });
37120
- const hasDense = this.store.vecAvailable && denseResults.length > 0;
37121
- const numRetrievers = hasDense ? 2 : 1;
37122
- const maxScore = numRetrievers / (RRF_K + 1);
37123
- const ranked = [...scoreMap.entries()].map(([id, scores]) => ({
37124
- id,
37125
- totalScore: scores.dense + scores.bm25,
37126
- hasDense: scores.dense > 0,
37127
- hasBm25: scores.bm25 > 0
37128
- })).sort((a, b) => b.totalScore - a.totalScore);
37129
- const chunks = this.store.getChunksByIds(ranked.map((r) => r.id));
37130
- const chunkMap = new Map(chunks.map((c) => [c.id, c]));
37126
+ if (rows.length === 0) return [];
37131
37127
  const results = [];
37132
- const seenSources = /* @__PURE__ */ new Set();
37133
- for (const r of ranked) {
37134
- if (results.length >= topK) break;
37135
- const chunk = chunkMap.get(r.id);
37136
- if (!chunk) continue;
37137
- if (seenSources.has(chunk.source)) continue;
37138
- seenSources.add(chunk.source);
37139
- const normalizedScore = maxScore > 0 ? r.totalScore / maxScore : 0;
37140
- const retriever = r.hasDense && r.hasBm25 ? "both" : r.hasDense ? "dense" : "bm25";
37128
+ for (const row of rows) {
37141
37129
  let contentSource = "file";
37142
37130
  try {
37143
- chunk.content = fs6.readFileSync(path8.join(this.config.wikiDir, chunk.source), "utf8");
37131
+ row.chunk.content = await fs6.promises.readFile(path8.join(this.config.wikiDir, row.chunk.source), "utf8");
37144
37132
  } catch {
37145
37133
  contentSource = "fallback";
37146
37134
  }
37147
- results.push({ chunk, score: normalizedScore, retriever, contentSource });
37135
+ results.push({ chunk: row.chunk, score: row.score, retriever: row.retriever, contentSource });
37148
37136
  }
37149
37137
  return results;
37150
37138
  }
@@ -37153,13 +37141,13 @@ var MemoryIndexer = class {
37153
37141
  const rawDir = path8.join(this.config.wikiDir, "raw");
37154
37142
  const savePath = path8.join(rawDir, `conv_save_${datePart}.md`);
37155
37143
  const lockPath = `${savePath}.lock`;
37156
- fs6.mkdirSync(rawDir, { recursive: true });
37144
+ await fs6.promises.mkdir(rawDir, { recursive: true });
37157
37145
  const acquired = await acquireLock(lockPath, LOCK_TIMEOUT_MS, LOCK_RETRY_MS);
37158
37146
  if (!acquired) {
37159
37147
  console.warn("[memory-indexer] Could not acquire write lock, writing anyway (fail-open)");
37160
37148
  }
37161
37149
  try {
37162
- fs6.appendFileSync(savePath, `
37150
+ await fs6.promises.appendFile(savePath, `
37163
37151
  ${content}
37164
37152
  `, "utf8");
37165
37153
  } finally {
@@ -37321,6 +37309,7 @@ var MemoryStore = class {
37321
37309
  return 0;
37322
37310
  }
37323
37311
  upsert(chunks, embeddings) {
37312
+ const deleteChunk = this.db.prepare(`DELETE FROM memory_chunks WHERE id = ?`);
37324
37313
  const insertChunk = this.db.prepare(`
37325
37314
  INSERT OR REPLACE INTO memory_chunks
37326
37315
  (id, source, source_type, heading, heading_level, content, line_start, line_end, indexed_at, file_mtime_at, embedding)
@@ -37331,7 +37320,7 @@ var MemoryStore = class {
37331
37320
  const chunk = chunks[i];
37332
37321
  try {
37333
37322
  const vectorBinding = this.toVectorBinding(embeddings[i]);
37334
- this.db.prepare(`DELETE FROM memory_chunks WHERE id = ?`).run(chunk.id);
37323
+ deleteChunk.run(chunk.id);
37335
37324
  insertChunk.run(
37336
37325
  chunk.id,
37337
37326
  chunk.source,
@@ -37360,15 +37349,10 @@ var MemoryStore = class {
37360
37349
  if (!embedding) throw new Error("missing embedding");
37361
37350
  if (!(embedding instanceof Float32Array)) throw new Error("missing embedding");
37362
37351
  if (embedding.length !== this.config.vectorDimension) throw new Error("embedding dimension mismatch");
37363
- const values = Array.from(embedding, (value) => {
37364
- if (!Number.isFinite(value)) throw new Error("embedding contains non-finite value");
37365
- return value;
37366
- });
37367
- return JSON.stringify(values);
37368
- }
37369
- normalizeVectorDistance(distance) {
37370
- if (distance <= 0) return 1;
37371
- return Math.max(0, Math.min(1, 1 - distance));
37352
+ for (let i = 0; i < embedding.length; i++) {
37353
+ if (!Number.isFinite(embedding[i])) throw new Error("embedding contains non-finite value");
37354
+ }
37355
+ return Buffer.from(embedding.buffer, embedding.byteOffset, embedding.byteLength);
37372
37356
  }
37373
37357
  hashesBySource(source) {
37374
37358
  const rows = this.db.prepare(`SELECT id FROM memory_chunks WHERE source = ?`).all(source);
@@ -37406,61 +37390,12 @@ var MemoryStore = class {
37406
37390
  });
37407
37391
  deleteAll();
37408
37392
  }
37409
- searchDense(embedding, limit) {
37410
- try {
37411
- const queryVector = this.toVectorBinding(embedding);
37412
- const rows = this.db.prepare(
37413
- `SELECT mc.id, vector_distance_cos(mc.embedding, vector32(?)) AS distance
37414
- FROM vector_top_k('idx_memory_chunks_embedding', vector32(?), ?) AS vector_matches
37415
- JOIN memory_chunks mc ON mc.rowid = vector_matches.id
37416
- ORDER BY distance
37417
- LIMIT ?`
37418
- ).all(queryVector, queryVector, limit, limit);
37419
- return rows.map((r) => ({
37420
- id: r.id,
37421
- score: this.normalizeVectorDistance(r.distance)
37422
- }));
37423
- } catch (err) {
37424
- console.warn("[memory-store] Dense search failed:", err);
37425
- return [];
37426
- }
37427
- }
37428
- searchBm25(query, limit) {
37429
- if (!query.trim()) return [];
37393
+ buildFtsQuery(query) {
37430
37394
  const tokens = query.trim().split(/\s+/).map((t) => t.replace(/[^a-zA-Z0-9_]/g, "").toLowerCase()).filter((t) => t.length > 1);
37431
- const ftsQuery = (0, import_stopword.removeStopwords)(tokens, import_stopword.eng).map((t) => `${t.replace(/s$/, "")}*`).join(" OR ");
37432
- if (!ftsQuery) return [];
37433
- try {
37434
- const rows = this.db.prepare(
37435
- `SELECT mc.id, bm25(memory_fts, 0.2, 2.0, 5.0) AS rank
37436
- FROM memory_fts
37437
- JOIN memory_chunks mc ON mc.rowid = memory_fts.rowid
37438
- WHERE memory_fts MATCH ?
37439
- ORDER BY rank
37440
- LIMIT ?`
37441
- ).all(ftsQuery, limit);
37442
- if (rows.length === 0) return [];
37443
- const ranks = rows.map((r) => r.rank);
37444
- const minRank = Math.min(...ranks);
37445
- const maxRank = Math.max(...ranks);
37446
- const range = maxRank - minRank;
37447
- return rows.map((r) => ({
37448
- id: r.id,
37449
- score: range > 0 ? (maxRank - r.rank) / range : 1
37450
- }));
37451
- } catch (err) {
37452
- console.warn("[memory-store] BM25 search failed:", err);
37453
- return [];
37454
- }
37395
+ return (0, import_stopword.removeStopwords)(tokens, import_stopword.eng).map((t) => `${t.replace(/s$/, "")}*`).join(" OR ");
37455
37396
  }
37456
- getChunksByIds(ids) {
37457
- if (ids.length === 0) return [];
37458
- const placeholders = ids.map(() => "?").join(", ");
37459
- const rows = this.db.prepare(
37460
- `SELECT id, source, source_type, heading, heading_level, content, line_start, line_end, file_mtime_at
37461
- FROM memory_chunks WHERE id IN (${placeholders})`
37462
- ).all(...ids);
37463
- return rows.map((r) => ({
37397
+ mapMemoryChunkRow(r) {
37398
+ return {
37464
37399
  id: r.id,
37465
37400
  source: r.source,
37466
37401
  sourceType: r.source_type,
@@ -37470,7 +37405,201 @@ var MemoryStore = class {
37470
37405
  lineStart: r.line_start,
37471
37406
  lineEnd: r.line_end,
37472
37407
  fileMtimeAt: r.file_mtime_at
37473
- }));
37408
+ };
37409
+ }
37410
+ searchHybrid(options) {
37411
+ const {
37412
+ query,
37413
+ embedding,
37414
+ topK,
37415
+ fetchLimit,
37416
+ denseScoreFloor,
37417
+ recencyWeight,
37418
+ rrfK,
37419
+ sourceType,
37420
+ sinceMtimeAt,
37421
+ includeDebug
37422
+ } = options;
37423
+ const ftsQuery = this.buildFtsQuery(query);
37424
+ const hasDense = embedding !== void 0;
37425
+ const hasBm25 = ftsQuery.length > 0;
37426
+ if (!hasDense && !hasBm25) return [];
37427
+ const hasFilters = sourceType !== void 0 || sinceMtimeAt !== void 0;
37428
+ try {
37429
+ const params = [];
37430
+ let denseCte = "";
37431
+ let denseRankedCte = "";
37432
+ if (hasDense) {
37433
+ const queryBuf = this.toVectorBinding(embedding);
37434
+ if (hasFilters) {
37435
+ const filterClauses = [];
37436
+ if (sourceType) {
37437
+ filterClauses.push("mc.source_type = ?");
37438
+ params.push(sourceType);
37439
+ }
37440
+ if (sinceMtimeAt !== void 0) {
37441
+ filterClauses.push("mc.file_mtime_at >= ?");
37442
+ params.push(sinceMtimeAt);
37443
+ }
37444
+ const whereClause = filterClauses.length > 0 ? `WHERE ${filterClauses.join(" AND ")}` : "";
37445
+ params.push(queryBuf);
37446
+ params.push(fetchLimit);
37447
+ denseCte = `dense_data AS (
37448
+ SELECT mc.id, mc.source, mc.source_type, mc.file_mtime_at,
37449
+ mc.heading, mc.heading_level, mc.content, mc.line_start, mc.line_end,
37450
+ vector_distance_cos(mc.embedding, vector32(?)) AS dense_dist
37451
+ FROM memory_chunks mc
37452
+ ${whereClause}
37453
+ ORDER BY dense_dist
37454
+ LIMIT ?
37455
+ )`;
37456
+ } else {
37457
+ params.push(queryBuf);
37458
+ params.push(queryBuf);
37459
+ params.push(fetchLimit);
37460
+ denseCte = `dense_data AS (
37461
+ SELECT mc.id, mc.source, mc.source_type, mc.file_mtime_at,
37462
+ mc.heading, mc.heading_level, mc.content, mc.line_start, mc.line_end,
37463
+ vector_distance_cos(mc.embedding, vector32(?)) AS dense_dist
37464
+ FROM vector_top_k('idx_memory_chunks_embedding', vector32(?), ?) AS vtk
37465
+ JOIN memory_chunks mc ON mc.rowid = vtk.id
37466
+ )`;
37467
+ }
37468
+ denseRankedCte = `dense_ranked AS (
37469
+ SELECT *, ROW_NUMBER() OVER (ORDER BY dense_dist, id) AS dense_rank
37470
+ FROM dense_data
37471
+ )`;
37472
+ }
37473
+ let bm25Cte = "";
37474
+ let bm25RankedCte = "";
37475
+ if (hasBm25) {
37476
+ const bm25FilterClauses = [];
37477
+ if (sourceType) bm25FilterClauses.push("mc.source_type = ?");
37478
+ if (sinceMtimeAt !== void 0) bm25FilterClauses.push("mc.file_mtime_at >= ?");
37479
+ const bm25WhereExtra = bm25FilterClauses.length > 0 ? ` AND ${bm25FilterClauses.join(" AND ")}` : "";
37480
+ params.push(ftsQuery);
37481
+ if (sourceType) params.push(sourceType);
37482
+ if (sinceMtimeAt !== void 0) params.push(sinceMtimeAt);
37483
+ params.push(fetchLimit);
37484
+ bm25Cte = `bm25_data AS (
37485
+ SELECT mc.id, mc.source, mc.source_type, mc.file_mtime_at,
37486
+ mc.heading, mc.heading_level, mc.content, mc.line_start, mc.line_end,
37487
+ bm25(memory_fts, 0.2, 2.0, 5.0) AS bm25_neg
37488
+ FROM memory_fts
37489
+ JOIN memory_chunks mc ON mc.rowid = memory_fts.rowid
37490
+ WHERE memory_fts MATCH ?${bm25WhereExtra}
37491
+ LIMIT ?
37492
+ )`;
37493
+ bm25RankedCte = `bm25_ranked AS (
37494
+ SELECT *, ROW_NUMBER() OVER (ORDER BY bm25_neg, id) AS bm25_rank
37495
+ FROM bm25_data
37496
+ )`;
37497
+ }
37498
+ let candidatesCte = "";
37499
+ if (hasDense && hasBm25) {
37500
+ candidatesCte = `all_candidates AS (
37501
+ SELECT d.id, d.source, d.source_type, d.file_mtime_at,
37502
+ d.heading, d.heading_level, d.content, d.line_start, d.line_end,
37503
+ CAST(d.dense_rank AS INTEGER) AS dense_rank, d.dense_dist,
37504
+ b.bm25_rank,
37505
+ CASE WHEN b.id IS NOT NULL THEN 'both' ELSE 'dense' END AS retriever
37506
+ FROM dense_ranked d
37507
+ LEFT JOIN bm25_ranked b ON b.id = d.id
37508
+ UNION ALL
37509
+ SELECT b.id, b.source, b.source_type, b.file_mtime_at,
37510
+ b.heading, b.heading_level, b.content, b.line_start, b.line_end,
37511
+ NULL AS dense_rank, NULL AS dense_dist,
37512
+ CAST(b.bm25_rank AS INTEGER) AS bm25_rank,
37513
+ 'bm25' AS retriever
37514
+ FROM bm25_ranked b
37515
+ LEFT JOIN dense_ranked d ON d.id = b.id
37516
+ WHERE d.id IS NULL
37517
+ )`;
37518
+ } else if (hasDense) {
37519
+ candidatesCte = `all_candidates AS (
37520
+ SELECT id, source, source_type, file_mtime_at,
37521
+ heading, heading_level, content, line_start, line_end,
37522
+ CAST(dense_rank AS INTEGER) AS dense_rank, dense_dist,
37523
+ NULL AS bm25_rank, 'dense' AS retriever
37524
+ FROM dense_ranked
37525
+ )`;
37526
+ } else {
37527
+ candidatesCte = `all_candidates AS (
37528
+ SELECT id, source, source_type, file_mtime_at,
37529
+ heading, heading_level, content, line_start, line_end,
37530
+ NULL AS dense_rank, NULL AS dense_dist,
37531
+ CAST(bm25_rank AS INTEGER) AS bm25_rank, 'bm25' AS retriever
37532
+ FROM bm25_ranked
37533
+ )`;
37534
+ }
37535
+ params.push(rrfK, rrfK, recencyWeight, rrfK, denseScoreFloor, topK);
37536
+ const sql = `
37537
+ WITH
37538
+ ${[denseCte, denseRankedCte, bm25Cte, bm25RankedCte, candidatesCte].filter(Boolean).join(",\n")}
37539
+ ,
37540
+ recency_ranked AS (
37541
+ SELECT id, ROW_NUMBER() OVER (ORDER BY file_mtime_at DESC, source, id) AS recency_rank
37542
+ FROM all_candidates
37543
+ ),
37544
+ scored AS (
37545
+ SELECT c.id, c.source, c.source_type, c.file_mtime_at,
37546
+ c.heading, c.heading_level, c.content, c.line_start, c.line_end,
37547
+ c.dense_rank, c.dense_dist, c.bm25_rank, r.recency_rank,
37548
+ c.retriever,
37549
+ COALESCE(1.0 / (? + c.dense_rank), 0.0) +
37550
+ COALESCE(1.0 / (? + c.bm25_rank), 0.0) +
37551
+ ? * (1.0 / (? + r.recency_rank)) AS total_score
37552
+ FROM all_candidates c
37553
+ JOIN recency_ranked r ON r.id = c.id
37554
+ WHERE c.retriever != 'dense' OR (1.0 - c.dense_dist) >= ?
37555
+ ),
37556
+ best_per_source AS (
37557
+ SELECT *,
37558
+ ROW_NUMBER() OVER (PARTITION BY source ORDER BY total_score DESC, source, id) AS source_rank
37559
+ FROM scored
37560
+ )
37561
+ SELECT id, source, source_type, file_mtime_at, heading, heading_level, content, line_start, line_end,
37562
+ dense_rank, dense_dist, bm25_rank, recency_rank, total_score, retriever
37563
+ FROM best_per_source
37564
+ WHERE source_rank = 1
37565
+ ORDER BY total_score DESC, source, id
37566
+ LIMIT ?
37567
+ `;
37568
+ const rows = this.db.prepare(sql).all(...params);
37569
+ if (rows.length === 0) return [];
37570
+ const denseActive = hasDense;
37571
+ const bm25Active = hasBm25;
37572
+ const recencyActive = rows.length > 0;
37573
+ const maxScore = ((denseActive ? 1 : 0) + (bm25Active ? 1 : 0) + (recencyActive ? recencyWeight : 0)) / (rrfK + 1);
37574
+ return rows.map((r) => {
37575
+ const chunk = this.mapMemoryChunkRow(r);
37576
+ const normalizedScore = maxScore > 0 ? r.total_score / maxScore : 0;
37577
+ const row = {
37578
+ chunk,
37579
+ score: normalizedScore,
37580
+ retriever: r.retriever
37581
+ };
37582
+ if (includeDebug) {
37583
+ const denseScore = r.dense_dist !== null ? Math.max(0, Math.min(1, 1 - r.dense_dist)) : void 0;
37584
+ const bm25Score = r.bm25_rank !== null ? 1 / (rrfK + r.bm25_rank) : void 0;
37585
+ const recencyScore = recencyWeight * (1 / (rrfK + r.recency_rank));
37586
+ const debug = {
37587
+ denseRank: r.dense_rank ?? void 0,
37588
+ denseScore,
37589
+ bm25Rank: r.bm25_rank ?? void 0,
37590
+ bm25Score,
37591
+ recencyRank: r.recency_rank,
37592
+ recencyScore,
37593
+ totalScore: r.total_score
37594
+ };
37595
+ row.debug = debug;
37596
+ }
37597
+ return row;
37598
+ });
37599
+ } catch (err) {
37600
+ console.warn("[memory-store] Hybrid search failed:", err);
37601
+ return [];
37602
+ }
37474
37603
  }
37475
37604
  get vecAvailable() {
37476
37605
  return true;
@@ -37955,7 +38084,7 @@ ${content}`;
37955
38084
  const blocks = [];
37956
38085
  for (const row of rows) {
37957
38086
  try {
37958
- const content = fs11.readFileSync(path14.join(config2.wikiDir, row.source), "utf8");
38087
+ const content = await fs11.promises.readFile(path14.join(config2.wikiDir, row.source), "utf8");
37959
38088
  const displaySource = row.source.replace(/^compiled\//, "");
37960
38089
  blocks.push(`### ${displaySource}
37961
38090
  ${content}`);
@@ -72,10 +72,8 @@ describe('digestConversationFile', () => {
72
72
  });
73
73
  assert.equal(result.indexed, true);
74
74
  assert.equal(result.markdown, markdownPath);
75
- const denseResults = store.searchDense(new Float32Array(384).fill(0.05), 5);
76
75
  const indexedSource = path.relative(config.wikiDir, markdownPath);
77
- const indexedChunks = store.getChunksByIds(denseResults.map((denseResult) => denseResult.id));
78
- assert.ok(indexedChunks.some((chunk) => chunk.source === indexedSource), `Expected dense search to return a chunk for ${indexedSource}`);
76
+ assert.ok(store.hashesBySource(indexedSource).size > 0, `Expected source ${indexedSource} to be indexed in store`);
79
77
  }
80
78
  finally {
81
79
  store.close();
@@ -4,3 +4,6 @@ export declare const DEFAULT_MEMORY_CONFIG: MemoryConfig;
4
4
  export declare const LOCK_RETRY_MS = 50;
5
5
  export declare const LOCK_TIMEOUT_MS = 500;
6
6
  export declare const RRF_K = 60;
7
+ export declare const FETCH_MULTIPLIER = 4;
8
+ export declare const RECENCY_WEIGHT = 0.5;
9
+ export declare const DENSE_SCORE_FLOOR = 0.2;
@@ -16,3 +16,9 @@ export const DEFAULT_MEMORY_CONFIG = {
16
16
  export const LOCK_RETRY_MS = 50;
17
17
  export const LOCK_TIMEOUT_MS = 500;
18
18
  export const RRF_K = 60;
19
+ // Tunable starting default: fetchLimit = topK * FETCH_MULTIPLIER (was hardcoded topK * 2)
20
+ export const FETCH_MULTIPLIER = 4;
21
+ // Tunable starting default: weight on the recency RRF channel; < 1 so recency cannot outrank a dual-channel durable hit
22
+ export const RECENCY_WEIGHT = 0.5;
23
+ // Tunable starting default: min dense retrieval score [0,1] for a dense-only candidate to survive (permissive start)
24
+ export const DENSE_SCORE_FLOOR = 0.2;