@xfabric/memory 0.1.0

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.
Files changed (121) hide show
  1. package/dist/chunking/index.d.ts +3 -0
  2. package/dist/chunking/index.d.ts.map +1 -0
  3. package/dist/chunking/index.js +3 -0
  4. package/dist/chunking/index.js.map +1 -0
  5. package/dist/chunking/markdown.d.ts +13 -0
  6. package/dist/chunking/markdown.d.ts.map +1 -0
  7. package/dist/chunking/markdown.js +106 -0
  8. package/dist/chunking/markdown.js.map +1 -0
  9. package/dist/chunking/session.d.ts +24 -0
  10. package/dist/chunking/session.d.ts.map +1 -0
  11. package/dist/chunking/session.js +173 -0
  12. package/dist/chunking/session.js.map +1 -0
  13. package/dist/index.d.ts +18 -0
  14. package/dist/index.d.ts.map +1 -0
  15. package/dist/index.js +24 -0
  16. package/dist/index.js.map +1 -0
  17. package/dist/memory-manager.d.ts +189 -0
  18. package/dist/memory-manager.d.ts.map +1 -0
  19. package/dist/memory-manager.js +1055 -0
  20. package/dist/memory-manager.js.map +1 -0
  21. package/dist/providers/gemini.d.ts +6 -0
  22. package/dist/providers/gemini.d.ts.map +1 -0
  23. package/dist/providers/gemini.js +73 -0
  24. package/dist/providers/gemini.js.map +1 -0
  25. package/dist/providers/index.d.ts +20 -0
  26. package/dist/providers/index.d.ts.map +1 -0
  27. package/dist/providers/index.js +102 -0
  28. package/dist/providers/index.js.map +1 -0
  29. package/dist/providers/local.d.ts +14 -0
  30. package/dist/providers/local.d.ts.map +1 -0
  31. package/dist/providers/local.js +73 -0
  32. package/dist/providers/local.js.map +1 -0
  33. package/dist/providers/openai.d.ts +6 -0
  34. package/dist/providers/openai.d.ts.map +1 -0
  35. package/dist/providers/openai.js +48 -0
  36. package/dist/providers/openai.js.map +1 -0
  37. package/dist/providers/types.d.ts +62 -0
  38. package/dist/providers/types.d.ts.map +1 -0
  39. package/dist/providers/types.js +2 -0
  40. package/dist/providers/types.js.map +1 -0
  41. package/dist/search/fts.d.ts +11 -0
  42. package/dist/search/fts.d.ts.map +1 -0
  43. package/dist/search/fts.js +50 -0
  44. package/dist/search/fts.js.map +1 -0
  45. package/dist/search/hybrid.d.ts +16 -0
  46. package/dist/search/hybrid.d.ts.map +1 -0
  47. package/dist/search/hybrid.js +83 -0
  48. package/dist/search/hybrid.js.map +1 -0
  49. package/dist/search/index.d.ts +4 -0
  50. package/dist/search/index.d.ts.map +1 -0
  51. package/dist/search/index.js +4 -0
  52. package/dist/search/index.js.map +1 -0
  53. package/dist/search/vector.d.ts +25 -0
  54. package/dist/search/vector.d.ts.map +1 -0
  55. package/dist/search/vector.js +152 -0
  56. package/dist/search/vector.js.map +1 -0
  57. package/dist/storage/index.d.ts +4 -0
  58. package/dist/storage/index.d.ts.map +1 -0
  59. package/dist/storage/index.js +4 -0
  60. package/dist/storage/index.js.map +1 -0
  61. package/dist/storage/schema.d.ts +24 -0
  62. package/dist/storage/schema.d.ts.map +1 -0
  63. package/dist/storage/schema.js +175 -0
  64. package/dist/storage/schema.js.map +1 -0
  65. package/dist/storage/sqlite-vec.d.ts +22 -0
  66. package/dist/storage/sqlite-vec.d.ts.map +1 -0
  67. package/dist/storage/sqlite-vec.js +85 -0
  68. package/dist/storage/sqlite-vec.js.map +1 -0
  69. package/dist/storage/sqlite.d.ts +206 -0
  70. package/dist/storage/sqlite.d.ts.map +1 -0
  71. package/dist/storage/sqlite.js +352 -0
  72. package/dist/storage/sqlite.js.map +1 -0
  73. package/dist/sync/index.d.ts +4 -0
  74. package/dist/sync/index.d.ts.map +1 -0
  75. package/dist/sync/index.js +4 -0
  76. package/dist/sync/index.js.map +1 -0
  77. package/dist/sync/minimatch.d.ts +6 -0
  78. package/dist/sync/minimatch.d.ts.map +1 -0
  79. package/dist/sync/minimatch.js +60 -0
  80. package/dist/sync/minimatch.js.map +1 -0
  81. package/dist/sync/session-monitor.d.ts +50 -0
  82. package/dist/sync/session-monitor.d.ts.map +1 -0
  83. package/dist/sync/session-monitor.js +126 -0
  84. package/dist/sync/session-monitor.js.map +1 -0
  85. package/dist/sync/watcher.d.ts +44 -0
  86. package/dist/sync/watcher.d.ts.map +1 -0
  87. package/dist/sync/watcher.js +110 -0
  88. package/dist/sync/watcher.js.map +1 -0
  89. package/dist/tools/index.d.ts +3 -0
  90. package/dist/tools/index.d.ts.map +1 -0
  91. package/dist/tools/index.js +3 -0
  92. package/dist/tools/index.js.map +1 -0
  93. package/dist/tools/memory-get.d.ts +32 -0
  94. package/dist/tools/memory-get.d.ts.map +1 -0
  95. package/dist/tools/memory-get.js +53 -0
  96. package/dist/tools/memory-get.js.map +1 -0
  97. package/dist/tools/memory-search.d.ts +32 -0
  98. package/dist/tools/memory-search.d.ts.map +1 -0
  99. package/dist/tools/memory-search.js +56 -0
  100. package/dist/tools/memory-search.js.map +1 -0
  101. package/dist/types.d.ts +350 -0
  102. package/dist/types.d.ts.map +1 -0
  103. package/dist/types.js +15 -0
  104. package/dist/types.js.map +1 -0
  105. package/dist/utils/concurrency.d.ts +25 -0
  106. package/dist/utils/concurrency.d.ts.map +1 -0
  107. package/dist/utils/concurrency.js +59 -0
  108. package/dist/utils/concurrency.js.map +1 -0
  109. package/dist/utils/hash.d.ts +9 -0
  110. package/dist/utils/hash.d.ts.map +1 -0
  111. package/dist/utils/hash.js +16 -0
  112. package/dist/utils/hash.js.map +1 -0
  113. package/dist/utils/index.d.ts +4 -0
  114. package/dist/utils/index.d.ts.map +1 -0
  115. package/dist/utils/index.js +4 -0
  116. package/dist/utils/index.js.map +1 -0
  117. package/dist/utils/retry.d.ts +22 -0
  118. package/dist/utils/retry.d.ts.map +1 -0
  119. package/dist/utils/retry.js +48 -0
  120. package/dist/utils/retry.js.map +1 -0
  121. package/package.json +67 -0
