@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.
- package/dist/chunking/index.d.ts +3 -0
- package/dist/chunking/index.d.ts.map +1 -0
- package/dist/chunking/index.js +3 -0
- package/dist/chunking/index.js.map +1 -0
- package/dist/chunking/markdown.d.ts +13 -0
- package/dist/chunking/markdown.d.ts.map +1 -0
- package/dist/chunking/markdown.js +106 -0
- package/dist/chunking/markdown.js.map +1 -0
- package/dist/chunking/session.d.ts +24 -0
- package/dist/chunking/session.d.ts.map +1 -0
- package/dist/chunking/session.js +173 -0
- package/dist/chunking/session.js.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +24 -0
- package/dist/index.js.map +1 -0
- package/dist/memory-manager.d.ts +189 -0
- package/dist/memory-manager.d.ts.map +1 -0
- package/dist/memory-manager.js +1055 -0
- package/dist/memory-manager.js.map +1 -0
- package/dist/providers/gemini.d.ts +6 -0
- package/dist/providers/gemini.d.ts.map +1 -0
- package/dist/providers/gemini.js +73 -0
- package/dist/providers/gemini.js.map +1 -0
- package/dist/providers/index.d.ts +20 -0
- package/dist/providers/index.d.ts.map +1 -0
- package/dist/providers/index.js +102 -0
- package/dist/providers/index.js.map +1 -0
- package/dist/providers/local.d.ts +14 -0
- package/dist/providers/local.d.ts.map +1 -0
- package/dist/providers/local.js +73 -0
- package/dist/providers/local.js.map +1 -0
- package/dist/providers/openai.d.ts +6 -0
- package/dist/providers/openai.d.ts.map +1 -0
- package/dist/providers/openai.js +48 -0
- package/dist/providers/openai.js.map +1 -0
- package/dist/providers/types.d.ts +62 -0
- package/dist/providers/types.d.ts.map +1 -0
- package/dist/providers/types.js +2 -0
- package/dist/providers/types.js.map +1 -0
- package/dist/search/fts.d.ts +11 -0
- package/dist/search/fts.d.ts.map +1 -0
- package/dist/search/fts.js +50 -0
- package/dist/search/fts.js.map +1 -0
- package/dist/search/hybrid.d.ts +16 -0
- package/dist/search/hybrid.d.ts.map +1 -0
- package/dist/search/hybrid.js +83 -0
- package/dist/search/hybrid.js.map +1 -0
- package/dist/search/index.d.ts +4 -0
- package/dist/search/index.d.ts.map +1 -0
- package/dist/search/index.js +4 -0
- package/dist/search/index.js.map +1 -0
- package/dist/search/vector.d.ts +25 -0
- package/dist/search/vector.d.ts.map +1 -0
- package/dist/search/vector.js +152 -0
- package/dist/search/vector.js.map +1 -0
- package/dist/storage/index.d.ts +4 -0
- package/dist/storage/index.d.ts.map +1 -0
- package/dist/storage/index.js +4 -0
- package/dist/storage/index.js.map +1 -0
- package/dist/storage/schema.d.ts +24 -0
- package/dist/storage/schema.d.ts.map +1 -0
- package/dist/storage/schema.js +175 -0
- package/dist/storage/schema.js.map +1 -0
- package/dist/storage/sqlite-vec.d.ts +22 -0
- package/dist/storage/sqlite-vec.d.ts.map +1 -0
- package/dist/storage/sqlite-vec.js +85 -0
- package/dist/storage/sqlite-vec.js.map +1 -0
- package/dist/storage/sqlite.d.ts +206 -0
- package/dist/storage/sqlite.d.ts.map +1 -0
- package/dist/storage/sqlite.js +352 -0
- package/dist/storage/sqlite.js.map +1 -0
- package/dist/sync/index.d.ts +4 -0
- package/dist/sync/index.d.ts.map +1 -0
- package/dist/sync/index.js +4 -0
- package/dist/sync/index.js.map +1 -0
- package/dist/sync/minimatch.d.ts +6 -0
- package/dist/sync/minimatch.d.ts.map +1 -0
- package/dist/sync/minimatch.js +60 -0
- package/dist/sync/minimatch.js.map +1 -0
- package/dist/sync/session-monitor.d.ts +50 -0
- package/dist/sync/session-monitor.d.ts.map +1 -0
- package/dist/sync/session-monitor.js +126 -0
- package/dist/sync/session-monitor.js.map +1 -0
- package/dist/sync/watcher.d.ts +44 -0
- package/dist/sync/watcher.d.ts.map +1 -0
- package/dist/sync/watcher.js +110 -0
- package/dist/sync/watcher.js.map +1 -0
- package/dist/tools/index.d.ts +3 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +3 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/memory-get.d.ts +32 -0
- package/dist/tools/memory-get.d.ts.map +1 -0
- package/dist/tools/memory-get.js +53 -0
- package/dist/tools/memory-get.js.map +1 -0
- package/dist/tools/memory-search.d.ts +32 -0
- package/dist/tools/memory-search.d.ts.map +1 -0
- package/dist/tools/memory-search.js +56 -0
- package/dist/tools/memory-search.js.map +1 -0
- package/dist/types.d.ts +350 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +15 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/concurrency.d.ts +25 -0
- package/dist/utils/concurrency.d.ts.map +1 -0
- package/dist/utils/concurrency.js +59 -0
- package/dist/utils/concurrency.js.map +1 -0
- package/dist/utils/hash.d.ts +9 -0
- package/dist/utils/hash.d.ts.map +1 -0
- package/dist/utils/hash.js +16 -0
- package/dist/utils/hash.js.map +1 -0
- package/dist/utils/index.d.ts +4 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +4 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/retry.d.ts +22 -0
- package/dist/utils/retry.d.ts.map +1 -0
- package/dist/utils/retry.js +48 -0
- package/dist/utils/retry.js.map +1 -0
- 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
|