specmem-hardwicksoftware 3.7.7 → 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.
- package/dist/mcp/embeddingServerManager.js +67 -23
- package/embedding-sandbox/__pycache__/frankenstein-embeddings.cpython-313.pyc +0 -0
- package/embedding-sandbox/frankenstein-embeddings.py +1 -1
- package/embedding-sandbox/warm-start.sh +13 -2
- package/package.json +1 -1
- package/scripts/specmem-init.cjs +62 -0
|
@@ -202,10 +202,12 @@ export class EmbeddingServerManager extends EventEmitter {
|
|
|
202
202
|
return true;
|
|
203
203
|
}
|
|
204
204
|
}
|
|
205
|
-
// Existing processes found but unhealthy - kill
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
|
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 (
|
|
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
|
|
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
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
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
|
|
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
|
-
|
|
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:
|
|
1966
|
-
|
|
1967
|
-
|
|
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
|
|
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 <
|
|
2043
|
+
for (let i = 1; i < thisProjectHealthServers.length; i++) {
|
|
2000
2044
|
try {
|
|
2001
|
-
process.kill(
|
|
2002
|
-
logger.warn({ 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 */ }
|
|
Binary file
|
|
@@ -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.
|
|
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
|
-
|
|
136
|
-
|
|
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.
|
|
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",
|
package/scripts/specmem-init.cjs
CHANGED
|
@@ -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 {
|