@@ -0,0 +1,1055 @@
1
+ import { join, relative, resolve, dirname, basename } from "node:path";
2
+ import { readFile, stat, readdir, copyFile, rename, unlink } from "node:fs/promises";
3
+ import { existsSync, mkdirSync } from "node:fs";
4
+ import { hashText } from "./utils/hash.js";
5
+ import { retry } from "./utils/retry.js";
6
+ import { Semaphore } from "./utils/concurrency.js";
7
+ import { createEmbeddingProvider, } from "./providers/index.js";
8
+ import { MemoryStorage } from "./storage/sqlite.js";
9
+ import { loadSqliteVec } from "./storage/sqlite-vec.js";
10
+ import { chunkMarkdown } from "./chunking/markdown.js";
11
+ import { chunkSession } from "./chunking/session.js";
12
+ import { FileWatcher } from "./sync/watcher.js";
13
+ import { searchVector, searchVectorWithSqliteVec } from "./search/vector.js";
14
+ import { searchFts } from "./search/fts.js";
15
+ import { mergeHybridResults } from "./search/hybrid.js";
16
+ import { computeProviderKey } from "./providers/index.js";
17
+ // Cache of MemoryManager instances
18
+ const INDEX_CACHE = new Map();
19
+ // Default batch configuration
20
+ const DEFAULT_BATCH_CONFIG = {
21
+ enabled: true,
22
+ maxBatchSize: 100,
23
+ concurrency: 5,
24
+ timeoutMs: 60000,
25
+ maxConsecutiveFailures: 3,
26
+ };
27
+ /**
28
+ * Memory manager for indexing and searching workspace content
29
+ */
30
+ export class MemoryManager {
31
+ config;
32
+ storage;
33
+ providerResult = null;
34
+ watcher = null;
35
+ syncPromise = null;
36
+ embeddingSemaphore;
37
+ closed = false;
38
+ // Dirty flags for triggering sync
39
+ memoryDirty = false;
40
+ sessionsDirty = false;
41
+ memoryDirtyFiles = new Set();
42
+ sessionsDirtyFiles = new Set();
43
+ // Provider fallback tracking
44
+ consecutiveEmbeddingFailures = 0;
45
+ fallbackActivated = false;
46
+ fallbackProvider = null;
47
+ // Batch processing state
48
+ batchDisabled = false;
49
+ batchDisableReason;
50
+ // sqlite-vec availability
51
+ sqliteVecLoaded = false;
52
+ sqliteVecError;
53
+ // Interval sync timer
54
+ syncIntervalTimer = null;
55
+ // Last sync timestamp
56
+ lastSyncAt;
57
+ constructor(config, storage) {
58
+ this.config = config;
59
+ this.storage = storage;
60
+ this.embeddingSemaphore = new Semaphore(config.batch.concurrency);
61
+ }
62
+ /**
63
+ * Create or get a cached MemoryManager instance
64
+ */
65
+ static async create(config) {
66
+ const extConfig = config;
67
+ const normalizedConfig = {
68
+ ...config,
69
+ workspaceDir: resolve(config.workspaceDir),
70
+ provider: config.provider ?? "openai",
71
+ chunking: {
72
+ tokens: config.chunking?.tokens ?? 400,
73
+ overlap: config.chunking?.overlap ?? 80,
74
+ },
75
+ query: {
76
+ maxResults: config.query?.maxResults ?? 10,
77
+ minScore: config.query?.minScore ?? 0.35,
78
+ hybrid: {
79
+ vectorWeight: config.query?.hybrid?.vectorWeight ?? 0.7,
80
+ textWeight: config.query?.hybrid?.textWeight ?? 0.3,
81
+ },
82
+ },
83
+ sync: {
84
+ watchDebounceMs: config.sync?.watchDebounceMs ?? 1500,
85
+ enabled: config.sync?.enabled ?? true,
86
+ intervalMs: extConfig.sync?.intervalMs ?? 0,
87
+ syncSessions: extConfig.sync?.syncSessions ?? false,
88
+ },
89
+ batch: {
90
+ ...DEFAULT_BATCH_CONFIG,
91
+ ...extConfig.batch,
92
+ },
93
+ sessionsDir: extConfig.sessionsDir,
94
+ extraPaths: extConfig.extraPaths,
95
+ onProgress: extConfig.onProgress,
96
+ };
97
+ // Generate cache key
98
+ const cacheKey = `${normalizedConfig.agentId}:${normalizedConfig.workspaceDir}`;
99
+ // Check cache
100
+ const cached = INDEX_CACHE.get(cacheKey);
101
+ if (cached && !cached.closed) {
102
+ return cached;
103
+ }
104
+ // Create storage
105
+ const dbPath = normalizedConfig.store?.path ??
106
+ join(normalizedConfig.workspaceDir, ".memory", "index.sqlite");
107
+ const storage = new MemoryStorage({ path: dbPath });
108
+ const manager = new MemoryManager(normalizedConfig, storage);
109
+ // Try to load sqlite-vec
110
+ manager.tryLoadSqliteVec();
111
+ // Initialize embedding provider
112
+ await manager.initProvider();
113
+ // Initial sync
114
+ await manager.sync();
115
+ // Start file watcher if enabled
116
+ if (normalizedConfig.sync.enabled) {
117
+ await manager.startWatching();
118
+ }
119
+ // Start interval sync if configured
120
+ if (normalizedConfig.sync.intervalMs > 0) {
121
+ manager.startIntervalSync();
122
+ }
123
+ // Cache instance
124
+ INDEX_CACHE.set(cacheKey, manager);
125
+ return manager;
126
+ }
127
+ /**
128
+ * Try to load sqlite-vec extension
129
+ */
130
+ tryLoadSqliteVec() {
131
+ try {
132
+ this.sqliteVecLoaded = loadSqliteVec(this.storage.database);
133
+ }
134
+ catch (err) {
135
+ this.sqliteVecError = err instanceof Error ? err.message : String(err);
136
+ this.sqliteVecLoaded = false;
137
+ }
138
+ }
139
+ /**
140
+ * Initialize the embedding provider
141
+ */
142
+ async initProvider() {
143
+ const apiKey = this.config.remote?.apiKey ?? process.env.OPENAI_API_KEY;
144
+ this.providerResult = await createEmbeddingProvider(this.config.provider, {
145
+ apiKey,
146
+ baseUrl: this.config.remote?.baseUrl,
147
+ model: this.config.remote?.model,
148
+ modelPath: this.config.local?.modelPath,
149
+ }, {
150
+ geminiApiKey: process.env.GEMINI_API_KEY,
151
+ });
152
+ }
153
+ /**
154
+ * Get the embedding provider (with fallback support)
155
+ */
156
+ get provider() {
157
+ if (this.fallbackActivated && this.fallbackProvider) {
158
+ return this.fallbackProvider;
159
+ }
160
+ if (!this.providerResult) {
161
+ throw new Error("Provider not initialized");
162
+ }
163
+ return this.providerResult.provider;
164
+ }
165
+ /**
166
+ * Get provider key for cache isolation
167
+ */
168
+ get providerKey() {
169
+ return computeProviderKey(this.provider.id, this.config.remote?.baseUrl, this.config.remote?.model);
170
+ }
171
+ /**
172
+ * Search memory for relevant content
173
+ */
174
+ async search(query, options = {}) {
175
+ const maxResults = options.maxResults ?? this.config.query.maxResults;
176
+ const minScore = options.minScore ?? this.config.query.minScore;
177
+ // Get query embedding
178
+ const queryEmbedding = await this.embedWithFallback(query);
179
+ // Vector search - use sqlite-vec if available
180
+ const vectorResults = this.sqliteVecLoaded
181
+ ? searchVectorWithSqliteVec(this.storage.database, queryEmbedding, {
182
+ maxResults: maxResults * 2,
183
+ minScore,
184
+ source: options.source,
185
+ })
186
+ : searchVector(this.storage.database, queryEmbedding, {
187
+ maxResults: maxResults * 2,
188
+ minScore,
189
+ source: options.source,
190
+ });
191
+ // FTS search
192
+ const keywordResults = searchFts(this.storage.database, query, {
193
+ maxResults: maxResults * 2,
194
+ source: options.source,
195
+ });
196
+ // Merge results
197
+ const merged = mergeHybridResults(vectorResults, keywordResults, this.config.query.hybrid);
198
+ // Filter by minimum score and limit
199
+ return merged.filter((r) => r.score >= minScore).slice(0, maxResults);
200
+ }
201
+ /**
202
+ * Embed text with automatic fallback on provider failures
203
+ */
204
+ async embedWithFallback(text) {
205
+ try {
206
+ const result = await this.provider.embedQuery(text);
207
+ // Reset failure counter on success
208
+ this.consecutiveEmbeddingFailures = 0;
209
+ return result;
210
+ }
211
+ catch (err) {
212
+ this.consecutiveEmbeddingFailures++;
213
+ // Attempt fallback if we've hit threshold and haven't already activated
214
+ if (this.consecutiveEmbeddingFailures >= this.config.batch.maxConsecutiveFailures &&
215
+ !this.fallbackActivated) {
216
+ await this.activateFallbackProvider(err instanceof Error ? err.message : String(err));
217
+ // Retry with fallback
218
+ if (this.fallbackProvider) {
219
+ return this.fallbackProvider.embedQuery(text);
220
+ }
221
+ }
222
+ throw err;
223
+ }
224
+ }
225
+ /**
226
+ * Activate fallback provider
227
+ */
228
+ async activateFallbackProvider(reason) {
229
+ if (this.fallbackActivated)
230
+ return;
231
+ // Determine fallback based on current provider
232
+ const currentId = this.provider.id;
233
+ let fallbackType = null;
234
+ if (currentId === "openai" && process.env.GEMINI_API_KEY) {
235
+ fallbackType = "gemini";
236
+ }
237
+ else if (currentId === "gemini" && (this.config.remote?.apiKey ?? process.env.OPENAI_API_KEY)) {
238
+ fallbackType = "openai";
239
+ }
240
+ if (!fallbackType)
241
+ return;
242
+ try {
243
+ const result = await createEmbeddingProvider(fallbackType, {
244
+ apiKey: fallbackType === "gemini" ? process.env.GEMINI_API_KEY : this.config.remote?.apiKey,
245
+ baseUrl: this.config.remote?.baseUrl,
246
+ }, {});
247
+ this.fallbackProvider = result.provider;
248
+ this.fallbackActivated = true;
249
+ this.providerResult.fallbackFrom = currentId;
250
+ this.providerResult.fallbackReason = reason;
251
+ }
252
+ catch {
253
+ // Fallback failed, continue with original provider errors
254
+ }
255
+ }
256
+ /**
257
+ * Read file content with optional line range
258
+ */
259
+ async readFile(params) {
260
+ const absPath = join(this.config.workspaceDir, params.relPath);
261
+ if (!existsSync(absPath)) {
262
+ throw new Error(`File not found: ${params.relPath}`);
263
+ }
264
+ const content = await readFile(absPath, "utf-8");
265
+ const allLines = content.split("\n");
266
+ const startLine = Math.max(1, params.from ?? 1);
267
+ const endLine = params.lines
268
+ ? Math.min(startLine + params.lines - 1, allLines.length)
269
+ : allLines.length;
270
+ const selectedLines = allLines.slice(startLine - 1, endLine);
271
+ return {
272
+ content: selectedLines.join("\n"),
273
+ startLine,
274
+ endLine,
275
+ };
276
+ }
277
+ /**
278
+ * Synchronize memory index with filesystem
279
+ */
280
+ async sync() {
281
+ // Deduplicate concurrent sync calls
282
+ if (this.syncPromise) {
283
+ return this.syncPromise;
284
+ }
285
+ this.syncPromise = this.performSync();
286
+ try {
287
+ await this.syncPromise;
288
+ }
289
+ finally {
290
+ this.syncPromise = null;
291
+ }
292
+ }
293
+ /**
294
+ * Perform atomic reindex into a temporary database
295
+ * On success, swap temp DB with main DB
296
+ * On failure, restore original
297
+ */
298
+ async reindex() {
299
+ const mainDbPath = this.storage.path;
300
+ if (mainDbPath === ":memory:") {
301
+ // For in-memory DB, just do a regular sync
302
+ await this.performFullSync();
303
+ return;
304
+ }
305
+ const tempDbPath = `${mainDbPath}.tmp`;
306
+ const backupDbPath = `${mainDbPath}.backup`;
307
+ // Ensure temp directory exists
308
+ mkdirSync(dirname(tempDbPath), { recursive: true });
309
+ try {
310
+ // Create temp storage
311
+ const tempStorage = new MemoryStorage({ path: tempDbPath });
312
+ // Try to load sqlite-vec in temp DB
313
+ try {
314
+ loadSqliteVec(tempStorage.database);
315
+ }
316
+ catch {
317
+ // Ignore - sqlite-vec optional
318
+ }
319
+ // Index all files into temp DB
320
+ await this.indexAllFilesInto(tempStorage);
321
+ // Close temp storage
322
+ tempStorage.close();
323
+ // Backup current DB
324
+ await copyFile(mainDbPath, backupDbPath);
325
+ // Close main storage
326
+ this.storage.close();
327
+ // Swap temp to main
328
+ await rename(tempDbPath, mainDbPath);
329
+ // Reopen main storage
330
+ this.storage = new MemoryStorage({ path: mainDbPath });
331
+ this.tryLoadSqliteVec();
332
+ // Clean up backup
333
+ try {
334
+ await unlink(backupDbPath);
335
+ }
336
+ catch {
337
+ // Ignore cleanup errors
338
+ }
339
+ this.lastSyncAt = Date.now();
340
+ this.memoryDirty = false;
341
+ this.sessionsDirty = false;
342
+ this.memoryDirtyFiles.clear();
343
+ this.sessionsDirtyFiles.clear();
344
+ }
345
+ catch (err) {
346
+ // Restore from backup if it exists
347
+ if (existsSync(backupDbPath)) {
348
+ try {
349
+ this.storage.close();
350
+ await rename(backupDbPath, mainDbPath);
351
+ this.storage = new MemoryStorage({ path: mainDbPath });
352
+ this.tryLoadSqliteVec();
353
+ }
354
+ catch {
355
+ // Last resort - reopen original if rename fails
356
+ this.storage = new MemoryStorage({ path: mainDbPath });
357
+ }
358
+ }
359
+ // Clean up temp if it exists
360
+ if (existsSync(tempDbPath)) {
361
+ try {
362
+ await unlink(tempDbPath);
363
+ }
364
+ catch {
365
+ // Ignore
366
+ }
367
+ }
368
+ throw err;
369
+ }
370
+ }
371
+ /**
372
+ * Index all files into a specific storage instance
373
+ */
374
+ async indexAllFilesInto(storage) {
375
+ const memoryDir = join(this.config.workspaceDir, "memory");
376
+ if (existsSync(memoryDir)) {
377
+ const files = await this.scanMemoryFiles(memoryDir);
378
+ for (const file of files) {
379
+ await this.indexFileInto(file, storage, "memory");
380
+ }
381
+ }
382
+ // Index extra paths
383
+ if (this.config.extraPaths) {
384
+ for (const extraPath of this.config.extraPaths) {
385
+ if (existsSync(extraPath)) {
386
+ const files = await this.scanMemoryFiles(extraPath);
387
+ for (const file of files) {
388
+ await this.indexFileInto(file, storage, "memory");
389
+ }
390
+ }
391
+ }
392
+ }
393
+ // Index sessions if configured
394
+ if (this.config.sync.syncSessions && this.config.sessionsDir) {
395
+ await this.indexSessionsInto(storage);
396
+ }
397
+ }
398
+ /**
399
+ * Perform full sync (for reindex operation)
400
+ */
401
+ async performFullSync() {
402
+ // Delete all existing data
403
+ this.storage.deleteBySource("memory");
404
+ this.storage.deleteBySource("sessions");
405
+ // Re-index everything
406
+ await this.performSync();
407
+ }
408
+ /**
409
+ * Perform the actual sync operation
410
+ */
411
+ async performSync() {
412
+ const progress = {
413
+ phase: "scanning",
414
+ source: "memory",
415
+ totalFiles: 0,
416
+ processedFiles: 0,
417
+ totalChunks: 0,
418
+ embeddedChunks: 0,
419
+ errors: [],
420
+ };
421
+ this.reportProgress(progress);
422
+ // Sync memory files
423
+ await this.syncMemoryFiles(progress);
424
+ // Sync sessions if configured
425
+ if (this.config.sync.syncSessions && this.config.sessionsDir) {
426
+ progress.source = "sessions";
427
+ await this.syncSessionFiles(progress);
428
+ }
429
+ progress.phase = "complete";
430
+ this.reportProgress(progress);
431
+ this.lastSyncAt = Date.now();
432
+ this.memoryDirty = false;
433
+ this.sessionsDirty = false;
434
+ this.memoryDirtyFiles.clear();
435
+ this.sessionsDirtyFiles.clear();
436
+ }
437
+ /**
438
+ * Sync memory markdown files
439
+ */
440
+ async syncMemoryFiles(progress) {
441
+ const memoryDir = join(this.config.workspaceDir, "memory");
442
+ if (!existsSync(memoryDir)) {
443
+ return;
444
+ }
445
+ // Get current files
446
+ const files = await this.scanMemoryFiles(memoryDir);
447
+ // Also scan extra paths
448
+ if (this.config.extraPaths) {
449
+ for (const extraPath of this.config.extraPaths) {
450
+ if (existsSync(extraPath)) {
451
+ const extraFiles = await this.scanMemoryFiles(extraPath);
452
+ files.push(...extraFiles);
453
+ }
454
+ }
455
+ }
456
+ const storedFiles = this.storage.getAllFiles();
457
+ const storedMemoryFiles = storedFiles.filter((f) => f.source === "memory");
458
+ const storedFileMap = new Map(storedMemoryFiles.map((f) => [f.path, f]));
459
+ // Find files to add, update, or remove
460
+ const toProcess = [];
461
+ const currentPaths = new Set();
462
+ progress.totalFiles = files.length;
463
+ for (const file of files) {
464
+ currentPaths.add(file.path);
465
+ const stored = storedFileMap.get(file.path);
466
+ if (!stored || stored.hash !== file.hash) {
467
+ toProcess.push(file);
468
+ }
469
+ }
470
+ // Remove deleted files
471
+ for (const stored of storedMemoryFiles) {
472
+ if (!currentPaths.has(stored.path)) {
473
+ this.storage.deleteFile(stored.path);
474
+ this.storage.deleteChunksByPath(stored.path);
475
+ }
476
+ }
477
+ // Process changed files
478
+ progress.phase = "chunking";
479
+ this.reportProgress(progress);
480
+ for (const file of toProcess) {
481
+ try {
482
+ await this.indexFile(file);
483
+ progress.processedFiles++;
484
+ this.reportProgress(progress);
485
+ }
486
+ catch (err) {
487
+ progress.errors.push({
488
+ file: file.path,
489
+ error: err instanceof Error ? err.message : String(err),
490
+ });
491
+ }
492
+ }
493
+ }
494
+ /**
495
+ * Sync session files
496
+ */
497
+ async syncSessionFiles(progress) {
498
+ if (!this.config.sessionsDir)
499
+ return;
500
+ const sessionsDir = this.config.sessionsDir;
501
+ if (!existsSync(sessionsDir))
502
+ return;
503
+ // Find all JSONL files
504
+ const sessionFiles = await this.scanSessionFiles(sessionsDir);
505
+ const storedFiles = this.storage.getFilesBySource("sessions");
506
+ const storedFileMap = new Map(storedFiles.map((f) => [f.path, f]));
507
+ progress.totalFiles = sessionFiles.length;
508
+ for (const file of sessionFiles) {
509
+ const stored = storedFileMap.get(file.path);
510
+ // Check if file changed (using mtime or hash)
511
+ if (!stored || stored.hash !== file.hash) {
512
+ try {
513
+ await this.indexSessionFile(file, stored?.byteOffset ?? 0);
514
+ progress.processedFiles++;
515
+ this.reportProgress(progress);
516
+ }
517
+ catch (err) {
518
+ progress.errors.push({
519
+ file: file.path,
520
+ error: err instanceof Error ? err.message : String(err),
521
+ });
522
+ }
523
+ }
524
+ }
525
+ }
526
+ /**
527
+ * Scan session directory for JSONL files
528
+ */
529
+ async scanSessionFiles(dir) {
530
+ const files = [];
531
+ try {
532
+ const entries = await readdir(dir, { withFileTypes: true });
533
+ for (const entry of entries) {
534
+ const absPath = join(dir, entry.name);
535
+ if (entry.isDirectory()) {
536
+ const subFiles = await this.scanSessionFiles(absPath);
537
+ files.push(...subFiles);
538
+ }
539
+ else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
540
+ const fileStat = await stat(absPath);
541
+ const content = await readFile(absPath, "utf-8");
542
+ const hash = hashText(content);
543
+ files.push({
544
+ path: absPath, // Use absolute path for sessions
545
+ absPath,
546
+ mtimeMs: fileStat.mtimeMs,
547
+ size: fileStat.size,
548
+ hash,
549
+ });
550
+ }
551
+ }
552
+ }
553
+ catch {
554
+ // Directory might not exist or be accessible
555
+ }
556
+ return files;
557
+ }
558
+ /**
559
+ * Index a session JSONL file with delta tracking
560
+ */
561
+ async indexSessionFile(file, lastByteOffset) {
562
+ const content = await readFile(file.absPath, "utf-8");
563
+ // Only process new content if we have a previous offset
564
+ const newContent = lastByteOffset > 0 ? content.slice(lastByteOffset) : content;
565
+ if (!newContent.trim()) {
566
+ // Just update the file record with new mtime
567
+ this.storage.upsertFile({
568
+ path: file.path,
569
+ source: "sessions",
570
+ hash: file.hash,
571
+ mtime: file.mtimeMs,
572
+ size: file.size,
573
+ });
574
+ return;
575
+ }
576
+ // Extract session ID from filename
577
+ const sessionId = basename(file.path, ".jsonl");
578
+ // Chunk the session content
579
+ const chunks = chunkSession(newContent, sessionId, {
580
+ tokens: this.config.chunking.tokens,
581
+ overlap: this.config.chunking.overlap,
582
+ startByteOffset: lastByteOffset,
583
+ });
584
+ if (chunks.length === 0) {
585
+ return;
586
+ }
587
+ // Generate embeddings
588
+ const texts = chunks.map((c) => c.text);
589
+ const embeddings = await this.embedBatch(texts);
590
+ const now = Date.now();
591
+ const storedChunks = chunks.map((chunk, i) => ({
592
+ id: `${file.path}:${chunk.byteOffset}:${chunk.chunkIndex}`,
593
+ path: file.path,
594
+ source: "sessions",
595
+ startLine: chunk.startLine,
596
+ endLine: chunk.endLine,
597
+ hash: chunk.hash,
598
+ model: this.provider.model,
599
+ text: chunk.text,
600
+ embedding: embeddings[i],
601
+ updatedAt: now,
602
+ sessionId: chunk.sessionId,
603
+ role: chunk.role,
604
+ byteOffset: chunk.byteOffset,
605
+ }));
606
+ // Store chunks in batch
607
+ this.storage.upsertChunksBatch(storedChunks);
608
+ // Update file record with new byte offset
609
+ this.storage.upsertFile({
610
+ path: file.path,
611
+ source: "sessions",
612
+ hash: file.hash,
613
+ mtime: file.mtimeMs,
614
+ size: file.size,
615
+ });
616
+ this.storage.updateFileByteOffset(file.path, content.length);
617
+ }
618
+ /**
619
+ * Index sessions into a specific storage
620
+ */
621
+ async indexSessionsInto(storage) {
622
+ if (!this.config.sessionsDir)
623
+ return;
624
+ const sessionsDir = this.config.sessionsDir;
625
+ if (!existsSync(sessionsDir))
626
+ return;
627
+ const sessionFiles = await this.scanSessionFiles(sessionsDir);
628
+ for (const file of sessionFiles) {
629
+ const content = await readFile(file.absPath, "utf-8");
630
+ const sessionId = basename(file.path, ".jsonl");
631
+ const chunks = chunkSession(content, sessionId, {
632
+ tokens: this.config.chunking.tokens,
633
+ overlap: this.config.chunking.overlap,
634
+ startByteOffset: 0,
635
+ });
636
+ if (chunks.length === 0)
637
+ continue;
638
+ const texts = chunks.map((c) => c.text);
639
+ const embeddings = await this.embedBatch(texts);
640
+ const now = Date.now();
641
+ const storedChunks = chunks.map((chunk, i) => ({
642
+ id: `${file.path}:${chunk.byteOffset}:${chunk.chunkIndex}`,
643
+ path: file.path,
644
+ source: "sessions",
645
+ startLine: chunk.startLine,
646
+ endLine: chunk.endLine,
647
+ hash: chunk.hash,
648
+ model: this.provider.model,
649
+ text: chunk.text,
650
+ embedding: embeddings[i],
651
+ updatedAt: now,
652
+ sessionId: chunk.sessionId,
653
+ role: chunk.role,
654
+ byteOffset: chunk.byteOffset,
655
+ }));
656
+ storage.upsertChunksBatch(storedChunks);
657
+ storage.upsertFile({
658
+ path: file.path,
659
+ source: "sessions",
660
+ hash: file.hash,
661
+ mtime: file.mtimeMs,
662
+ size: file.size,
663
+ });
664
+ }
665
+ }
666
+ /**
667
+ * Report sync progress
668
+ */
669
+ reportProgress(progress) {
670
+ if (this.config.onProgress) {
671
+ this.config.onProgress({ ...progress });
672
+ }
673
+ }
674
+ /**
675
+ * Scan memory directory for markdown files
676
+ */
677
+ async scanMemoryFiles(dir) {
678
+ const files = [];
679
+ const entries = await readdir(dir, { withFileTypes: true });
680
+ for (const entry of entries) {
681
+ const absPath = join(dir, entry.name);
682
+ if (entry.isDirectory()) {
683
+ const subFiles = await this.scanMemoryFiles(absPath);
684
+ files.push(...subFiles);
685
+ }
686
+ else if (entry.isFile() && entry.name.endsWith(".md")) {
687
+ const fileStat = await stat(absPath);
688
+ const content = await readFile(absPath, "utf-8");
689
+ const hash = hashText(content);
690
+ files.push({
691
+ path: relative(this.config.workspaceDir, absPath),
692
+ absPath,
693
+ mtimeMs: fileStat.mtimeMs,
694
+ size: fileStat.size,
695
+ hash,
696
+ });
697
+ }
698
+ }
699
+ return files;
700
+ }
701
+ /**
702
+ * Index a single file
703
+ */
704
+ async indexFile(file) {
705
+ const content = await readFile(file.absPath, "utf-8");
706
+ // Chunk the content
707
+ const chunks = chunkMarkdown(content, this.config.chunking);
708
+ // Delete existing chunks for this file
709
+ this.storage.deleteChunksByPath(file.path);
710
+ // Generate embeddings and store chunks
711
+ const texts = chunks.map((c) => c.text);
712
+ const embeddings = await this.embedBatch(texts);
713
+ const now = Date.now();
714
+ const storedChunks = chunks.map((chunk, i) => ({
715
+ id: `${file.path}:${chunk.startLine}`,
716
+ path: file.path,
717
+ source: "memory",
718
+ startLine: chunk.startLine,
719
+ endLine: chunk.endLine,
720
+ hash: chunk.hash,
721
+ model: this.provider.model,
722
+ text: chunk.text,
723
+ embedding: embeddings[i],
724
+ updatedAt: now,
725
+ }));
726
+ // Store chunks in batch
727
+ this.storage.upsertChunksBatch(storedChunks);
728
+ // Update file record
729
+ this.storage.upsertFile({
730
+ path: file.path,
731
+ source: "memory",
732
+ hash: file.hash,
733
+ mtime: file.mtimeMs,
734
+ size: file.size,
735
+ });
736
+ }
737
+ /**
738
+ * Index a single file into a specific storage
739
+ */
740
+ async indexFileInto(file, storage, source) {
741
+ const content = await readFile(file.absPath, "utf-8");
742
+ const chunks = chunkMarkdown(content, this.config.chunking);
743
+ const texts = chunks.map((c) => c.text);
744
+ const embeddings = await this.embedBatch(texts);
745
+ const now = Date.now();
746
+ const storedChunks = chunks.map((chunk, i) => ({
747
+ id: `${file.path}:${chunk.startLine}`,
748
+ path: file.path,
749
+ source,
750
+ startLine: chunk.startLine,
751
+ endLine: chunk.endLine,
752
+ hash: chunk.hash,
753
+ model: this.provider.model,
754
+ text: chunk.text,
755
+ embedding: embeddings[i],
756
+ updatedAt: now,
757
+ }));
758
+ storage.upsertChunksBatch(storedChunks);
759
+ storage.upsertFile({
760
+ path: file.path,
761
+ source,
762
+ hash: file.hash,
763
+ mtime: file.mtimeMs,
764
+ size: file.size,
765
+ });
766
+ }
767
+ /**
768
+ * Embed a batch of texts with caching and rate limiting
769
+ */
770
+ async embedBatch(texts) {
771
+ if (texts.length === 0) {
772
+ return [];
773
+ }
774
+ const provider = this.provider;
775
+ const providerKey = this.providerKey;
776
+ // Check cache for existing embeddings
777
+ const results = [];
778
+ const uncachedTexts = [];
779
+ for (let i = 0; i < texts.length; i++) {
780
+ const text = texts[i];
781
+ const hash = hashText(text);
782
+ const cached = this.storage.getCachedEmbedding(provider.id, provider.model, providerKey, hash);
783
+ if (cached) {
784
+ results[i] = cached;
785
+ }
786
+ else {
787
+ results[i] = null;
788
+ uncachedTexts.push({ index: i, text, hash });
789
+ }
790
+ }
791
+ // Embed uncached texts in batches
792
+ if (uncachedTexts.length > 0) {
793
+ const batchSize = this.config.batch.maxBatchSize;
794
+ for (let i = 0; i < uncachedTexts.length; i += batchSize) {
795
+ const batch = uncachedTexts.slice(i, i + batchSize);
796
+ const batchTexts = batch.map((t) => t.text);
797
+ // Use semaphore for rate limiting
798
+ const embeddings = await this.embeddingSemaphore.run(() => retry(async () => {
799
+ try {
800
+ const result = await provider.embedBatch(batchTexts);
801
+ this.consecutiveEmbeddingFailures = 0;
802
+ return result;
803
+ }
804
+ catch (err) {
805
+ this.consecutiveEmbeddingFailures++;
806
+ if (this.consecutiveEmbeddingFailures >= this.config.batch.maxConsecutiveFailures &&
807
+ !this.fallbackActivated) {
808
+ await this.activateFallbackProvider(err instanceof Error ? err.message : String(err));
809
+ if (this.fallbackProvider) {
810
+ return this.fallbackProvider.embedBatch(batchTexts);
811
+ }
812
+ }
813
+ throw err;
814
+ }
815
+ }, {
816
+ maxAttempts: 3,
817
+ baseDelayMs: 1000,
818
+ }));
819
+ // Store in cache and results
820
+ for (let j = 0; j < batch.length; j++) {
821
+ const { index, hash } = batch[j];
822
+ const embedding = embeddings[j];
823
+ results[index] = embedding;
824
+ this.storage.cacheEmbedding(provider.id, provider.model, providerKey, hash, embedding);
825
+ }
826
+ }
827
+ }
828
+ return results;
829
+ }
830
+ /**
831
+ * Start watching for file changes
832
+ */
833
+ async startWatching() {
834
+ if (this.watcher) {
835
+ return;
836
+ }
837
+ const patterns = ["memory/**/*.md"];
838
+ this.watcher = new FileWatcher(this.config.workspaceDir, {
839
+ debounceMs: this.config.sync.watchDebounceMs,
840
+ patterns,
841
+ onEvent: (event) => {
842
+ this.memoryDirty = true;
843
+ this.memoryDirtyFiles.add(event.path);
844
+ // Trigger sync on file changes
845
+ this.sync().catch(() => { });
846
+ },
847
+ });
848
+ await this.watcher.start();
849
+ }
850
+ /**
851
+ * Start interval-based sync
852
+ */
853
+ startIntervalSync() {
854
+ if (this.syncIntervalTimer) {
855
+ return;
856
+ }
857
+ this.syncIntervalTimer = setInterval(() => {
858
+ if (this.memoryDirty || this.sessionsDirty) {
859
+ this.sync().catch(() => { });
860
+ }
861
+ }, this.config.sync.intervalMs);
862
+ }
863
+ /**
864
+ * Stop interval-based sync
865
+ */
866
+ stopIntervalSync() {
867
+ if (this.syncIntervalTimer) {
868
+ clearInterval(this.syncIntervalTimer);
869
+ this.syncIntervalTimer = null;
870
+ }
871
+ }
872
+ /**
873
+ * Stop watching for file changes
874
+ */
875
+ async stopWatching() {
876
+ if (this.watcher) {
877
+ await this.watcher.stop();
878
+ this.watcher = null;
879
+ }
880
+ }
881
+ /**
882
+ * Close the manager and release resources
883
+ */
884
+ async close() {
885
+ this.stopIntervalSync();
886
+ await this.stopWatching();
887
+ this.storage.close();
888
+ this.closed = true;
889
+ // Remove from cache
890
+ const cacheKey = `${this.config.agentId}:${this.config.workspaceDir}`;
891
+ INDEX_CACHE.delete(cacheKey);
892
+ }
893
+ /**
894
+ * Get provider information
895
+ */
896
+ getProviderInfo() {
897
+ if (!this.providerResult) {
898
+ return { provider: "unknown", model: "unknown" };
899
+ }
900
+ return {
901
+ provider: this.fallbackActivated && this.fallbackProvider
902
+ ? this.fallbackProvider.id
903
+ : this.providerResult.provider.id,
904
+ model: this.fallbackActivated && this.fallbackProvider
905
+ ? this.fallbackProvider.model
906
+ : this.providerResult.provider.model,
907
+ fallbackFrom: this.providerResult.fallbackFrom,
908
+ fallbackReason: this.providerResult.fallbackReason,
909
+ };
910
+ }
911
+ /**
912
+ * Get comprehensive status information
913
+ */
914
+ getStatus() {
915
+ const sourceCounts = this.storage.getSourceCounts();
916
+ const memoryStats = sourceCounts.find((s) => s.source === "memory") ?? { files: 0, chunks: 0 };
917
+ const sessionStats = sourceCounts.find((s) => s.source === "sessions") ?? { files: 0, chunks: 0 };
918
+ const provider = this.providerResult?.provider ?? { id: "unknown", model: "unknown", dimensions: 0 };
919
+ return {
920
+ database: {
921
+ path: this.storage.path,
922
+ sizeBytes: this.storage.getSizeBytes(),
923
+ schemaVersion: this.storage.schemaStatus.version,
924
+ },
925
+ sources: {
926
+ memory: { files: memoryStats.files, chunks: memoryStats.chunks },
927
+ sessions: { files: sessionStats.files, chunks: sessionStats.chunks },
928
+ },
929
+ fts: {
930
+ available: this.storage.schemaStatus.ftsAvailable,
931
+ error: this.storage.schemaStatus.ftsError,
932
+ },
933
+ vector: {
934
+ sqliteVecAvailable: this.sqliteVecLoaded,
935
+ usingJsFallback: !this.sqliteVecLoaded,
936
+ error: this.sqliteVecError,
937
+ },
938
+ provider: {
939
+ id: this.fallbackActivated && this.fallbackProvider
940
+ ? this.fallbackProvider.id
941
+ : provider.id,
942
+ model: this.fallbackActivated && this.fallbackProvider
943
+ ? this.fallbackProvider.model
944
+ : provider.model,
945
+ dimensions: this.fallbackActivated && this.fallbackProvider
946
+ ? (this.fallbackProvider.dimensions ?? 0)
947
+ : (provider.dimensions ?? 0),
948
+ fallbackActivated: this.fallbackActivated,
949
+ fallbackFrom: this.providerResult?.fallbackFrom,
950
+ fallbackReason: this.providerResult?.fallbackReason,
951
+ consecutiveFailures: this.consecutiveEmbeddingFailures,
952
+ },
953
+ batch: {
954
+ enabled: this.config.batch.enabled && !this.batchDisabled,
955
+ pendingJobs: 0, // TODO: track pending batch jobs
956
+ autoDisabled: this.batchDisabled,
957
+ disableReason: this.batchDisableReason,
958
+ },
959
+ sync: {
960
+ watching: this.watcher !== null,
961
+ intervalMs: this.config.sync.intervalMs > 0 ? this.config.sync.intervalMs : undefined,
962
+ lastSyncAt: this.lastSyncAt,
963
+ memoryDirty: this.memoryDirty,
964
+ sessionsDirty: this.sessionsDirty,
965
+ },
966
+ };
967
+ }
968
+ /**
969
+ * Probe vector search availability
970
+ */
971
+ probeVectorAvailability() {
972
+ const start = Date.now();
973
+ if (this.sqliteVecLoaded) {
974
+ try {
975
+ // Test a simple vector operation
976
+ this.storage.database.prepare("SELECT vec_version()").get();
977
+ return {
978
+ available: true,
979
+ latencyMs: Date.now() - start,
980
+ };
981
+ }
982
+ catch (err) {
983
+ return {
984
+ available: false,
985
+ error: err instanceof Error ? err.message : String(err),
986
+ latencyMs: Date.now() - start,
987
+ };
988
+ }
989
+ }
990
+ // JS fallback is always available
991
+ return {
992
+ available: true,
993
+ latencyMs: Date.now() - start,
994
+ };
995
+ }
996
+ /**
997
+ * Probe embedding API availability
998
+ */
999
+ async probeEmbeddingAvailability() {
1000
+ const start = Date.now();
1001
+ try {
1002
+ await this.provider.embedQuery("test");
1003
+ return {
1004
+ available: true,
1005
+ latencyMs: Date.now() - start,
1006
+ };
1007
+ }
1008
+ catch (err) {
1009
+ return {
1010
+ available: false,
1011
+ error: err instanceof Error ? err.message : String(err),
1012
+ latencyMs: Date.now() - start,
1013
+ };
1014
+ }
1015
+ }
1016
+ /**
1017
+ * Probe FTS5 availability
1018
+ */
1019
+ probeFtsAvailability() {
1020
+ const start = Date.now();
1021
+ try {
1022
+ this.storage.database.prepare("SELECT 1 FROM chunks_fts LIMIT 0").run();
1023
+ return {
1024
+ available: true,
1025
+ latencyMs: Date.now() - start,
1026
+ };
1027
+ }
1028
+ catch (err) {
1029
+ return {
1030
+ available: false,
1031
+ error: err instanceof Error ? err.message : String(err),
1032
+ latencyMs: Date.now() - start,
1033
+ };
1034
+ }
1035
+ }
1036
+ /**
1037
+ * Mark memory files as dirty (for external triggers)
1038
+ */
1039
+ markMemoryDirty(files) {
1040
+ this.memoryDirty = true;
1041
+ if (files) {
1042
+ files.forEach((f) => this.memoryDirtyFiles.add(f));
1043
+ }
1044
+ }
1045
+ /**
1046
+ * Mark session files as dirty (for external triggers)
1047
+ */
1048
+ markSessionsDirty(files) {
1049
+ this.sessionsDirty = true;
1050
+ if (files) {
1051
+ files.forEach((f) => this.sessionsDirtyFiles.add(f));
1052
+ }
1053
+ }
1054
+ }
1055
+ //# sourceMappingURL=memory-manager.js.map