@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.
@@ -1,7 +1,7 @@
1
1
  import * as fs from 'fs';
2
2
  import * as path from 'path';
3
3
  import { chunkMarkdown } from './chunker.js';
4
- import { LOCK_RETRY_MS, LOCK_TIMEOUT_MS, RRF_K } from './constants.js';
4
+ import { DENSE_SCORE_FLOOR, FETCH_MULTIPLIER, LOCK_RETRY_MS, LOCK_TIMEOUT_MS, RECENCY_WEIGHT, RRF_K, } from './constants.js';
5
5
  import { acquireLock, releaseLock } from '../../utils/files.js';
6
6
  export function deriveSourceType(source) {
7
7
  if (source.startsWith('compiled/provisional/conversation-digests/'))
@@ -32,7 +32,7 @@ export class MemoryIndexer {
32
32
  const existingChunkIds = this.store.hashesBySource(source);
33
33
  let text;
34
34
  try {
35
- text = fs.readFileSync(absolutePath, 'utf8');
35
+ text = await fs.promises.readFile(absolutePath, 'utf8');
36
36
  }
37
37
  catch (err) {
38
38
  console.warn(`[memory-indexer] Cannot read file ${absolutePath}:`, err);
@@ -40,7 +40,7 @@ export class MemoryIndexer {
40
40
  }
41
41
  let fileMtimeAt;
42
42
  try {
43
- fileMtimeAt = fs.statSync(absolutePath).mtimeMs;
43
+ fileMtimeAt = (await fs.promises.stat(absolutePath)).mtimeMs;
44
44
  }
45
45
  catch (err) {
46
46
  console.warn(`[memory-indexer] Cannot stat file ${absolutePath}:`, err);
@@ -75,24 +75,34 @@ export class MemoryIndexer {
75
75
  }
76
76
  async indexDirectory(rootDir, opts = {}) {
77
77
  const totals = { indexed: 0, deleted: 0, skipped: 0 };
78
- if (!fs.existsSync(rootDir))
78
+ let rootExists;
79
+ try {
80
+ await fs.promises.stat(rootDir);
81
+ rootExists = true;
82
+ }
83
+ catch {
84
+ rootExists = false;
85
+ }
86
+ if (!rootExists)
79
87
  return totals;
80
88
  const relativeBase = opts.relativeBase ?? this.config.wikiDir;
81
89
  const excludeFiles = new Set(opts.excludeFiles ?? []);
82
90
  const files = [];
83
- const walk = (dirPath) => {
91
+ const walk = async (dirPath) => {
84
92
  let entries;
85
93
  try {
86
- entries = fs.readdirSync(dirPath, { withFileTypes: true });
94
+ entries = await fs.promises.readdir(dirPath, { withFileTypes: true });
87
95
  }
88
96
  catch (err) {
89
97
  console.warn(`[memory-indexer] Cannot scan directory ${dirPath}:`, err);
90
98
  return;
91
99
  }
100
+ // Sort for deterministic traversal order
101
+ entries.sort((a, b) => a.name.localeCompare(b.name));
92
102
  for (const entry of entries) {
93
103
  const entryPath = path.join(dirPath, entry.name);
94
104
  if (entry.isDirectory()) {
95
- walk(entryPath);
105
+ await walk(entryPath);
96
106
  continue;
97
107
  }
98
108
  if (entry.isFile() && /\.md$/i.test(entry.name) && !excludeFiles.has(entry.name)) {
@@ -100,7 +110,7 @@ export class MemoryIndexer {
100
110
  }
101
111
  }
102
112
  };
103
- walk(rootDir);
113
+ await walk(rootDir);
104
114
  const currentSources = new Set(files.map((file) => path.relative(relativeBase, file)));
105
115
  // Remove stale sources (deleted files or pre-migration daily-file sources)
106
116
  for (const stale of this.store.indexedSources()) {
@@ -118,65 +128,38 @@ export class MemoryIndexer {
118
128
  return totals;
119
129
  }
120
130
  async search(query, topK) {
121
- const fetchLimit = topK * 2;
122
- let denseResults = [];
131
+ const fetchLimit = topK * FETCH_MULTIPLIER;
132
+ let embedding;
123
133
  if (this.store.vecAvailable) {
124
134
  try {
125
- const queryEmbedding = await this.embedder.embed([query]);
126
- denseResults = this.store.searchDense(queryEmbedding[0], fetchLimit);
135
+ const embeddings = await this.embedder.embed([query]);
136
+ embedding = embeddings[0];
127
137
  }
128
138
  catch (err) {
129
139
  console.warn('[memory-indexer] Dense search embedding failed:', err);
130
140
  }
131
141
  }
132
- const bm25Results = this.store.searchBm25(query, fetchLimit);
133
- const bm25Ids = new Set(bm25Results.map((result) => result.id));
134
- if (bm25Ids.size > 0) {
135
- denseResults = denseResults.filter((result) => bm25Ids.has(result.id));
136
- }
137
- const scoreMap = new Map();
138
- denseResults.forEach((r, rank) => {
139
- scoreMap.set(r.id, { dense: 1 / (RRF_K + rank + 1), bm25: 0 });
140
- });
141
- bm25Results.forEach((r, rank) => {
142
- const entry = scoreMap.get(r.id) ?? { dense: 0, bm25: 0 };
143
- entry.bm25 = 1 / (RRF_K + rank + 1);
144
- scoreMap.set(r.id, entry);
142
+ const rows = this.store.searchHybrid({
143
+ query,
144
+ embedding,
145
+ topK,
146
+ fetchLimit,
147
+ denseScoreFloor: DENSE_SCORE_FLOOR,
148
+ recencyWeight: RECENCY_WEIGHT,
149
+ rrfK: RRF_K,
145
150
  });
146
- const hasDense = this.store.vecAvailable && denseResults.length > 0;
147
- const numRetrievers = hasDense ? 2 : 1;
148
- const maxScore = numRetrievers / (RRF_K + 1);
149
- const ranked = [...scoreMap.entries()]
150
- .map(([id, scores]) => ({
151
- id,
152
- totalScore: scores.dense + scores.bm25,
153
- hasDense: scores.dense > 0,
154
- hasBm25: scores.bm25 > 0,
155
- }))
156
- .sort((a, b) => b.totalScore - a.totalScore);
157
- const chunks = this.store.getChunksByIds(ranked.map((r) => r.id));
158
- const chunkMap = new Map(chunks.map((c) => [c.id, c]));
151
+ if (rows.length === 0)
152
+ return [];
159
153
  const results = [];
160
- const seenSources = new Set();
161
- for (const r of ranked) {
162
- if (results.length >= topK)
163
- break;
164
- const chunk = chunkMap.get(r.id);
165
- if (!chunk)
166
- continue;
167
- if (seenSources.has(chunk.source))
168
- continue;
169
- seenSources.add(chunk.source);
170
- const normalizedScore = maxScore > 0 ? r.totalScore / maxScore : 0;
171
- const retriever = r.hasDense && r.hasBm25 ? 'both' : r.hasDense ? 'dense' : 'bm25';
154
+ for (const row of rows) {
172
155
  let contentSource = 'file';
173
156
  try {
174
- chunk.content = fs.readFileSync(path.join(this.config.wikiDir, chunk.source), 'utf8');
157
+ row.chunk.content = await fs.promises.readFile(path.join(this.config.wikiDir, row.chunk.source), 'utf8');
175
158
  }
176
159
  catch {
177
160
  contentSource = 'fallback';
178
161
  }
179
- results.push({ chunk, score: normalizedScore, retriever, contentSource });
162
+ results.push({ chunk: row.chunk, score: row.score, retriever: row.retriever, contentSource });
180
163
  }
181
164
  return results;
182
165
  }
@@ -185,13 +168,13 @@ export class MemoryIndexer {
185
168
  const rawDir = path.join(this.config.wikiDir, 'raw');
186
169
  const savePath = path.join(rawDir, `conv_save_${datePart}.md`);
187
170
  const lockPath = `${savePath}.lock`;
188
- fs.mkdirSync(rawDir, { recursive: true });
171
+ await fs.promises.mkdir(rawDir, { recursive: true });
189
172
  const acquired = await acquireLock(lockPath, LOCK_TIMEOUT_MS, LOCK_RETRY_MS);
190
173
  if (!acquired) {
191
174
  console.warn('[memory-indexer] Could not acquire write lock, writing anyway (fail-open)');
192
175
  }
193
176
  try {
194
- fs.appendFileSync(savePath, `\n${content}\n`, 'utf8');
177
+ await fs.promises.appendFile(savePath, `\n${content}\n`, 'utf8');
195
178
  }
196
179
  finally {
197
180
  if (acquired)