specmem-hardwicksoftware 3.7.7 → 3.7.9

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.
@@ -189,23 +189,55 @@ export class EmbeddingServerManager extends EventEmitter {
189
189
  logger.info({ count: runningServers.length, pids: runningServers.map(s => s.pid) },
190
190
  '[EmbeddingServerManager] FIX 1B: Found existing embedding server processes, checking health');
191
191
  // Test if any existing process is healthy via socket
192
+ // CRITICAL FIX: Retry health check up to 15s to give freshly-spawned servers time to warm up
193
+ // This prevents MCP bootstrap from killing servers that init just spawned
192
194
  if (existsSync(this.socketPath)) {
193
- const healthResult = await this.healthCheck();
194
- if (healthResult.success) {
195
- logger.info({
196
- pids: runningServers.map(s => s.pid),
197
- responseTimeMs: healthResult.responseTimeMs,
198
- }, '[EmbeddingServerManager] FIX 1B: Existing server is healthy - reusing instead of spawning new');
199
- this.isRunning = true;
200
- this.startTime = Date.now();
201
- this.consecutiveFailures = 0;
202
- return true;
195
+ for (let attempt = 0; attempt < 6; attempt++) {
196
+ const healthResult = await this.healthCheck();
197
+ if (healthResult.success) {
198
+ logger.info({
199
+ pids: runningServers.map(s => s.pid),
200
+ responseTimeMs: healthResult.responseTimeMs,
201
+ attempt,
202
+ }, '[EmbeddingServerManager] FIX 1B: Existing server is healthy - reusing instead of spawning new');
203
+ this.isRunning = true;
204
+ this.startTime = Date.now();
205
+ this.consecutiveFailures = 0;
206
+ return true;
207
+ }
208
+ if (attempt < 5) {
209
+ logger.debug({ attempt }, '[EmbeddingServerManager] FIX 1B: Health check failed, waiting 3s before retry...');
210
+ await this.sleep(3000);
211
+ // Re-check socket still exists
212
+ if (!existsSync(this.socketPath)) break;
213
+ }
214
+ }
215
+ } else {
216
+ // Socket doesn't exist yet but processes are running - wait for socket to appear
217
+ logger.debug('[EmbeddingServerManager] FIX 1B: No socket yet, waiting up to 15s for server to create it');
218
+ for (let i = 0; i < 15; i++) {
219
+ await this.sleep(1000);
220
+ if (existsSync(this.socketPath)) {
221
+ const healthResult = await this.healthCheck();
222
+ if (healthResult.success) {
223
+ logger.info({
224
+ pids: runningServers.map(s => s.pid),
225
+ responseTimeMs: healthResult.responseTimeMs,
226
+ }, '[EmbeddingServerManager] FIX 1B: Server became healthy after waiting - reusing');
227
+ this.isRunning = true;
228
+ this.startTime = Date.now();
229
+ this.consecutiveFailures = 0;
230
+ return true;
231
+ }
232
+ }
203
233
  }
204
234
  }
205
- // Existing processes found but unhealthy - kill them all before starting fresh
206
- logger.warn({ count: runningServers.length },
207
- '[EmbeddingServerManager] FIX 1B: Existing processes are unhealthy, killing all before fresh start');
208
- for (const server of runningServers) {
235
+ // Existing processes found but unhealthy after retries - kill only THIS project's before starting fresh
236
+ // PROJECT ISOLATION: Filter to only processes belonging to this project
237
+ const thisProjectServers = runningServers.filter(s => this._isProcessForThisProject(s.pid));
238
+ logger.warn({ count: thisProjectServers.length, totalFound: runningServers.length },
239
+ '[EmbeddingServerManager] FIX 1B: Existing processes are unhealthy, killing this project\'s before fresh start');
240
+ for (const server of thisProjectServers) {
209
241
  try {
210
242
  process.kill(server.pid, 'SIGTERM');
211
243
  logger.info({ pid: server.pid }, '[EmbeddingServerManager] FIX 1B: Sent SIGTERM to unhealthy server');
@@ -217,7 +249,7 @@ export class EmbeddingServerManager extends EventEmitter {
217
249
  // Wait 2 seconds for processes to terminate
218
250
  await this.sleep(2000);
219
251
  // Force kill any survivors
220
- for (const server of runningServers) {
252
+ for (const server of thisProjectServers) {
221
253
  try {
222
254
  process.kill(server.pid, 0); // Check if alive
223
255
  process.kill(server.pid, 'SIGKILL');
@@ -367,19 +399,27 @@ export class EmbeddingServerManager extends EventEmitter {
367
399
  }
368
400
  }
369
401
  // 2. Kill via pgrep as fallback (catches processes without PID files)
402
+ // PROJECT ISOLATION: Only kill processes belonging to THIS project
370
403
  try {
371
404
  const { execSync: execSyncLocal } = await import('child_process');
372
405
  const pids = execSyncLocal(`pgrep -f "frankenstein-embeddings.py" 2>/dev/null || true`, { encoding: 'utf8' }).trim().split('\n').filter(Boolean);
406
+ let killedAny = false;
373
407
  for (const pidStr of pids) {
374
408
  const pid = parseInt(pidStr, 10);
375
409
  if (pid && pid !== process.pid) {
410
+ // PROJECT ISOLATION: Skip processes belonging to other projects
411
+ if (!this._isProcessForThisProject(pid)) {
412
+ logger.debug({ pid }, '[EmbeddingServerManager] Pre-spawn: Skipping process belonging to another project');
413
+ continue;
414
+ }
376
415
  try {
377
416
  process.kill(pid, 'SIGTERM');
417
+ killedAny = true;
378
418
  logger.info({ pid }, '[EmbeddingServerManager] Killed orphan frankenstein process (pgrep)');
379
419
  } catch { /* already dead */ }
380
420
  }
381
421
  }
382
- if (pids.filter(Boolean).length > 0) {
422
+ if (killedAny) {
383
423
  await this.sleep(killWaitMs);
384
424
  }
385
425
  } catch { /* pgrep not available or no matches */ }
@@ -841,18 +881,20 @@ export class EmbeddingServerManager extends EventEmitter {
841
881
  }
842
882
  // Also try killing by PID file (in case process reference was lost)
843
883
  await this.killByPidFile();
844
- // FIX: Kill ALL running embedding server processes (orphans from previous sessions/subagents)
884
+ // FIX: Kill remaining embedding server processes for THIS project (orphans from previous sessions/subagents)
885
+ // PROJECT ISOLATION: Only kill processes belonging to this project, not other projects
845
886
  const allServers = this.findRunningEmbeddingServers();
846
- if (allServers.length > 0) {
847
- logger.info({ count: allServers.length, pids: allServers.map(s => s.pid) },
848
- '[EmbeddingServerManager] Killing all remaining embedding server processes');
849
- for (const server of allServers) {
887
+ const thisProjectStopServers = allServers.filter(s => this._isProcessForThisProject(s.pid));
888
+ if (thisProjectStopServers.length > 0) {
889
+ logger.info({ count: thisProjectStopServers.length, totalFound: allServers.length, pids: thisProjectStopServers.map(s => s.pid) },
890
+ '[EmbeddingServerManager] Killing remaining embedding server processes for THIS project');
891
+ for (const server of thisProjectStopServers) {
850
892
  try {
851
893
  process.kill(server.pid, 'SIGTERM');
852
894
  } catch { /* already dead */ }
853
895
  }
854
896
  await this.sleep(2000);
855
- for (const server of allServers) {
897
+ for (const server of thisProjectStopServers) {
856
898
  try {
857
899
  process.kill(server.pid, 0);
858
900
  process.kill(server.pid, 'SIGKILL');
@@ -1410,7 +1452,7 @@ export class EmbeddingServerManager extends EventEmitter {
1410
1452
  // CRITICAL FIX: Also search for PID files in ANY project path
1411
1453
  // This catches project-specific socket directories like /newServer/specmem/sockets/
1412
1454
  try {
1413
- const allProjectPids = execSync(`find / -path "*/specmem/sockets/embedding.pid" -o -path "*/.specmem/*/sockets/embedding.pid" 2>/dev/null | xargs cat 2>/dev/null || true`, { encoding: 'utf8', timeout: 5000 }).trim();
1455
+ const allProjectPids = execSync(`find /home /root /opt /srv /var /tmp -maxdepth 6 -path "*/specmem/sockets/embedding.pid" -o -path "*/.specmem/*/sockets/embedding.pid" 2>/dev/null | xargs cat 2>/dev/null || true`, { encoding: 'utf8', timeout: 5000 }).trim();
1414
1456
  for (const line of allProjectPids.split('\n')) {
1415
1457
  if (!line.trim())
1416
1458
  continue;
@@ -1533,6 +1575,34 @@ export class EmbeddingServerManager extends EventEmitter {
1533
1575
  return null;
1534
1576
  }
1535
1577
  }
1578
+ /**
1579
+ * Check if a running process belongs to THIS project instance.
1580
+ * Reads /proc/PID/environ and checks SPECMEM_PROJECT_PATH, SPECMEM_SOCKET_PATH,
1581
+ * and SPECMEM_EMBEDDING_SOCKET for a match against this project's paths.
1582
+ * Returns true if the process belongs to this project, false otherwise.
1583
+ * Returns false on any error (permission denied, process gone, etc.)
1584
+ */
1585
+ _isProcessForThisProject(pid) {
1586
+ try {
1587
+ const environPath = `/proc/${pid}/environ`;
1588
+ if (!existsSync(environPath)) {
1589
+ return false;
1590
+ }
1591
+ const environ = readFileSync(environPath, 'utf8');
1592
+ const envVars = environ.split('\0');
1593
+ const projectPath = this.projectPath || process.cwd();
1594
+ const socketPath = this.socketPath;
1595
+ for (const v of envVars) {
1596
+ if (v.startsWith('SPECMEM_PROJECT_PATH=') && v.includes(projectPath)) return true;
1597
+ if (v.startsWith('SPECMEM_SOCKET_PATH=') && v.includes(projectPath)) return true;
1598
+ if (v.startsWith('SPECMEM_EMBEDDING_SOCKET=') && socketPath && v.includes(socketPath)) return true;
1599
+ }
1600
+ return false;
1601
+ }
1602
+ catch {
1603
+ return false;
1604
+ }
1605
+ }
1536
1606
  /**
1537
1607
  * Get process info for an orphaned process (no PID file)
1538
1608
  */
@@ -1957,19 +2027,23 @@ export class EmbeddingServerManager extends EventEmitter {
1957
2027
  }
1958
2028
  }
1959
2029
  // FIX 4: Duplicate process detection during health monitoring
1960
- // Check for multiple embedding server processes and kill extras
2030
+ // Check for multiple embedding server processes FOR THIS PROJECT and kill extras
2031
+ // PROJECT ISOLATION: Filter to only this project's processes before killing duplicates
1961
2032
  try {
1962
2033
  const runningServers = this.findRunningEmbeddingServers();
1963
- if (runningServers.length > 1) {
2034
+ // PROJECT ISOLATION: Only consider processes belonging to this project
2035
+ const thisProjectHealthServers = runningServers.filter(s => this._isProcessForThisProject(s.pid));
2036
+ if (thisProjectHealthServers.length > 1) {
1964
2037
  logger.error({
1965
- count: runningServers.length,
1966
- pids: runningServers.map(s => s.pid),
1967
- }, '[EmbeddingServerManager] CRITICAL: Multiple embedding server processes detected!');
2038
+ count: thisProjectHealthServers.length,
2039
+ totalSystemWide: runningServers.length,
2040
+ pids: thisProjectHealthServers.map(s => s.pid),
2041
+ }, '[EmbeddingServerManager] CRITICAL: Multiple embedding server processes detected for THIS project!');
1968
2042
  // Determine the legitimate PID from PID file
1969
2043
  const legitimatePid = this.readPidFile();
1970
2044
  if (legitimatePid) {
1971
- // Kill all processes that are NOT the legitimate one
1972
- for (const server of runningServers) {
2045
+ // Kill all THIS PROJECT's processes that are NOT the legitimate one
2046
+ for (const server of thisProjectHealthServers) {
1973
2047
  if (server.pid !== legitimatePid) {
1974
2048
  try {
1975
2049
  logger.warn({ pid: server.pid, legitimatePid },
@@ -1994,12 +2068,12 @@ export class EmbeddingServerManager extends EventEmitter {
1994
2068
  }
1995
2069
  }
1996
2070
  else {
1997
- // No PID file - keep the first one, kill the rest
2071
+ // No PID file - keep the first one, kill the rest (only this project's processes)
1998
2072
  logger.warn('[EmbeddingServerManager] FIX 4: No PID file found, keeping oldest process');
1999
- for (let i = 1; i < runningServers.length; i++) {
2073
+ for (let i = 1; i < thisProjectHealthServers.length; i++) {
2000
2074
  try {
2001
- process.kill(runningServers[i].pid, 'SIGTERM');
2002
- logger.warn({ pid: runningServers[i].pid },
2075
+ process.kill(thisProjectHealthServers[i].pid, 'SIGTERM');
2076
+ logger.warn({ pid: thisProjectHealthServers[i].pid },
2003
2077
  '[EmbeddingServerManager] FIX 4: Killing extra duplicate process');
2004
2078
  }
2005
2079
  catch { /* ignore */ }
@@ -150,7 +150,7 @@ import signal
150
150
  import sys
151
151
 
152
152
  # Fix BrokenPipeError when parent process dies - ignore SIGPIPE
153
- signal.signal(signal.SIGPIPE, signal.SIG_DFL)
153
+ signal.signal(signal.SIGPIPE, signal.SIG_IGN)
154
154
 
155
155
  def _safe_print(msg, file=None):
156
156
  """Print that ignores BrokenPipeError when parent dies"""
@@ -132,8 +132,19 @@ cleanup_stale() {
132
132
 
133
133
  # If socket exists but container not running, remove orphan socket
134
134
  if [ -S "$SOCKET_PATH" ] && [ "$(get_container_state)" != "running" ] && [ "$(get_container_state)" != "paused" ]; then
135
- log "Removing orphaned socket"
136
- rm -f "$SOCKET_PATH"
135
+ # Check if a native Python embedding server owns this socket before removing
136
+ NATIVE_PID=""
137
+ PID_FILE="$(dirname "$SOCKET_PATH")/embedding.pid"
138
+ if [ -f "$PID_FILE" ]; then
139
+ NATIVE_PID=$(cut -d: -f1 "$PID_FILE" 2>/dev/null)
140
+ fi
141
+ if [ -n "$NATIVE_PID" ] && kill -0 "$NATIVE_PID" 2>/dev/null; then
142
+ log "Socket owned by native Python server (PID $NATIVE_PID) - NOT removing"
143
+ else
144
+ log "Removing orphaned socket (no Docker container, no native server)"
145
+ rm -f "$SOCKET_PATH"
146
+ [ -f "$PID_FILE" ] && rm -f "$PID_FILE"
147
+ fi
137
148
  fi
138
149
  }
139
150
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "specmem-hardwicksoftware",
3
- "version": "3.7.7",
3
+ "version": "3.7.9",
4
4
  "type": "module",
5
5
  "description": "Persistent memory system for coding sessions - semantic search with pgvector, token compression, team coordination, file watching. Needs root: installs system-wide hooks, manages docker/PostgreSQL, writes global configs, handles screen sessions. justcalljon.pro",
6
6
  "main": "dist/index.js",
@@ -3087,6 +3087,39 @@ async function indexCodebase(projectPath, ui, embeddingResult) {
3087
3087
  const serverRunning = embeddingResult?.serverRunning || false;
3088
3088
  const socketExists = activeSocketPath !== null;
3089
3089
 
3090
+ // Helper: Kill any existing embedding server for this project before spawning a new one
3091
+ function killExistingEmbeddingServer(projectPath) {
3092
+ const pidFile = path.join(projectPath, 'specmem', 'sockets', 'embedding.pid');
3093
+ try {
3094
+ if (!fs.existsSync(pidFile)) return false;
3095
+ const content = fs.readFileSync(pidFile, 'utf8').trim();
3096
+ const pid = parseInt(content.split(':')[0], 10);
3097
+ if (!pid || isNaN(pid)) return false;
3098
+ // Check if process is alive
3099
+ try { process.kill(pid, 0); } catch {
3100
+ // Process dead, clean up PID file
3101
+ try { fs.unlinkSync(pidFile); } catch {}
3102
+ return false;
3103
+ }
3104
+ // Process alive — kill it gracefully
3105
+ initLog(`[EMBED] Killing existing embedding server PID ${pid} before respawn`);
3106
+ process.kill(pid, 'SIGTERM');
3107
+ // Wait up to 3 seconds for it to die
3108
+ for (let i = 0; i < 6; i++) {
3109
+ const { execSync } = require('child_process');
3110
+ try { execSync(`sleep 0.5`); } catch {}
3111
+ try { process.kill(pid, 0); } catch { return true; } // Dead
3112
+ }
3113
+ // Force kill if still alive
3114
+ try { process.kill(pid, 'SIGKILL'); } catch {}
3115
+ try { fs.unlinkSync(pidFile); } catch {}
3116
+ return true;
3117
+ } catch (e) {
3118
+ initLog(`[EMBED] PID file check error: ${e.message}`);
3119
+ return false;
3120
+ }
3121
+ }
3122
+
3090
3123
  // CRITICAL: If no embedding server, START IT NOW - never skip embeddings!
3091
3124
  if (!serverRunning && !socketExists) {
3092
3125
  ui.setStatus('Starting embedding server...');
@@ -3116,6 +3149,9 @@ async function indexCodebase(projectPath, ui, embeddingResult) {
3116
3149
  const socketsDir = path.join(projectPath, 'specmem', 'sockets');
3117
3150
  safeMkdir(socketsDir);
3118
3151
 
3152
+ // Kill any existing embedding server before spawning a new one
3153
+ killExistingEmbeddingServer(projectPath);
3154
+
3119
3155
  // Clean up stale socket
3120
3156
  if (fs.existsSync(projectSocketPath)) {
3121
3157
  try {
@@ -3238,6 +3274,9 @@ async function indexCodebase(projectPath, ui, embeddingResult) {
3238
3274
  initLog('Attempting socket recovery - cleaning stale socket and restarting server...');
3239
3275
  ui.setStatus('Recovering embedding server...');
3240
3276
 
3277
+ // Kill any existing embedding server before recovery
3278
+ killExistingEmbeddingServer(projectPath);
3279
+
3241
3280
  // Clean up the stale socket file
3242
3281
  try {
3243
3282
  if (fs.existsSync(projectSocketPath)) {
@@ -3516,6 +3555,10 @@ async function indexCodebase(projectPath, ui, embeddingResult) {
3516
3555
  let consecutiveEmbeddingFailures = 0;
3517
3556
  const MAX_CONSECUTIVE_FAILURES = 3;
3518
3557
 
3558
+ // Respawn backoff for revalidateSocket() — prevents rapid-fire restarts
3559
+ let lastRevalidateTime = 0;
3560
+ let revalidateBackoffMs = 1000; // Start at 1s, doubles each time up to 30s
3561
+
3519
3562
  // FIX: Categorize embedding errors for better debugging and error tracking
3520
3563
  function categorizeEmbeddingError(error) {
3521
3564
  const msg = (error && error.message) ? error.message.toLowerCase() : '';
@@ -3598,6 +3641,16 @@ async function indexCodebase(projectPath, ui, embeddingResult) {
3598
3641
 
3599
3642
  // Revalidate socket path after failures (may have been restarted externally)
3600
3643
  async function revalidateSocket() {
3644
+ // Backoff: prevent rapid-fire revalidation/respawn loops
3645
+ const now = Date.now();
3646
+ const timeSinceLastRevalidate = now - lastRevalidateTime;
3647
+ if (timeSinceLastRevalidate < revalidateBackoffMs) {
3648
+ initLog(`[EMBED] Revalidation throttled (${revalidateBackoffMs}ms backoff, ${timeSinceLastRevalidate}ms elapsed)`);
3649
+ return false;
3650
+ }
3651
+ lastRevalidateTime = now;
3652
+ revalidateBackoffMs = Math.min(revalidateBackoffMs * 2, 30000);
3653
+
3601
3654
  const projectSocketPath = path.join(projectPath, 'specmem', 'sockets', 'embeddings.sock');
3602
3655
  const projDirName = path.basename(projectPath).replace(/[^a-zA-Z0-9_-]/g, '_');
3603
3656
  const sharedSocketPath = path.join(os.homedir(), '.specmem', projDirName, 'sockets', 'embeddings.sock');
@@ -3609,6 +3662,7 @@ async function indexCodebase(projectPath, ui, embeddingResult) {
3609
3662
  if (healthy) {
3610
3663
  initLog(`Revalidated socket: using project socket at ${projectSocketPath}`);
3611
3664
  consecutiveEmbeddingFailures = 0;
3665
+ revalidateBackoffMs = 1000;
3612
3666
  return true;
3613
3667
  }
3614
3668
  }
@@ -3620,6 +3674,7 @@ async function indexCodebase(projectPath, ui, embeddingResult) {
3620
3674
  if (healthy) {
3621
3675
  initLog(`Revalidated socket: using shared socket at ${sharedSocketPath}`);
3622
3676
  consecutiveEmbeddingFailures = 0;
3677
+ revalidateBackoffMs = 1000;
3623
3678
  return true;
3624
3679
  }
3625
3680
  }
@@ -3633,6 +3688,7 @@ async function indexCodebase(projectPath, ui, embeddingResult) {
3633
3688
  if (healthy) {
3634
3689
  initLog(`Revalidated socket: using /tmp shared socket at ${tmpSocket}`);
3635
3690
  consecutiveEmbeddingFailures = 0;
3691
+ revalidateBackoffMs = 1000;
3636
3692
  return true;
3637
3693
  }
3638
3694
  }
@@ -3654,6 +3710,8 @@ async function indexCodebase(projectPath, ui, embeddingResult) {
3654
3710
  if (embeddingScript) {
3655
3711
  const socketsDir = path.join(projectPath, 'specmem', 'sockets');
3656
3712
  if (!fs.existsSync(socketsDir)) fs.mkdirSync(socketsDir, { recursive: true });
3713
+ // Kill any existing embedding server before respawn
3714
+ killExistingEmbeddingServer(projectPath);
3657
3715
  // Clean stale socket
3658
3716
  if (fs.existsSync(projectSocketPath)) {
3659
3717
  try { fs.unlinkSync(projectSocketPath); } catch { /* ignore */ }
@@ -3681,6 +3739,7 @@ async function indexCodebase(projectPath, ui, embeddingResult) {
3681
3739
  if (healthy) {
3682
3740
  initLog(`Embedding server auto-restarted successfully, socket at ${projectSocketPath}`);
3683
3741
  consecutiveEmbeddingFailures = 0;
3742
+ revalidateBackoffMs = 1000;
3684
3743
  return true;
3685
3744
  }
3686
3745
  }
@@ -4858,6 +4917,9 @@ async function extractSessions(projectPath, ui, embeddingResult = null) {
4858
4917
  const socketsDir = path.join(projectPath, 'specmem', 'sockets');
4859
4918
  safeMkdir(socketsDir);
4860
4919
 
4920
+ // Kill any existing embedding server before spawning for session extraction
4921
+ killExistingEmbeddingServer(projectPath);
4922
+
4861
4923
  // Clean up stale socket
4862
4924
  if (fs.existsSync(projectSocketPath)) {
4863
4925
  try {