specmem-hardwicksoftware 3.5.99 → 3.6.1
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/bin/specmem-statusbar.cjs +154 -298
- package/claude-hooks/agent-loading-hook.js +8 -4
- package/claude-hooks/team-comms-enforcer.cjs +109 -92
- package/dist/config/embeddingTimeouts.js +4 -4
- package/dist/database.js +52 -6
- package/dist/db/bigBrainMigrations.js +7 -6
- package/dist/db/memoryDrilldown.sql +1 -1
- package/dist/db/projectSchemaInit.sql +21 -0
- package/dist/index.js +238 -13
- package/dist/installer/firstRun.js +2 -2
- package/dist/mcp/embeddingServerManager.js +225 -7
- package/dist/mcp/healthMonitor.js +165 -32
- package/dist/mcp/tools/embeddingControl.js +31 -0
- package/dist/mcp/tools/teamComms.js +16 -0
- package/dist/mcp/watcherIntegration.js +50 -7
- package/dist/services/CameraZoomSearch.js +62 -5
- package/dist/services/DimensionService.js +73 -6
- package/dist/services/EmbeddingQueue.js +64 -0
- package/dist/services/MemoryDrilldown.js +19 -12
- package/dist/tools/goofy/findCodePointers.js +11 -7
- package/dist/tools/goofy/findWhatISaid.js +145 -53
- package/dist/utils/qoms.js +187 -4
- package/dist/watcher/changeHandler.js +54 -4
- package/dist/watcher/fileWatcher.js +121 -1
- package/dist/watcher/index.js +75 -31
- package/dist/watcher/syncChecker.js +248 -63
- package/embedding-sandbox/__pycache__/frankenstein-embeddings.cpython-313.pyc +0 -0
- package/embedding-sandbox/frankenstein-embeddings.py +175 -64
- package/package.json +1 -1
|
@@ -17,6 +17,23 @@ import { join } from 'path';
|
|
|
17
17
|
import { createHash } from 'crypto';
|
|
18
18
|
import { logger } from '../utils/logger.js';
|
|
19
19
|
import { glob } from 'glob';
|
|
20
|
+
import { getEmbeddingTimeout } from '../config/embeddingTimeouts.js';
|
|
21
|
+
/**
|
|
22
|
+
* Wrap an async operation with a timeout. Prevents sync/resync from hanging
|
|
23
|
+
* indefinitely when embedding service or DB becomes unresponsive.
|
|
24
|
+
*/
|
|
25
|
+
function withSyncTimeout(operation, timeoutMs, operationName) {
|
|
26
|
+
return new Promise((resolve, reject) => {
|
|
27
|
+
const timeoutId = setTimeout(() => {
|
|
28
|
+
const err = new Error(`[Sync] ${operationName} timed out after ${Math.round(timeoutMs / 1000)}s`);
|
|
29
|
+
err.code = 'SYNC_TIMEOUT';
|
|
30
|
+
reject(err);
|
|
31
|
+
}, timeoutMs);
|
|
32
|
+
Promise.resolve().then(() => operation())
|
|
33
|
+
.then(result => { clearTimeout(timeoutId); resolve(result); })
|
|
34
|
+
.catch(error => { clearTimeout(timeoutId); reject(error); });
|
|
35
|
+
});
|
|
36
|
+
}
|
|
20
37
|
/**
|
|
21
38
|
* areWeStillInSync - sync status checker
|
|
22
39
|
*
|
|
@@ -124,6 +141,9 @@ export class AreWeStillInSync {
|
|
|
124
141
|
async resyncEverythingFrFr() {
|
|
125
142
|
logger.info('starting full resync (non-blocking)...');
|
|
126
143
|
const startTime = Date.now();
|
|
144
|
+
// Overall resync deadline: 10 minutes max (configurable via SPECMEM_RESYNC_TIMEOUT_MS)
|
|
145
|
+
const RESYNC_TIMEOUT_MS = parseInt(process.env['SPECMEM_RESYNC_TIMEOUT_MS'] || '600000');
|
|
146
|
+
const isOverDeadline = () => (Date.now() - startTime) > RESYNC_TIMEOUT_MS;
|
|
127
147
|
const result = {
|
|
128
148
|
success: false,
|
|
129
149
|
filesAdded: 0,
|
|
@@ -140,21 +160,31 @@ export class AreWeStillInSync {
|
|
|
140
160
|
missingFromDisk: driftReport.missingFromDisk.length,
|
|
141
161
|
contentMismatch: driftReport.contentMismatch.length
|
|
142
162
|
}, 'drift detected - starting resync');
|
|
143
|
-
// Helper to process files in PARALLEL batches with concurrency
|
|
163
|
+
// Helper to process files in PARALLEL batches with concurrency + retry on transient failure
|
|
144
164
|
const CONCURRENCY = 20; // Process 20 files simultaneously for 10+ files/sec throughput
|
|
165
|
+
const PER_FILE_TIMEOUT = getEmbeddingTimeout('fileWatcher'); // 120s per file operation
|
|
166
|
+
const MAX_FILE_RETRIES = 1; // Retry failed files once before giving up
|
|
145
167
|
const processInBatches = async (files, handler, operationType) => {
|
|
146
168
|
const batchErrors = [];
|
|
147
169
|
let processed = 0;
|
|
170
|
+
let retryQueue = []; // Files that failed transiently and should be retried
|
|
148
171
|
for (let i = 0; i < files.length; i += CONCURRENCY) {
|
|
172
|
+
if (isOverDeadline()) break; // Respect overall deadline
|
|
149
173
|
const batch = files.slice(i, i + CONCURRENCY);
|
|
150
|
-
const results = await Promise.allSettled(batch.map(path => handler(path).then(() => path)));
|
|
151
|
-
for (
|
|
174
|
+
const results = await Promise.allSettled(batch.map(path => withSyncTimeout(() => handler(path).then(() => path), PER_FILE_TIMEOUT, `${operationType} ${path}`)));
|
|
175
|
+
for (let j = 0; j < results.length; j++) {
|
|
176
|
+
const result = results[j];
|
|
152
177
|
if (result.status === 'fulfilled') {
|
|
153
178
|
processed++;
|
|
154
179
|
} else {
|
|
155
180
|
const errMsg = result.reason?.message || String(result.reason);
|
|
156
|
-
|
|
157
|
-
|
|
181
|
+
const isTransient = errMsg.includes('timeout') || errMsg.includes('ECONNRESET') || errMsg.includes('socket') || errMsg.includes('QOMS');
|
|
182
|
+
if (isTransient) {
|
|
183
|
+
retryQueue.push(batch[j]);
|
|
184
|
+
} else {
|
|
185
|
+
batchErrors.push(`Failed to ${operationType}: ${errMsg}`);
|
|
186
|
+
logger.error({ error: result.reason }, `failed to ${operationType} file during resync`);
|
|
187
|
+
}
|
|
158
188
|
}
|
|
159
189
|
}
|
|
160
190
|
// Yield to event loop between parallel batches
|
|
@@ -166,6 +196,24 @@ export class AreWeStillInSync {
|
|
|
166
196
|
logger.info({ processed, total: files.length, operationType }, 'resync progress');
|
|
167
197
|
}
|
|
168
198
|
}
|
|
199
|
+
// Retry pass: process transiently-failed files once more with backoff
|
|
200
|
+
if (retryQueue.length > 0 && !isOverDeadline()) {
|
|
201
|
+
logger.info({ retryCount: retryQueue.length, operationType }, '[Sync] Retrying transiently-failed files after 2s backoff');
|
|
202
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
203
|
+
for (let i = 0; i < retryQueue.length; i += CONCURRENCY) {
|
|
204
|
+
if (isOverDeadline()) break;
|
|
205
|
+
const retryBatch = retryQueue.slice(i, i + CONCURRENCY);
|
|
206
|
+
const retryResults = await Promise.allSettled(retryBatch.map(path => withSyncTimeout(() => handler(path).then(() => path), PER_FILE_TIMEOUT, `${operationType} retry ${path}`)));
|
|
207
|
+
for (const result of retryResults) {
|
|
208
|
+
if (result.status === 'fulfilled') {
|
|
209
|
+
processed++;
|
|
210
|
+
} else {
|
|
211
|
+
const errMsg = result.reason?.message || String(result.reason);
|
|
212
|
+
batchErrors.push(`Failed to ${operationType} (after retry): ${errMsg}`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
169
217
|
return { processed, errors: batchErrors };
|
|
170
218
|
};
|
|
171
219
|
// 2. add missing files to MCP
|
|
@@ -182,6 +230,13 @@ export class AreWeStillInSync {
|
|
|
182
230
|
}, 'add');
|
|
183
231
|
result.filesAdded = addResult.processed;
|
|
184
232
|
result.errors.push(...addResult.errors);
|
|
233
|
+
// Check overall deadline between phases
|
|
234
|
+
if (isOverDeadline()) {
|
|
235
|
+
logger.warn({ elapsedMs: Date.now() - startTime, phase: 'after-add' }, '[Sync] Resync deadline exceeded, stopping early');
|
|
236
|
+
result.errors.push(`Resync deadline exceeded after add phase (${Math.round((Date.now() - startTime) / 1000)}s)`);
|
|
237
|
+
result.duration = Date.now() - startTime;
|
|
238
|
+
return result;
|
|
239
|
+
}
|
|
185
240
|
// 3. update files with content mismatch
|
|
186
241
|
const updateResult = await processInBatches(driftReport.contentMismatch, async (path) => {
|
|
187
242
|
const fullPath = join(this.config.rootPath, path);
|
|
@@ -196,6 +251,13 @@ export class AreWeStillInSync {
|
|
|
196
251
|
}, 'update');
|
|
197
252
|
result.filesUpdated = updateResult.processed;
|
|
198
253
|
result.errors.push(...updateResult.errors);
|
|
254
|
+
// Check overall deadline between phases
|
|
255
|
+
if (isOverDeadline()) {
|
|
256
|
+
logger.warn({ elapsedMs: Date.now() - startTime, phase: 'after-update' }, '[Sync] Resync deadline exceeded, stopping early');
|
|
257
|
+
result.errors.push(`Resync deadline exceeded after update phase (${Math.round((Date.now() - startTime) / 1000)}s)`);
|
|
258
|
+
result.duration = Date.now() - startTime;
|
|
259
|
+
return result;
|
|
260
|
+
}
|
|
199
261
|
// 4. mark deleted files
|
|
200
262
|
const deleteResult = await processInBatches(driftReport.missingFromDisk, async (path) => {
|
|
201
263
|
await this.config.changeHandler.handleChange({
|
|
@@ -229,38 +291,52 @@ export class AreWeStillInSync {
|
|
|
229
291
|
/**
|
|
230
292
|
* scanDiskFiles - scans filesystem for all files
|
|
231
293
|
*
|
|
232
|
-
* NON-BLOCKING:
|
|
233
|
-
*
|
|
294
|
+
* NON-BLOCKING: Uses streaming/generator pattern to avoid loading all paths into memory.
|
|
295
|
+
* Processes files in configurable batches with memory pressure detection.
|
|
296
|
+
* Respects .gitignore and configurable ignore patterns.
|
|
297
|
+
*
|
|
298
|
+
* Env vars:
|
|
299
|
+
* SPECMEM_SCAN_BATCH_SIZE - files per batch (default 500)
|
|
300
|
+
* SPECMEM_SCAN_MAX_FILES - max files to scan (default 50000)
|
|
301
|
+
* SPECMEM_SCAN_MAX_HEAP_MB - heap limit before pausing (default 2048)
|
|
302
|
+
* SPECMEM_SCAN_IGNORE_PATTERNS - comma-separated ignore dirs (default "node_modules,.git,dist,build,.next,__pycache__")
|
|
234
303
|
*/
|
|
235
304
|
async scanDiskFiles() {
|
|
236
|
-
logger.debug('scanning disk files (non-blocking)...');
|
|
237
|
-
//
|
|
305
|
+
logger.debug('scanning disk files (streaming, non-blocking)...');
|
|
306
|
+
// Configurable limits via environment variables
|
|
307
|
+
const SCAN_BATCH_SIZE = parseInt(process.env['SPECMEM_SCAN_BATCH_SIZE'] || '500');
|
|
308
|
+
const SCAN_MAX_FILES = parseInt(process.env['SPECMEM_SCAN_MAX_FILES'] || '50000');
|
|
309
|
+
const SCAN_MAX_HEAP_MB = parseInt(process.env['SPECMEM_SCAN_MAX_HEAP_MB'] || '2048');
|
|
310
|
+
const SCAN_MAX_HEAP_BYTES = SCAN_MAX_HEAP_MB * 1024 * 1024;
|
|
311
|
+
// Configurable ignore patterns via env var (comma-separated directory names)
|
|
312
|
+
const defaultIgnoreDirs = 'node_modules,.git,dist,build,.next,__pycache__';
|
|
313
|
+
const envIgnoreDirs = process.env['SPECMEM_SCAN_IGNORE_PATTERNS'] || defaultIgnoreDirs;
|
|
314
|
+
const ignoreDirNames = envIgnoreDirs.split(',').map(d => d.trim()).filter(Boolean);
|
|
315
|
+
// Build glob ignore patterns from the directory names
|
|
238
316
|
const ignorePatterns = [
|
|
239
|
-
|
|
240
|
-
'**/.git/**',
|
|
241
|
-
'**/dist/**',
|
|
242
|
-
'**/build/**',
|
|
243
|
-
'**/.next/**',
|
|
317
|
+
...ignoreDirNames.map(d => `**/${d}/**`),
|
|
244
318
|
'**/coverage/**',
|
|
245
319
|
'**/.cache/**',
|
|
246
320
|
...this.config.ignorePatterns
|
|
247
321
|
];
|
|
248
|
-
//
|
|
249
|
-
const
|
|
322
|
+
// Use glob stream to avoid loading all paths into a single array
|
|
323
|
+
const fileStream = glob.stream('**/*', {
|
|
250
324
|
cwd: this.config.rootPath,
|
|
251
325
|
ignore: ignorePatterns,
|
|
252
326
|
nodir: true, // only files
|
|
253
327
|
dot: false, // ignore dotfiles
|
|
254
328
|
absolute: false
|
|
255
329
|
});
|
|
256
|
-
// read and hash each file in batches, yielding to event loop between batches
|
|
257
330
|
const fileData = [];
|
|
258
|
-
|
|
331
|
+
let batch = [];
|
|
259
332
|
let processedCount = 0;
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
333
|
+
let totalEnumerated = 0;
|
|
334
|
+
let memoryPressurePaused = false;
|
|
335
|
+
/**
|
|
336
|
+
* Process a single batch of file paths: stat, read, hash.
|
|
337
|
+
*/
|
|
338
|
+
const processBatch = async (fileBatch) => {
|
|
339
|
+
for (const file of fileBatch) {
|
|
264
340
|
try {
|
|
265
341
|
const fullPath = join(this.config.rootPath, file);
|
|
266
342
|
const stats = await fs.stat(fullPath);
|
|
@@ -282,70 +358,179 @@ export class AreWeStillInSync {
|
|
|
282
358
|
logger.debug({ error, path: file }, 'failed to process file');
|
|
283
359
|
}
|
|
284
360
|
}
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
361
|
+
};
|
|
362
|
+
// Consume the glob stream in batches
|
|
363
|
+
for await (const filePath of fileStream) {
|
|
364
|
+
totalEnumerated++;
|
|
365
|
+
// Enforce max files limit to prevent runaway scanning
|
|
366
|
+
if (totalEnumerated > SCAN_MAX_FILES) {
|
|
367
|
+
logger.warn({ maxFiles: SCAN_MAX_FILES, enumerated: totalEnumerated }, '[SyncChecker] Max files limit reached, stopping scan');
|
|
368
|
+
break;
|
|
369
|
+
}
|
|
370
|
+
batch.push(typeof filePath === 'string' ? filePath : filePath.toString());
|
|
371
|
+
// When batch is full, process it
|
|
372
|
+
if (batch.length >= SCAN_BATCH_SIZE) {
|
|
373
|
+
// Memory pressure detection
|
|
374
|
+
const heapUsed = process.memoryUsage().heapUsed;
|
|
375
|
+
if (heapUsed > SCAN_MAX_HEAP_BYTES) {
|
|
376
|
+
if (!memoryPressurePaused) {
|
|
377
|
+
logger.warn({
|
|
378
|
+
heapUsedMB: Math.round(heapUsed / (1024 * 1024)),
|
|
379
|
+
limitMB: SCAN_MAX_HEAP_MB,
|
|
380
|
+
processedSoFar: processedCount,
|
|
381
|
+
enumerated: totalEnumerated
|
|
382
|
+
}, '[SyncChecker] Memory pressure detected during disk scan, pausing to allow GC');
|
|
383
|
+
memoryPressurePaused = true;
|
|
384
|
+
}
|
|
385
|
+
// Force GC if available, then yield to let it run
|
|
386
|
+
if (global.gc) {
|
|
387
|
+
global.gc();
|
|
388
|
+
}
|
|
389
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
390
|
+
// Re-check after pause
|
|
391
|
+
const heapAfter = process.memoryUsage().heapUsed;
|
|
392
|
+
if (heapAfter > SCAN_MAX_HEAP_BYTES) {
|
|
393
|
+
logger.warn({
|
|
394
|
+
heapUsedMB: Math.round(heapAfter / (1024 * 1024)),
|
|
395
|
+
limitMB: SCAN_MAX_HEAP_MB,
|
|
396
|
+
processedSoFar: processedCount
|
|
397
|
+
}, '[SyncChecker] Memory pressure persists after GC pause, stopping scan early');
|
|
398
|
+
break;
|
|
399
|
+
}
|
|
400
|
+
memoryPressurePaused = false;
|
|
401
|
+
}
|
|
402
|
+
await processBatch(batch);
|
|
403
|
+
processedCount += batch.length;
|
|
404
|
+
batch = [];
|
|
405
|
+
// Yield to event loop between batches - prevents blocking
|
|
289
406
|
await new Promise(resolve => setImmediate(resolve));
|
|
290
|
-
// Log progress
|
|
291
|
-
if (processedCount %
|
|
292
|
-
logger.debug({ processed: processedCount,
|
|
407
|
+
// Log progress periodically
|
|
408
|
+
if (processedCount % 1000 === 0) {
|
|
409
|
+
logger.debug({ processed: processedCount, hashed: fileData.length, enumerated: totalEnumerated }, 'disk scan progress');
|
|
293
410
|
}
|
|
294
411
|
}
|
|
295
412
|
}
|
|
296
|
-
|
|
413
|
+
// Process any remaining files in the last partial batch
|
|
414
|
+
if (batch.length > 0) {
|
|
415
|
+
await processBatch(batch);
|
|
416
|
+
processedCount += batch.length;
|
|
417
|
+
}
|
|
418
|
+
logger.debug({ count: fileData.length, totalProcessed: processedCount, totalEnumerated, maxFiles: SCAN_MAX_FILES }, 'disk scan complete');
|
|
297
419
|
return fileData;
|
|
298
420
|
}
|
|
299
421
|
/**
|
|
300
422
|
* scanMcpMemories - gets all file-watcher memories from MCP
|
|
301
|
-
* Also checks codebase_files table where actual indexed files are stored
|
|
423
|
+
* Also checks codebase_files table where actual indexed files are stored.
|
|
424
|
+
*
|
|
425
|
+
* Uses pagination to handle codebases with >10K memories.
|
|
426
|
+
*
|
|
427
|
+
* Env vars:
|
|
428
|
+
* SPECMEM_SYNC_MEMORY_LIMIT - max total memories to fetch (default 50000)
|
|
429
|
+
* SPECMEM_SYNC_MEMORY_PAGE_SIZE - page size for paginated queries (default 5000)
|
|
302
430
|
*/
|
|
303
431
|
async scanMcpMemories() {
|
|
304
432
|
logger.debug('scanning MCP memories and codebase_files...');
|
|
433
|
+
const MEMORY_LIMIT = parseInt(process.env['SPECMEM_SYNC_MEMORY_LIMIT'] || '50000');
|
|
434
|
+
const PAGE_SIZE = parseInt(process.env['SPECMEM_SYNC_MEMORY_PAGE_SIZE'] || '5000');
|
|
305
435
|
const allFiles = [];
|
|
306
436
|
const seenPaths = new Set();
|
|
307
437
|
try {
|
|
308
438
|
// 1. Check codebase_files table first (this is where indexed files actually live)
|
|
309
439
|
const pool = this.config.search.getPool();
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
440
|
+
// Get total count first for logging
|
|
441
|
+
const countResult = await pool.queryWithSwag(`SELECT COUNT(*) as total
|
|
442
|
+
FROM codebase_files
|
|
443
|
+
WHERE project_path = $1 AND content_hash IS NOT NULL`, [this.config.rootPath]);
|
|
444
|
+
const totalCodebaseFiles = parseInt(countResult.rows[0]?.total || '0');
|
|
445
|
+
logger.debug({ totalCodebaseFiles }, 'codebase_files total count');
|
|
446
|
+
// Paginated fetch of codebase_files
|
|
447
|
+
let codebaseOffset = 0;
|
|
448
|
+
let codebaseFetched = 0;
|
|
449
|
+
while (codebaseFetched < MEMORY_LIMIT) {
|
|
450
|
+
const currentPageSize = Math.min(PAGE_SIZE, MEMORY_LIMIT - codebaseFetched);
|
|
451
|
+
const codebaseResult = await pool.queryWithSwag(`SELECT file_path, content_hash
|
|
452
|
+
FROM codebase_files
|
|
453
|
+
WHERE project_path = $1 AND content_hash IS NOT NULL
|
|
454
|
+
ORDER BY file_path
|
|
455
|
+
LIMIT $2 OFFSET $3`, [this.config.rootPath, currentPageSize, codebaseOffset]);
|
|
456
|
+
if (codebaseResult.rows.length === 0) {
|
|
457
|
+
break; // No more rows
|
|
458
|
+
}
|
|
459
|
+
for (const row of codebaseResult.rows) {
|
|
460
|
+
if (row.file_path && !seenPaths.has(row.file_path)) {
|
|
461
|
+
seenPaths.add(row.file_path);
|
|
462
|
+
allFiles.push({
|
|
463
|
+
path: row.file_path,
|
|
464
|
+
hash: row.content_hash || '',
|
|
465
|
+
deleted: false
|
|
466
|
+
});
|
|
467
|
+
}
|
|
321
468
|
}
|
|
469
|
+
codebaseFetched += codebaseResult.rows.length;
|
|
470
|
+
codebaseOffset += codebaseResult.rows.length;
|
|
471
|
+
// If we got fewer than page size, we've reached the end
|
|
472
|
+
if (codebaseResult.rows.length < currentPageSize) {
|
|
473
|
+
break;
|
|
474
|
+
}
|
|
475
|
+
// Yield to event loop between pages
|
|
476
|
+
await new Promise(resolve => setImmediate(resolve));
|
|
322
477
|
}
|
|
323
|
-
logger.debug({ codebaseFilesCount:
|
|
478
|
+
logger.debug({ codebaseFilesCount: codebaseFetched, totalInDb: totalCodebaseFiles }, 'codebase_files scan complete');
|
|
324
479
|
// 2. Also check memories table for file-watcher entries (legacy support)
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
480
|
+
// Use pagination to avoid the old hardcoded 10K limit
|
|
481
|
+
const memoriesCountResult = await pool.queryWithSwag(`SELECT COUNT(*) as total
|
|
482
|
+
FROM memories
|
|
483
|
+
WHERE project_path = $1 AND metadata->>'source' = 'file-watcher'`, [this.config.rootPath]);
|
|
484
|
+
const totalMemories = parseInt(memoriesCountResult.rows[0]?.total || '0');
|
|
485
|
+
logger.debug({ totalMemories }, 'file-watcher memories total count');
|
|
486
|
+
let memoriesOffset = 0;
|
|
487
|
+
let memoriesFetched = 0;
|
|
488
|
+
let memoriesAdded = 0;
|
|
489
|
+
const memoryFetchLimit = MEMORY_LIMIT - codebaseFetched; // Respect overall limit
|
|
490
|
+
while (memoriesFetched < memoryFetchLimit && memoriesFetched < totalMemories) {
|
|
491
|
+
const currentPageSize = Math.min(PAGE_SIZE, memoryFetchLimit - memoriesFetched);
|
|
492
|
+
const results = await this.config.search.textSearch({
|
|
493
|
+
query: 'file-watcher',
|
|
494
|
+
limit: currentPageSize,
|
|
495
|
+
offset: memoriesOffset,
|
|
496
|
+
projectPath: this.config.rootPath
|
|
497
|
+
});
|
|
498
|
+
if (!results || results.length === 0) {
|
|
499
|
+
break; // No more results
|
|
343
500
|
}
|
|
501
|
+
const memoriesData = results
|
|
502
|
+
.filter(m => m.memory.metadata?.source === 'file-watcher')
|
|
503
|
+
.map(m => ({
|
|
504
|
+
path: m.memory.metadata?.filePath || '',
|
|
505
|
+
hash: m.memory.metadata?.contentHash || '',
|
|
506
|
+
deleted: m.memory.metadata?.deleted === true
|
|
507
|
+
}))
|
|
508
|
+
.filter(m => m.path);
|
|
509
|
+
// Merge memories entries (avoiding duplicates)
|
|
510
|
+
for (const mem of memoriesData) {
|
|
511
|
+
if (!seenPaths.has(mem.path)) {
|
|
512
|
+
seenPaths.add(mem.path);
|
|
513
|
+
allFiles.push(mem);
|
|
514
|
+
memoriesAdded++;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
memoriesFetched += results.length;
|
|
518
|
+
memoriesOffset += results.length;
|
|
519
|
+
// If we got fewer than page size, we've reached the end
|
|
520
|
+
if (results.length < currentPageSize) {
|
|
521
|
+
break;
|
|
522
|
+
}
|
|
523
|
+
// Yield to event loop between pages
|
|
524
|
+
await new Promise(resolve => setImmediate(resolve));
|
|
344
525
|
}
|
|
345
526
|
logger.debug({
|
|
346
527
|
totalCount: allFiles.length,
|
|
347
|
-
fromCodebaseFiles:
|
|
348
|
-
fromMemories:
|
|
528
|
+
fromCodebaseFiles: codebaseFetched,
|
|
529
|
+
fromMemories: memoriesAdded,
|
|
530
|
+
totalMemoriesInDb: totalMemories,
|
|
531
|
+
totalCodebaseInDb: totalCodebaseFiles,
|
|
532
|
+
memoryLimit: MEMORY_LIMIT,
|
|
533
|
+
pageSize: PAGE_SIZE
|
|
349
534
|
}, 'MCP scan complete');
|
|
350
535
|
return allFiles;
|
|
351
536
|
}
|
|
Binary file
|