specmem-hardwicksoftware 3.7.6 → 3.7.8

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.
@@ -202,10 +202,12 @@ export class EmbeddingServerManager extends EventEmitter {
202
202
  return true;
203
203
  }
204
204
  }
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) {
205
+ // Existing processes found but unhealthy - kill only THIS project's before starting fresh
206
+ // PROJECT ISOLATION: Filter to only processes belonging to this project
207
+ const thisProjectServers = runningServers.filter(s => this._isProcessForThisProject(s.pid));
208
+ logger.warn({ count: thisProjectServers.length, totalFound: runningServers.length },
209
+ '[EmbeddingServerManager] FIX 1B: Existing processes are unhealthy, killing this project\'s before fresh start');
210
+ for (const server of thisProjectServers) {
209
211
  try {
210
212
  process.kill(server.pid, 'SIGTERM');
211
213
  logger.info({ pid: server.pid }, '[EmbeddingServerManager] FIX 1B: Sent SIGTERM to unhealthy server');
@@ -217,7 +219,7 @@ export class EmbeddingServerManager extends EventEmitter {
217
219
  // Wait 2 seconds for processes to terminate
218
220
  await this.sleep(2000);
219
221
  // Force kill any survivors
220
- for (const server of runningServers) {
222
+ for (const server of thisProjectServers) {
221
223
  try {
222
224
  process.kill(server.pid, 0); // Check if alive
223
225
  process.kill(server.pid, 'SIGKILL');
@@ -367,19 +369,27 @@ export class EmbeddingServerManager extends EventEmitter {
367
369
  }
368
370
  }
369
371
  // 2. Kill via pgrep as fallback (catches processes without PID files)
372
+ // PROJECT ISOLATION: Only kill processes belonging to THIS project
370
373
  try {
371
374
  const { execSync: execSyncLocal } = await import('child_process');
372
375
  const pids = execSyncLocal(`pgrep -f "frankenstein-embeddings.py" 2>/dev/null || true`, { encoding: 'utf8' }).trim().split('\n').filter(Boolean);
376
+ let killedAny = false;
373
377
  for (const pidStr of pids) {
374
378
  const pid = parseInt(pidStr, 10);
375
379
  if (pid && pid !== process.pid) {
380
+ // PROJECT ISOLATION: Skip processes belonging to other projects
381
+ if (!this._isProcessForThisProject(pid)) {
382
+ logger.debug({ pid }, '[EmbeddingServerManager] Pre-spawn: Skipping process belonging to another project');
383
+ continue;
384
+ }
376
385
  try {
377
386
  process.kill(pid, 'SIGTERM');
387
+ killedAny = true;
378
388
  logger.info({ pid }, '[EmbeddingServerManager] Killed orphan frankenstein process (pgrep)');
379
389
  } catch { /* already dead */ }
380
390
  }
381
391
  }
382
- if (pids.filter(Boolean).length > 0) {
392
+ if (killedAny) {
383
393
  await this.sleep(killWaitMs);
384
394
  }
385
395
  } catch { /* pgrep not available or no matches */ }
@@ -841,18 +851,20 @@ export class EmbeddingServerManager extends EventEmitter {
841
851
  }
842
852
  // Also try killing by PID file (in case process reference was lost)
843
853
  await this.killByPidFile();
844
- // FIX: Kill ALL running embedding server processes (orphans from previous sessions/subagents)
854
+ // FIX: Kill remaining embedding server processes for THIS project (orphans from previous sessions/subagents)
855
+ // PROJECT ISOLATION: Only kill processes belonging to this project, not other projects
845
856
  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) {
857
+ const thisProjectStopServers = allServers.filter(s => this._isProcessForThisProject(s.pid));
858
+ if (thisProjectStopServers.length > 0) {
859
+ logger.info({ count: thisProjectStopServers.length, totalFound: allServers.length, pids: thisProjectStopServers.map(s => s.pid) },
860
+ '[EmbeddingServerManager] Killing remaining embedding server processes for THIS project');
861
+ for (const server of thisProjectStopServers) {
850
862
  try {
851
863
  process.kill(server.pid, 'SIGTERM');
852
864
  } catch { /* already dead */ }
853
865
  }
854
866
  await this.sleep(2000);
855
- for (const server of allServers) {
867
+ for (const server of thisProjectStopServers) {
856
868
  try {
857
869
  process.kill(server.pid, 0);
858
870
  process.kill(server.pid, 'SIGKILL');
@@ -1533,6 +1545,34 @@ export class EmbeddingServerManager extends EventEmitter {
1533
1545
  return null;
1534
1546
  }
1535
1547
  }
1548
+ /**
1549
+ * Check if a running process belongs to THIS project instance.
1550
+ * Reads /proc/PID/environ and checks SPECMEM_PROJECT_PATH, SPECMEM_SOCKET_PATH,
1551
+ * and SPECMEM_EMBEDDING_SOCKET for a match against this project's paths.
1552
+ * Returns true if the process belongs to this project, false otherwise.
1553
+ * Returns false on any error (permission denied, process gone, etc.)
1554
+ */
1555
+ _isProcessForThisProject(pid) {
1556
+ try {
1557
+ const environPath = `/proc/${pid}/environ`;
1558
+ if (!existsSync(environPath)) {
1559
+ return false;
1560
+ }
1561
+ const environ = readFileSync(environPath, 'utf8');
1562
+ const envVars = environ.split('\0');
1563
+ const projectPath = this.projectPath || process.cwd();
1564
+ const socketPath = this.socketPath;
1565
+ for (const v of envVars) {
1566
+ if (v.startsWith('SPECMEM_PROJECT_PATH=') && v.includes(projectPath)) return true;
1567
+ if (v.startsWith('SPECMEM_SOCKET_PATH=') && v.includes(projectPath)) return true;
1568
+ if (v.startsWith('SPECMEM_EMBEDDING_SOCKET=') && socketPath && v.includes(socketPath)) return true;
1569
+ }
1570
+ return false;
1571
+ }
1572
+ catch {
1573
+ return false;
1574
+ }
1575
+ }
1536
1576
  /**
1537
1577
  * Get process info for an orphaned process (no PID file)
1538
1578
  */
@@ -1957,19 +1997,23 @@ export class EmbeddingServerManager extends EventEmitter {
1957
1997
  }
1958
1998
  }
1959
1999
  // FIX 4: Duplicate process detection during health monitoring
1960
- // Check for multiple embedding server processes and kill extras
2000
+ // Check for multiple embedding server processes FOR THIS PROJECT and kill extras
2001
+ // PROJECT ISOLATION: Filter to only this project's processes before killing duplicates
1961
2002
  try {
1962
2003
  const runningServers = this.findRunningEmbeddingServers();
1963
- if (runningServers.length > 1) {
2004
+ // PROJECT ISOLATION: Only consider processes belonging to this project
2005
+ const thisProjectHealthServers = runningServers.filter(s => this._isProcessForThisProject(s.pid));
2006
+ if (thisProjectHealthServers.length > 1) {
1964
2007
  logger.error({
1965
- count: runningServers.length,
1966
- pids: runningServers.map(s => s.pid),
1967
- }, '[EmbeddingServerManager] CRITICAL: Multiple embedding server processes detected!');
2008
+ count: thisProjectHealthServers.length,
2009
+ totalSystemWide: runningServers.length,
2010
+ pids: thisProjectHealthServers.map(s => s.pid),
2011
+ }, '[EmbeddingServerManager] CRITICAL: Multiple embedding server processes detected for THIS project!');
1968
2012
  // Determine the legitimate PID from PID file
1969
2013
  const legitimatePid = this.readPidFile();
1970
2014
  if (legitimatePid) {
1971
- // Kill all processes that are NOT the legitimate one
1972
- for (const server of runningServers) {
2015
+ // Kill all THIS PROJECT's processes that are NOT the legitimate one
2016
+ for (const server of thisProjectHealthServers) {
1973
2017
  if (server.pid !== legitimatePid) {
1974
2018
  try {
1975
2019
  logger.warn({ pid: server.pid, legitimatePid },
@@ -1994,12 +2038,12 @@ export class EmbeddingServerManager extends EventEmitter {
1994
2038
  }
1995
2039
  }
1996
2040
  else {
1997
- // No PID file - keep the first one, kill the rest
2041
+ // No PID file - keep the first one, kill the rest (only this project's processes)
1998
2042
  logger.warn('[EmbeddingServerManager] FIX 4: No PID file found, keeping oldest process');
1999
- for (let i = 1; i < runningServers.length; i++) {
2043
+ for (let i = 1; i < thisProjectHealthServers.length; i++) {
2000
2044
  try {
2001
- process.kill(runningServers[i].pid, 'SIGTERM');
2002
- logger.warn({ pid: runningServers[i].pid },
2045
+ process.kill(thisProjectHealthServers[i].pid, 'SIGTERM');
2046
+ logger.warn({ pid: thisProjectHealthServers[i].pid },
2003
2047
  '[EmbeddingServerManager] FIX 4: Killing extra duplicate process');
2004
2048
  }
2005
2049
  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"""
@@ -4014,9 +4014,23 @@ def main():
4014
4014
 
4015
4015
  # Signal handling for graceful shutdown
4016
4016
  import signal
4017
+ import traceback
4017
4018
  def handle_signal(signum, frame):
4018
4019
  sig_name = signal.Signals(signum).name
4020
+ # DEBUG: Log who sent the signal for troubleshooting
4021
+ my_pid = os.getpid()
4022
+ my_ppid = os.getppid()
4019
4023
  print(f"\n⚡ Received {sig_name} - shutting down gracefully...", file=sys.stderr)
4024
+ print(f" DEBUG: my_pid={my_pid}, my_ppid={my_ppid}", file=sys.stderr)
4025
+ # Try to identify caller via /proc
4026
+ try:
4027
+ with open(f'/proc/{my_ppid}/cmdline', 'r') as f:
4028
+ parent_cmd = f.read().replace('\x00', ' ').strip()
4029
+ print(f" DEBUG: parent_cmd={parent_cmd[:200]}", file=sys.stderr)
4030
+ except Exception as e:
4031
+ print(f" DEBUG: could not read parent cmdline: {e}", file=sys.stderr)
4032
+ print(f" DEBUG: stack trace:", file=sys.stderr)
4033
+ traceback.print_stack(frame, file=sys.stderr)
4020
4034
  server.shutdown_requested = True
4021
4035
  # Stop QQMS v2 drain thread if enabled
4022
4036
  if qqms_v2_instance:
@@ -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.6",
3
+ "version": "3.7.8",
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,20 +3710,25 @@ 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 */ }
3660
3718
  }
3661
3719
  const pythonPath = getPythonPath();
3720
+ // CRITICAL: Use file descriptor stdio, NOT pipes. Piped stdio causes SIGTERM
3721
+ // when parent event loop is under heavy load during batch indexing.
3722
+ const revalLogPath = path.join(projectPath, 'specmem', 'sockets', 'embedding-autostart.log');
3723
+ const revalLogFd = fs.openSync(revalLogPath, 'a');
3662
3724
  const proc = spawn(pythonPath, [embeddingScript], {
3663
3725
  cwd: path.dirname(embeddingScript),
3664
3726
  env: { ...process.env, SPECMEM_SOCKET_PATH: projectSocketPath, SPECMEM_PROJECT_PATH: projectPath },
3665
3727
  detached: true,
3666
- stdio: ['ignore', 'pipe', 'pipe']
3728
+ stdio: ['ignore', revalLogFd, revalLogFd]
3667
3729
  });
3668
- proc.stdout.on('data', () => {});
3669
- proc.stderr.on('data', () => {});
3670
3730
  proc.on('error', () => {});
3731
+ fs.closeSync(revalLogFd);
3671
3732
  proc.unref();
3672
3733
  // Wait up to 15s for socket to appear
3673
3734
  for (let i = 0; i < 30; i++) {
@@ -3678,6 +3739,7 @@ async function indexCodebase(projectPath, ui, embeddingResult) {
3678
3739
  if (healthy) {
3679
3740
  initLog(`Embedding server auto-restarted successfully, socket at ${projectSocketPath}`);
3680
3741
  consecutiveEmbeddingFailures = 0;
3742
+ revalidateBackoffMs = 1000;
3681
3743
  return true;
3682
3744
  }
3683
3745
  }
@@ -4855,6 +4917,9 @@ async function extractSessions(projectPath, ui, embeddingResult = null) {
4855
4917
  const socketsDir = path.join(projectPath, 'specmem', 'sockets');
4856
4918
  safeMkdir(socketsDir);
4857
4919
 
4920
+ // Kill any existing embedding server before spawning for session extraction
4921
+ killExistingEmbeddingServer(projectPath);
4922
+
4858
4923
  // Clean up stale socket
4859
4924
  if (fs.existsSync(projectSocketPath)) {
4860
4925
  try {
@@ -4867,6 +4932,10 @@ async function extractSessions(projectPath, ui, embeddingResult = null) {
4867
4932
 
4868
4933
  // Task #22 fix: Use getPythonPath() instead of hardcoded 'python3'
4869
4934
  const pythonPath = getPythonPath();
4935
+ // CRITICAL: Use file descriptor stdio, NOT pipes. Piped stdio causes SIGTERM
4936
+ // when parent event loop is under heavy load during batch indexing.
4937
+ const sessEmbedLogPath = path.join(projectPath, 'specmem', 'sockets', 'embedding-autostart.log');
4938
+ const sessEmbedLogFd = fs.openSync(sessEmbedLogPath, 'a');
4870
4939
  const embeddingProcess = spawn(pythonPath, [embeddingScript], {
4871
4940
  cwd: path.dirname(embeddingScript),
4872
4941
  env: {
@@ -4875,7 +4944,7 @@ async function extractSessions(projectPath, ui, embeddingResult = null) {
4875
4944
  SPECMEM_PROJECT_PATH: projectPath
4876
4945
  },
4877
4946
  detached: true,
4878
- stdio: ['ignore', 'pipe', 'pipe']
4947
+ stdio: ['ignore', sessEmbedLogFd, sessEmbedLogFd]
4879
4948
  });
4880
4949
 
4881
4950
  // error handler BEFORE unref - prevents silent spawn failures
@@ -4883,14 +4952,7 @@ async function extractSessions(projectPath, ui, embeddingResult = null) {
4883
4952
  ui.setSubStatus('Embedding spawn error: ' + err.message);
4884
4953
  });
4885
4954
 
4886
- // CRITICAL: Consume stdout/stderr so Python startup banner doesn't leak to terminal
4887
- embeddingProcess.stdout.on('data', (chunk) => {
4888
- initLog('[EMBED-STDOUT] ' + chunk.toString().trim());
4889
- });
4890
- embeddingProcess.stderr.on('data', (chunk) => {
4891
- initLog('[EMBED-STDERR] ' + chunk.toString().trim());
4892
- });
4893
-
4955
+ fs.closeSync(sessEmbedLogFd);
4894
4956
  embeddingProcess.unref();
4895
4957
 
4896
4958
  // Wait for socket to appear (up to 30s)