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
package/dist/index.js
CHANGED
|
@@ -286,6 +286,12 @@ class LocalEmbeddingProvider {
|
|
|
286
286
|
socketConnected = false;
|
|
287
287
|
socketReconnecting = false;
|
|
288
288
|
pendingRequests = new Map();
|
|
289
|
+
// FIX Issue #1: Track active sockets to prevent FD leaks over 24h+ sessions
|
|
290
|
+
// Each entry: { socket, createdAt, label }
|
|
291
|
+
activeSockets = new Set();
|
|
292
|
+
_socketCleanupInterval = null;
|
|
293
|
+
// FIX Issue #6: Track pending request ages for stale cleanup
|
|
294
|
+
_pendingCleanupInterval = null;
|
|
289
295
|
// Timeout values - now centralized in config/embeddingTimeouts.ts
|
|
290
296
|
// Set SPECMEM_EMBEDDING_TIMEOUT (in seconds) to control ALL timeouts at once
|
|
291
297
|
// See config/embeddingTimeouts.ts for full documentation
|
|
@@ -327,6 +333,131 @@ class LocalEmbeddingProvider {
|
|
|
327
333
|
}
|
|
328
334
|
// Initialize persistent socket connection
|
|
329
335
|
this.initPersistentSocket();
|
|
336
|
+
// FIX Issue #1: Start periodic socket FD cleanup
|
|
337
|
+
this._startSocketCleanup();
|
|
338
|
+
// FIX Issue #6: Start periodic stale pendingRequests cleanup
|
|
339
|
+
this._startPendingRequestsCleanup();
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* FIX Issue #1: Track a socket in the active set for FD leak prevention.
|
|
343
|
+
* Every socket created via createConnection() MUST be registered here.
|
|
344
|
+
*/
|
|
345
|
+
_trackSocket(socket, label) {
|
|
346
|
+
const entry = { socket, createdAt: Date.now(), label };
|
|
347
|
+
this.activeSockets.add(entry);
|
|
348
|
+
// Auto-remove from tracking when socket is fully closed/destroyed
|
|
349
|
+
const removeFromTracking = () => {
|
|
350
|
+
this.activeSockets.delete(entry);
|
|
351
|
+
};
|
|
352
|
+
socket.once('close', removeFromTracking);
|
|
353
|
+
// If socket is already destroyed, remove immediately
|
|
354
|
+
if (socket.destroyed) {
|
|
355
|
+
this.activeSockets.delete(entry);
|
|
356
|
+
}
|
|
357
|
+
return entry;
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* FIX Issue #1: Periodic cleanup of leaked socket FDs.
|
|
361
|
+
* Destroys sockets that have been open longer than SPECMEM_SOCKET_MAX_AGE_MS.
|
|
362
|
+
* Runs every SPECMEM_SOCKET_CLEANUP_INTERVAL_MS (default 5min).
|
|
363
|
+
*/
|
|
364
|
+
_startSocketCleanup() {
|
|
365
|
+
if (this._socketCleanupInterval) {
|
|
366
|
+
clearInterval(this._socketCleanupInterval);
|
|
367
|
+
}
|
|
368
|
+
const cleanupIntervalMs = parseInt(process.env['SPECMEM_SOCKET_CLEANUP_INTERVAL_MS'] || '300000', 10);
|
|
369
|
+
const maxAgeMs = parseInt(process.env['SPECMEM_SOCKET_MAX_AGE_MS'] || '60000', 10);
|
|
370
|
+
this._socketCleanupInterval = setInterval(() => {
|
|
371
|
+
const now = Date.now();
|
|
372
|
+
let cleaned = 0;
|
|
373
|
+
for (const entry of this.activeSockets) {
|
|
374
|
+
const age = now - entry.createdAt;
|
|
375
|
+
if (age > maxAgeMs) {
|
|
376
|
+
try {
|
|
377
|
+
if (!entry.socket.destroyed) {
|
|
378
|
+
entry.socket.destroy();
|
|
379
|
+
cleaned++;
|
|
380
|
+
logger.debug({
|
|
381
|
+
label: entry.label,
|
|
382
|
+
ageMs: age,
|
|
383
|
+
maxAgeMs
|
|
384
|
+
}, 'LocalEmbeddingProvider: cleaned up stale socket (FD leak prevention)');
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
catch (e) {
|
|
388
|
+
// Ignore destroy errors during cleanup
|
|
389
|
+
}
|
|
390
|
+
this.activeSockets.delete(entry);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
if (cleaned > 0) {
|
|
394
|
+
logger.info({
|
|
395
|
+
cleaned,
|
|
396
|
+
remaining: this.activeSockets.size,
|
|
397
|
+
maxAgeMs,
|
|
398
|
+
cleanupIntervalMs
|
|
399
|
+
}, 'LocalEmbeddingProvider: periodic socket FD cleanup completed');
|
|
400
|
+
}
|
|
401
|
+
}, cleanupIntervalMs);
|
|
402
|
+
// Don't let cleanup interval prevent process exit
|
|
403
|
+
if (this._socketCleanupInterval.unref) {
|
|
404
|
+
this._socketCleanupInterval.unref();
|
|
405
|
+
}
|
|
406
|
+
logger.debug({
|
|
407
|
+
cleanupIntervalMs,
|
|
408
|
+
maxAgeMs
|
|
409
|
+
}, 'LocalEmbeddingProvider: socket FD cleanup started');
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* FIX Issue #6: Periodic cleanup of stale pendingRequests entries.
|
|
413
|
+
* Rejects and removes requests older than SPECMEM_PENDING_REQUEST_MAX_AGE_MS.
|
|
414
|
+
* Runs every SPECMEM_PENDING_CLEANUP_INTERVAL_MS (default 60s).
|
|
415
|
+
*/
|
|
416
|
+
_startPendingRequestsCleanup() {
|
|
417
|
+
if (this._pendingCleanupInterval) {
|
|
418
|
+
clearInterval(this._pendingCleanupInterval);
|
|
419
|
+
}
|
|
420
|
+
const cleanupIntervalMs = parseInt(process.env['SPECMEM_PENDING_CLEANUP_INTERVAL_MS'] || '60000', 10);
|
|
421
|
+
const maxAgeMs = parseInt(process.env['SPECMEM_PENDING_REQUEST_MAX_AGE_MS'] || '120000', 10);
|
|
422
|
+
this._pendingCleanupInterval = setInterval(() => {
|
|
423
|
+
const now = Date.now();
|
|
424
|
+
let cleaned = 0;
|
|
425
|
+
for (const [requestId, pending] of this.pendingRequests) {
|
|
426
|
+
const age = now - (pending.createdAt || 0);
|
|
427
|
+
if (age > maxAgeMs) {
|
|
428
|
+
clearTimeout(pending.timeout);
|
|
429
|
+
this.pendingRequests.delete(requestId);
|
|
430
|
+
cleaned++;
|
|
431
|
+
try {
|
|
432
|
+
pending.reject(new Error(
|
|
433
|
+
`Stale pending request cleaned up after ${Math.round(age / 1000)}s ` +
|
|
434
|
+
`(max age: ${Math.round(maxAgeMs / 1000)}s). ` +
|
|
435
|
+
`Request ID: ${requestId}. ` +
|
|
436
|
+
`Increase SPECMEM_PENDING_REQUEST_MAX_AGE_MS if embeddings are legitimately slow.`
|
|
437
|
+
));
|
|
438
|
+
}
|
|
439
|
+
catch (e) {
|
|
440
|
+
// Ignore rejection errors (promise may already be settled)
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
if (cleaned > 0) {
|
|
445
|
+
logger.warn({
|
|
446
|
+
cleaned,
|
|
447
|
+
remaining: this.pendingRequests.size,
|
|
448
|
+
maxAgeMs,
|
|
449
|
+
cleanupIntervalMs
|
|
450
|
+
}, 'LocalEmbeddingProvider: cleaned up stale pending requests (memory leak prevention)');
|
|
451
|
+
}
|
|
452
|
+
}, cleanupIntervalMs);
|
|
453
|
+
// Don't let cleanup interval prevent process exit
|
|
454
|
+
if (this._pendingCleanupInterval.unref) {
|
|
455
|
+
this._pendingCleanupInterval.unref();
|
|
456
|
+
}
|
|
457
|
+
logger.debug({
|
|
458
|
+
cleanupIntervalMs,
|
|
459
|
+
maxAgeMs
|
|
460
|
+
}, 'LocalEmbeddingProvider: pending requests cleanup started');
|
|
330
461
|
}
|
|
331
462
|
/**
|
|
332
463
|
* Initialize the persistent socket connection
|
|
@@ -392,6 +523,8 @@ class LocalEmbeddingProvider {
|
|
|
392
523
|
logger.debug({ socketPath: this.sandboxSocketPath }, 'LocalEmbeddingProvider: initializing persistent socket');
|
|
393
524
|
try {
|
|
394
525
|
this.persistentSocket = createConnection(this.sandboxSocketPath);
|
|
526
|
+
// FIX Issue #1: Track persistent socket for FD leak prevention
|
|
527
|
+
this._trackSocket(this.persistentSocket, 'persistent');
|
|
395
528
|
__debugLog('[EMBEDDING DEBUG]', Date.now(), 'INIT_PERSISTENT_SOCKET_CONNECTION_CREATED', {
|
|
396
529
|
elapsedMs: Date.now() - methodStart
|
|
397
530
|
});
|
|
@@ -580,6 +713,18 @@ class LocalEmbeddingProvider {
|
|
|
580
713
|
pending.reject(new Error('Socket reset by user'));
|
|
581
714
|
}
|
|
582
715
|
this.pendingRequests.clear();
|
|
716
|
+
// FIX Issue #1: Destroy all tracked active sockets on reset
|
|
717
|
+
for (const entry of this.activeSockets) {
|
|
718
|
+
try {
|
|
719
|
+
if (!entry.socket.destroyed) {
|
|
720
|
+
entry.socket.destroy();
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
catch (e) {
|
|
724
|
+
// Ignore cleanup errors
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
this.activeSockets.clear();
|
|
583
728
|
// Re-detect socket path and reinitialize
|
|
584
729
|
this.sandboxSocketPath = getEmbeddingSocketPath();
|
|
585
730
|
this.initPersistentSocket();
|
|
@@ -1243,6 +1388,41 @@ class LocalEmbeddingProvider {
|
|
|
1243
1388
|
}
|
|
1244
1389
|
catch { /* proceed to spawn */ }
|
|
1245
1390
|
}
|
|
1391
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1392
|
+
// ORPHAN KILL: Kill any existing Frankenstein process BEFORE spawning new one
|
|
1393
|
+
// This prevents multiple processes fighting over the same socket
|
|
1394
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1395
|
+
const pidFile = join(socketDir, 'embedding.pid');
|
|
1396
|
+
try {
|
|
1397
|
+
if (existsSync(pidFile)) {
|
|
1398
|
+
const pidContent = readFileSync(pidFile, 'utf8').trim();
|
|
1399
|
+
const oldPid = parseInt(pidContent.split(':')[0], 10);
|
|
1400
|
+
if (oldPid && !isNaN(oldPid)) {
|
|
1401
|
+
try {
|
|
1402
|
+
process.kill(oldPid, 0); // Check if alive
|
|
1403
|
+
logger.info({ pid: oldPid }, '[LocalEmbeddingProvider] Killing existing embedding process before respawn');
|
|
1404
|
+
process.kill(oldPid, 'SIGTERM');
|
|
1405
|
+
// Give it 2s to die gracefully
|
|
1406
|
+
const killWaitMs = parseInt(process.env['SPECMEM_ORPHAN_KILL_WAIT_MS'] || '2000', 10);
|
|
1407
|
+
await new Promise(r => setTimeout(r, killWaitMs));
|
|
1408
|
+
try {
|
|
1409
|
+
process.kill(oldPid, 0); // Still alive?
|
|
1410
|
+
process.kill(oldPid, 'SIGKILL');
|
|
1411
|
+
logger.warn({ pid: oldPid }, '[LocalEmbeddingProvider] Force killed stubborn embedding process');
|
|
1412
|
+
} catch { /* dead - good */ }
|
|
1413
|
+
} catch { /* not running - fine */ }
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
} catch (pidErr) {
|
|
1417
|
+
logger.debug({ error: pidErr }, '[LocalEmbeddingProvider] PID file cleanup failed (non-fatal)');
|
|
1418
|
+
}
|
|
1419
|
+
// Also remove stale socket file to prevent bind failures
|
|
1420
|
+
if (existsSync(socketPath)) {
|
|
1421
|
+
try {
|
|
1422
|
+
unlinkSync(socketPath);
|
|
1423
|
+
logger.debug('[LocalEmbeddingProvider] Removed stale socket before respawn');
|
|
1424
|
+
} catch { /* ignore */ }
|
|
1425
|
+
}
|
|
1246
1426
|
// Find the embedding server script
|
|
1247
1427
|
// Try multiple locations: SPECMEM_PKG env, relative to this file, project specmem dir
|
|
1248
1428
|
// Docker location via env var (defaults to /opt/specmem if SPECMEM_DOCKER_PATH set)
|
|
@@ -1769,9 +1949,22 @@ class LocalEmbeddingProvider {
|
|
|
1769
1949
|
return new Promise((resolve, reject) => {
|
|
1770
1950
|
const socketPath = getEmbeddingSocketPath();
|
|
1771
1951
|
const socket = createConnection(socketPath);
|
|
1952
|
+
// FIX Issue #1: Track socket for FD leak prevention
|
|
1953
|
+
this._trackSocket(socket, `batch-${texts.length}`);
|
|
1772
1954
|
let buffer = '';
|
|
1773
1955
|
let resolved = false;
|
|
1774
1956
|
const startTime = Date.now();
|
|
1957
|
+
// FIX Issue #1: Ensure socket is destroyed on all exit paths
|
|
1958
|
+
const ensureSocketCleanup = () => {
|
|
1959
|
+
try {
|
|
1960
|
+
if (!socket.destroyed) {
|
|
1961
|
+
socket.destroy();
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1964
|
+
catch (e) {
|
|
1965
|
+
// Ignore cleanup errors
|
|
1966
|
+
}
|
|
1967
|
+
};
|
|
1775
1968
|
// Longer timeout for batches - scale with batch size
|
|
1776
1969
|
const baseTimeout = this.getAdaptiveTimeout();
|
|
1777
1970
|
const timeoutMs = Math.min(baseTimeout * Math.ceil(texts.length / 10), 300000); // Max 5 min
|
|
@@ -1783,7 +1976,7 @@ class LocalEmbeddingProvider {
|
|
|
1783
1976
|
let timeout = setTimeout(() => {
|
|
1784
1977
|
if (!resolved) {
|
|
1785
1978
|
resolved = true;
|
|
1786
|
-
|
|
1979
|
+
ensureSocketCleanup();
|
|
1787
1980
|
reject(new Error(`Batch embedding timeout after ${Math.round(timeoutMs / 1000)}s for ${texts.length} texts`));
|
|
1788
1981
|
}
|
|
1789
1982
|
}, timeoutMs);
|
|
@@ -1803,7 +1996,7 @@ class LocalEmbeddingProvider {
|
|
|
1803
1996
|
timeout = setTimeout(() => {
|
|
1804
1997
|
if (!resolved) {
|
|
1805
1998
|
resolved = true;
|
|
1806
|
-
|
|
1999
|
+
ensureSocketCleanup();
|
|
1807
2000
|
reject(new Error(`Batch embedding idle timeout for ${texts.length} texts`));
|
|
1808
2001
|
}
|
|
1809
2002
|
}, timeoutMs);
|
|
@@ -1830,7 +2023,7 @@ class LocalEmbeddingProvider {
|
|
|
1830
2023
|
// Got actual response - resolve or reject
|
|
1831
2024
|
clearTimeout(timeout);
|
|
1832
2025
|
resolved = true;
|
|
1833
|
-
|
|
2026
|
+
ensureSocketCleanup();
|
|
1834
2027
|
const responseTime = Date.now() - startTime;
|
|
1835
2028
|
this.recordResponseTime(responseTime / texts.length); // Per-text average
|
|
1836
2029
|
if (response.error) {
|
|
@@ -1852,7 +2045,7 @@ class LocalEmbeddingProvider {
|
|
|
1852
2045
|
catch (err) {
|
|
1853
2046
|
clearTimeout(timeout);
|
|
1854
2047
|
resolved = true;
|
|
1855
|
-
|
|
2048
|
+
ensureSocketCleanup();
|
|
1856
2049
|
reject(new Error(`Failed to parse batch embedding response: ${err}`));
|
|
1857
2050
|
}
|
|
1858
2051
|
}
|
|
@@ -1861,6 +2054,7 @@ class LocalEmbeddingProvider {
|
|
|
1861
2054
|
if (!resolved) {
|
|
1862
2055
|
resolved = true;
|
|
1863
2056
|
clearTimeout(timeout);
|
|
2057
|
+
ensureSocketCleanup();
|
|
1864
2058
|
reject(err);
|
|
1865
2059
|
}
|
|
1866
2060
|
});
|
|
@@ -2161,6 +2355,8 @@ class LocalEmbeddingProvider {
|
|
|
2161
2355
|
try {
|
|
2162
2356
|
__debugLog('[EMBEDDING DEBUG]', Date.now(), 'WARMING_SOCKET', { socketPath });
|
|
2163
2357
|
this.warmSocket = createConnection(socketPath);
|
|
2358
|
+
// FIX Issue #1: Track warm socket for FD leak prevention
|
|
2359
|
+
this._trackSocket(this.warmSocket, 'warm');
|
|
2164
2360
|
this.warmSocketPath = socketPath;
|
|
2165
2361
|
this.warmSocket.on('connect', () => {
|
|
2166
2362
|
__debugLog('[EMBEDDING DEBUG]', Date.now(), 'WARM_SOCKET_CONNECTED', { socketPath });
|
|
@@ -2400,10 +2596,23 @@ class LocalEmbeddingProvider {
|
|
|
2400
2596
|
generateWithDirectSocketAttempt(text, socketPath, attempt) {
|
|
2401
2597
|
return new Promise((resolve, reject) => {
|
|
2402
2598
|
const socket = createConnection(socketPath);
|
|
2599
|
+
// FIX Issue #1: Track socket for FD leak prevention
|
|
2600
|
+
this._trackSocket(socket, `direct-attempt-${attempt}`);
|
|
2403
2601
|
let buffer = '';
|
|
2404
2602
|
let resolved = false;
|
|
2405
2603
|
const startTime = Date.now();
|
|
2406
2604
|
const timeoutMs = this.getAdaptiveTimeout();
|
|
2605
|
+
// FIX Issue #1: Ensure socket is destroyed on all exit paths
|
|
2606
|
+
const ensureSocketCleanup = () => {
|
|
2607
|
+
try {
|
|
2608
|
+
if (!socket.destroyed) {
|
|
2609
|
+
socket.destroy();
|
|
2610
|
+
}
|
|
2611
|
+
}
|
|
2612
|
+
catch (e) {
|
|
2613
|
+
// Ignore cleanup errors
|
|
2614
|
+
}
|
|
2615
|
+
};
|
|
2407
2616
|
__debugLog('[EMBEDDING DEBUG]', Date.now(), 'DIRECT_SOCKET_CONNECTING', {
|
|
2408
2617
|
socketPath,
|
|
2409
2618
|
attempt,
|
|
@@ -2414,7 +2623,7 @@ class LocalEmbeddingProvider {
|
|
|
2414
2623
|
let timeout = setTimeout(() => {
|
|
2415
2624
|
if (!resolved) {
|
|
2416
2625
|
resolved = true;
|
|
2417
|
-
|
|
2626
|
+
ensureSocketCleanup(); // FIX Issue #1: Use cleanup helper
|
|
2418
2627
|
__debugLog('[EMBEDDING DEBUG]', Date.now(), 'DIRECT_SOCKET_INITIAL_TIMEOUT', {
|
|
2419
2628
|
socketPath,
|
|
2420
2629
|
attempt,
|
|
@@ -2443,7 +2652,7 @@ class LocalEmbeddingProvider {
|
|
|
2443
2652
|
timeout = setTimeout(() => {
|
|
2444
2653
|
if (!resolved) {
|
|
2445
2654
|
resolved = true;
|
|
2446
|
-
|
|
2655
|
+
ensureSocketCleanup(); // FIX Issue #1: Use cleanup helper
|
|
2447
2656
|
__debugLog('[EMBEDDING DEBUG]', Date.now(), 'DIRECT_SOCKET_IDLE_TIMEOUT', {
|
|
2448
2657
|
socketPath,
|
|
2449
2658
|
attempt,
|
|
@@ -2500,14 +2709,14 @@ class LocalEmbeddingProvider {
|
|
|
2500
2709
|
else {
|
|
2501
2710
|
reject(new Error(`Invalid response from embedding service (socket: ${socketPath})`));
|
|
2502
2711
|
}
|
|
2503
|
-
|
|
2712
|
+
ensureSocketCleanup(); // FIX Issue #1: destroy instead of end
|
|
2504
2713
|
return;
|
|
2505
2714
|
}
|
|
2506
2715
|
catch (err) {
|
|
2507
2716
|
clearTimeout(timeout);
|
|
2508
2717
|
resolved = true;
|
|
2509
2718
|
reject(new Error(`Failed to parse embedding response: ${err instanceof Error ? err.message : err} (socket: ${socketPath})`));
|
|
2510
|
-
|
|
2719
|
+
ensureSocketCleanup(); // FIX Issue #1: destroy instead of end
|
|
2511
2720
|
return;
|
|
2512
2721
|
}
|
|
2513
2722
|
}
|
|
@@ -2521,6 +2730,7 @@ class LocalEmbeddingProvider {
|
|
|
2521
2730
|
});
|
|
2522
2731
|
if (!resolved) {
|
|
2523
2732
|
resolved = true;
|
|
2733
|
+
ensureSocketCleanup(); // FIX Issue #1: Ensure socket destroyed on error
|
|
2524
2734
|
reject(new Error(`Socket error: ${err.message} (socket: ${socketPath})`));
|
|
2525
2735
|
}
|
|
2526
2736
|
});
|
|
@@ -2698,7 +2908,8 @@ class LocalEmbeddingProvider {
|
|
|
2698
2908
|
resolve(embedding);
|
|
2699
2909
|
},
|
|
2700
2910
|
reject,
|
|
2701
|
-
timeout
|
|
2911
|
+
timeout,
|
|
2912
|
+
createdAt: Date.now() // FIX Issue #6: Track creation time for stale cleanup
|
|
2702
2913
|
});
|
|
2703
2914
|
// Send request with ID so we can match responses
|
|
2704
2915
|
const request = JSON.stringify({ type: 'embed', text, requestId }) + '\n';
|
|
@@ -2765,15 +2976,28 @@ class LocalEmbeddingProvider {
|
|
|
2765
2976
|
generateWithNewSocketAttempt(text, attempt) {
|
|
2766
2977
|
return new Promise((resolve, reject) => {
|
|
2767
2978
|
const socket = createConnection(this.sandboxSocketPath);
|
|
2979
|
+
// FIX Issue #1: Track socket for FD leak prevention
|
|
2980
|
+
this._trackSocket(socket, `new-attempt-${attempt}`);
|
|
2768
2981
|
let buffer = '';
|
|
2769
2982
|
let resolved = false;
|
|
2770
2983
|
const startTime = Date.now();
|
|
2771
2984
|
const timeoutMs = this.getAdaptiveTimeout();
|
|
2985
|
+
// FIX Issue #1: Ensure socket is destroyed on all exit paths
|
|
2986
|
+
const ensureSocketCleanup = () => {
|
|
2987
|
+
try {
|
|
2988
|
+
if (!socket.destroyed) {
|
|
2989
|
+
socket.destroy();
|
|
2990
|
+
}
|
|
2991
|
+
}
|
|
2992
|
+
catch (e) {
|
|
2993
|
+
// Ignore cleanup errors
|
|
2994
|
+
}
|
|
2995
|
+
};
|
|
2772
2996
|
// IDLE-BASED TIMEOUT: Resets on any data received
|
|
2773
2997
|
let timeout = setTimeout(() => {
|
|
2774
2998
|
if (!resolved) {
|
|
2775
2999
|
resolved = true;
|
|
2776
|
-
|
|
3000
|
+
ensureSocketCleanup();
|
|
2777
3001
|
reject(new Error(`Embedding idle timeout after ${Math.round(timeoutMs / 1000)}s of no activity ` +
|
|
2778
3002
|
`(attempt ${attempt}/${LocalEmbeddingProvider.SOCKET_MAX_RETRIES}). ` +
|
|
2779
3003
|
`Socket: ${this.sandboxSocketPath}. ` +
|
|
@@ -2791,7 +3015,7 @@ class LocalEmbeddingProvider {
|
|
|
2791
3015
|
timeout = setTimeout(() => {
|
|
2792
3016
|
if (!resolved) {
|
|
2793
3017
|
resolved = true;
|
|
2794
|
-
|
|
3018
|
+
ensureSocketCleanup();
|
|
2795
3019
|
reject(new Error(`Embedding idle timeout after ${Math.round(timeoutMs / 1000)}s of no activity ` +
|
|
2796
3020
|
`(attempt ${attempt}/${LocalEmbeddingProvider.SOCKET_MAX_RETRIES}). ` +
|
|
2797
3021
|
`Socket: ${this.sandboxSocketPath}. ` +
|
|
@@ -2825,14 +3049,14 @@ class LocalEmbeddingProvider {
|
|
|
2825
3049
|
else {
|
|
2826
3050
|
reject(new Error(`Invalid response from embedding service (socket: ${this.sandboxSocketPath})`));
|
|
2827
3051
|
}
|
|
2828
|
-
|
|
3052
|
+
ensureSocketCleanup(); // FIX Issue #1: destroy instead of end
|
|
2829
3053
|
return;
|
|
2830
3054
|
}
|
|
2831
3055
|
catch (err) {
|
|
2832
3056
|
clearTimeout(timeout);
|
|
2833
3057
|
resolved = true;
|
|
2834
3058
|
reject(new Error(`Failed to parse embedding response: ${err instanceof Error ? err.message : err} (socket: ${this.sandboxSocketPath})`));
|
|
2835
|
-
|
|
3059
|
+
ensureSocketCleanup(); // FIX Issue #1: destroy instead of end
|
|
2836
3060
|
return;
|
|
2837
3061
|
}
|
|
2838
3062
|
}
|
|
@@ -2841,6 +3065,7 @@ class LocalEmbeddingProvider {
|
|
|
2841
3065
|
clearTimeout(timeout);
|
|
2842
3066
|
if (!resolved) {
|
|
2843
3067
|
resolved = true;
|
|
3068
|
+
ensureSocketCleanup(); // FIX Issue #1: Ensure socket destroyed on error
|
|
2844
3069
|
reject(new Error(`Socket error: ${err.message} (socket: ${this.sandboxSocketPath})`));
|
|
2845
3070
|
}
|
|
2846
3071
|
});
|
|
@@ -141,8 +141,8 @@ SPECMEM_DB_PASSWORD=specmem_westayunprofessional
|
|
|
141
141
|
SPECMEM_COORDINATION_ENABLED=true
|
|
142
142
|
SPECMEM_COORDINATION_PORT=3001
|
|
143
143
|
|
|
144
|
-
# Watcher Configuration
|
|
145
|
-
SPECMEM_WATCHER_ENABLED=
|
|
144
|
+
# Watcher Configuration
|
|
145
|
+
SPECMEM_WATCHER_ENABLED=true
|
|
146
146
|
SPECMEM_WATCHER_ROOT_PATH=
|
|
147
147
|
SPECMEM_WATCHER_IGNORE_PATH=
|
|
148
148
|
|