specmem-hardwicksoftware 3.7.35 → 3.7.38
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/CHANGELOG.md +34 -0
- package/README.md +11 -15
- package/bin/specmem-autoclaude.cjs +12 -1
- package/bin/specmem-cli.cjs +1077 -11
- package/bin/specmem-console.cjs +890 -63
- package/bootstrap.cjs +10 -2
- package/claude-hooks/agent-loading-hook.cjs +16 -16
- package/claude-hooks/agent-loading-hook.js +28 -21
- package/claude-hooks/agent-type-matcher.js +1 -1
- package/claude-hooks/background-completion-silencer.js +1 -1
- package/claude-hooks/file-claim-enforcer.cjs +37 -36
- package/claude-hooks/output-cleaner.cjs +1 -1
- package/claude-hooks/refusal-detector-hook.cjs +53 -0
- package/claude-hooks/settings.json +64 -4
- package/claude-hooks/smart-search-interceptor.js +1 -1
- package/claude-hooks/specmem-search-enforcer.cjs +2 -11
- package/claude-hooks/specmem-team-member-inject.js +1 -1
- package/claude-hooks/specmem-unified-hook.py +1 -1
- package/claude-hooks/subagent-loading-hook.cjs +1 -1
- package/claude-hooks/task-progress-hook.cjs +7 -7
- package/claude-hooks/task-progress-hook.js +3 -3
- package/claude-hooks/team-comms-enforcer.cjs +113 -47
- package/claude-hooks/use-code-pointers.cjs +1 -1
- package/dist/claude-sessions/sessionParser.js +5 -0
- package/dist/cli/deploy-to-claude.js +9 -2
- package/dist/codebase/codebaseIndexer.js +48 -17
- package/dist/codebase/exclusions.js +3 -4
- package/dist/codebase/index.js +4 -0
- package/dist/codebase/pdfExtractor.js +298 -0
- package/dist/dashboard/api/taskTeamMembers.js +2 -2
- package/dist/db/bigBrainMigrations.js +29 -0
- package/dist/hooks/hookManager.js +4 -4
- package/dist/hooks/teamFramingCli.js +1 -1
- package/dist/hooks/teamMemberPrepromptHook.js +5 -5
- package/dist/index.js +49 -12
- package/dist/init/claudeConfigInjector.js +27 -8
- package/dist/installer/autoInstall.js +7 -1
- package/dist/mcp/compactionProxy.js +1052 -192
- package/dist/mcp/compactionProxyDaemon.js +112 -37
- package/dist/mcp/contextVault.js +439 -0
- package/dist/mcp/embeddingServerManager.js +151 -17
- package/dist/mcp/mcpProtocolHandler.js +6 -1
- package/dist/mcp/miniCOTServerManager.js +82 -8
- package/dist/mcp/specMemServer.js +45 -10
- package/dist/mcp/toolRegistry.js +6 -0
- package/dist/startup/startupIndexing.js +14 -0
- package/dist/team-members/taskOrchestrator.js +3 -3
- package/dist/team-members/taskTeamMemberLogger.js +2 -2
- package/dist/tools/goofy/deployTeamMember.js +3 -3
- package/dist/tools/goofy/digInTheVault.js +81 -0
- package/dist/tools/goofy/findCodePointers.js +17 -0
- package/dist/tools/goofy/findWhatISaid.js +19 -0
- package/dist/tools/goofy/stashTheGoods.js +56 -0
- package/dist/tools/teamMemberDeployer.js +2 -2
- package/dist/watcher/changeHandler.js +65 -8
- package/dist/watcher/changeQueue.js +20 -1
- package/embedding-sandbox/frankenstein-embeddings.py +4 -3
- package/embedding-sandbox/mini-cot-service.py +11 -13
- package/embedding-sandbox/pdf-text-extract.py +208 -0
- package/package.json +1 -1
- package/scripts/deploy-hooks.cjs +12 -4
- package/scripts/fast-batch-embedder.cjs +2 -2
- package/scripts/force-retry.cjs +34 -0
- package/scripts/global-postinstall.cjs +97 -4
- package/scripts/poetic-abliteration.cjs +379 -0
- package/scripts/refusal-enforcer.cjs +88 -0
- package/scripts/specmem-init.cjs +222 -41
- package/specmem/model-config.json +6 -6
- package/specmem/supervisord.conf +1 -1
- package/svg-sections/readme-token-compaction.svg +246 -0
- package/claude-hooks/agent-chooser-hook.js +0 -179
|
@@ -47,13 +47,22 @@ const DEFAULT_CONFIG = {
|
|
|
47
47
|
startupTimeoutMs: parseInt(process.env['SPECMEM_EMBEDDING_STARTUP_TIMEOUT'] || '45000', 10),
|
|
48
48
|
maxRestartAttempts: parseInt(process.env['SPECMEM_EMBEDDING_MAX_RESTARTS'] || '5', 10),
|
|
49
49
|
autoStart: process.env['SPECMEM_EMBEDDING_AUTO_START'] !== 'false',
|
|
50
|
-
|
|
50
|
+
// FIX: Default to false - only kill if THIS project's socket/PID exists
|
|
51
|
+
// This prevents cross-project conflicts when multiple projects have embedding servers
|
|
52
|
+
killStaleOnStart: process.env['SPECMEM_EMBEDDING_KILL_STALE'] === 'true',
|
|
53
|
+
// New: Strict isolation mode - skip ALL cross-project process checks
|
|
54
|
+
// Set SPECMEM_EMBEDDING_STRICT_ISOLATION=1 to enable
|
|
55
|
+
strictIsolation: process.env['SPECMEM_EMBEDDING_STRICT_ISOLATION'] === '1',
|
|
51
56
|
maxProcessAgeHours: parseFloat(process.env['SPECMEM_EMBEDDING_MAX_AGE_HOURS'] || '1'),
|
|
52
57
|
// Circuit breaker configuration (Issue #10)
|
|
53
58
|
cbRestartWindowMs: parseInt(process.env['SPECMEM_RESTART_WINDOW_MS'] || '300000', 10),
|
|
54
59
|
cbMaxRestartsInWindow: parseInt(process.env['SPECMEM_RESTART_MAX_IN_WINDOW'] || '5', 10),
|
|
55
60
|
cbCooldownMs: parseInt(process.env['SPECMEM_RESTART_COOLDOWN_MS'] || '60000', 10),
|
|
56
61
|
cbMaxCooldownMs: parseInt(process.env['SPECMEM_RESTART_MAX_COOLDOWN_MS'] || '600000', 10),
|
|
62
|
+
// FIX: Status stability - require 3 consecutive failures before marking offline to prevent flickering
|
|
63
|
+
statusStabilityThreshold: parseInt(process.env['SPECMEM_STATUS_STABILITY_THRESHOLD'] || '3', 10),
|
|
64
|
+
// FIX: Reduce CPU by throttling duplicate detection to every 5th health check
|
|
65
|
+
duplicateCheckInterval: parseInt(process.env['SPECMEM_DUPLICATE_CHECK_INTERVAL'] || '5', 10),
|
|
57
66
|
};
|
|
58
67
|
// ============================================================================
|
|
59
68
|
// EMBEDDING SERVER MANAGER
|
|
@@ -75,6 +84,27 @@ export class EmbeddingServerManager extends EventEmitter {
|
|
|
75
84
|
healthCheckTimer = null;
|
|
76
85
|
isRunning = false;
|
|
77
86
|
consecutiveFailures = 0;
|
|
87
|
+
healthCheckCycle = 0; // FIX: Track health check cycles for throttling duplicate detection
|
|
88
|
+
// FIX: Unique instance ID to distinguish this SpecMem installation from others
|
|
89
|
+
instanceId = null;
|
|
90
|
+
// FIX: Generate unique instance ID per SpecMem installation
|
|
91
|
+
_ensureInstanceId() {
|
|
92
|
+
if (this.instanceId) return this.instanceId;
|
|
93
|
+
// Try to load from file
|
|
94
|
+
const instanceIdFile = path.join(this.dataDir || this.projectPath, '.specmem-instance-id');
|
|
95
|
+
try {
|
|
96
|
+
if (existsSync(instanceIdFile)) {
|
|
97
|
+
this.instanceId = readFileSync(instanceIdFile, 'utf-8').trim();
|
|
98
|
+
return this.instanceId;
|
|
99
|
+
}
|
|
100
|
+
} catch (e) { /* ignore */ }
|
|
101
|
+
// Generate new ID
|
|
102
|
+
this.instanceId = `inst-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
|
103
|
+
try {
|
|
104
|
+
writeFileSync(instanceIdFile, this.instanceId);
|
|
105
|
+
} catch (e) { /* ignore */ }
|
|
106
|
+
return this.instanceId;
|
|
107
|
+
}
|
|
78
108
|
restartCount = 0;
|
|
79
109
|
lastRestartTime = 0;
|
|
80
110
|
startTime = null;
|
|
@@ -156,9 +186,17 @@ export class EmbeddingServerManager extends EventEmitter {
|
|
|
156
186
|
this.startKysHeartbeat();
|
|
157
187
|
return;
|
|
158
188
|
}
|
|
159
|
-
//
|
|
160
|
-
|
|
189
|
+
// FIX: Only kill stale processes if THIS project's PID file exists
|
|
190
|
+
// This prevents cross-project conflicts when multiple projects have embedding servers
|
|
191
|
+
const pidFileExists = existsSync(this.pidFilePath);
|
|
192
|
+
// Step 1: Kill any stale processes (only if our PID file exists or explicitly enabled)
|
|
193
|
+
// skip if strictIsolation is enabled to prevent ANY cross-project checks
|
|
194
|
+
if (this.config.strictIsolation) {
|
|
195
|
+
logger.info('[EmbeddingServerManager] Strict isolation mode - skipping stale process cleanup');
|
|
196
|
+
} else if (this.config.killStaleOnStart && pidFileExists) {
|
|
161
197
|
await this.killStaleProcesses();
|
|
198
|
+
} else if (this.config.killStaleOnStart) {
|
|
199
|
+
logger.debug('[EmbeddingServerManager] killStaleOnStart enabled but no PID file - skipping cleanup');
|
|
162
200
|
}
|
|
163
201
|
// Step 2: Start the embedding server if auto-start is enabled
|
|
164
202
|
if (this.config.autoStart) {
|
|
@@ -187,6 +225,8 @@ export class EmbeddingServerManager extends EventEmitter {
|
|
|
187
225
|
this.isRunning = true;
|
|
188
226
|
return true;
|
|
189
227
|
}
|
|
228
|
+
// Refresh socket path before start to pick up any externally-created sockets
|
|
229
|
+
this.refreshSocketPath();
|
|
190
230
|
if (this.isRunning) {
|
|
191
231
|
logger.debug('[EmbeddingServerManager] Server already running');
|
|
192
232
|
return true;
|
|
@@ -561,6 +601,7 @@ export class EmbeddingServerManager extends EventEmitter {
|
|
|
561
601
|
SPECMEM_SOCKET_DIR: socketDir,
|
|
562
602
|
SPECMEM_EMBEDDING_SOCKET: this.socketPath,
|
|
563
603
|
SPECMEM_EMBEDDING_IDLE_TIMEOUT: '0',
|
|
604
|
+
SPECMEM_INSTANCE_ID: this.instanceId || this._ensureInstanceId(),
|
|
564
605
|
SPECMEM_DB_SCHEMA: projectSchema,
|
|
565
606
|
...configEnv,
|
|
566
607
|
});
|
|
@@ -924,12 +965,12 @@ export class EmbeddingServerManager extends EventEmitter {
|
|
|
924
965
|
if (response.error) {
|
|
925
966
|
clearTimeout(timeout);
|
|
926
967
|
resolved = true;
|
|
927
|
-
socket.
|
|
968
|
+
socket.destroy();
|
|
928
969
|
reject(new Error(response.error));
|
|
929
970
|
return;
|
|
930
971
|
}
|
|
931
|
-
// Skip "processing" status messages - wait for actual embedding
|
|
932
|
-
if (response.status === 'processing') {
|
|
972
|
+
// Skip "processing"/"working" status messages - wait for actual embedding
|
|
973
|
+
if (response.status === 'working' || response.status === 'processing') {
|
|
933
974
|
logger.debug({ textLength: response.text_length }, '[EmbeddingServerManager] Embedding request queued, waiting for result...');
|
|
934
975
|
continue; // Keep reading for the actual embedding
|
|
935
976
|
}
|
|
@@ -937,7 +978,7 @@ export class EmbeddingServerManager extends EventEmitter {
|
|
|
937
978
|
if (response.embedding && Array.isArray(response.embedding)) {
|
|
938
979
|
clearTimeout(timeout);
|
|
939
980
|
resolved = true;
|
|
940
|
-
socket.
|
|
981
|
+
socket.destroy();
|
|
941
982
|
resolve(response.embedding);
|
|
942
983
|
return;
|
|
943
984
|
}
|
|
@@ -953,6 +994,7 @@ export class EmbeddingServerManager extends EventEmitter {
|
|
|
953
994
|
clearTimeout(timeout);
|
|
954
995
|
if (!resolved) {
|
|
955
996
|
resolved = true;
|
|
997
|
+
socket.destroy();
|
|
956
998
|
reject(err);
|
|
957
999
|
}
|
|
958
1000
|
});
|
|
@@ -993,17 +1035,17 @@ export class EmbeddingServerManager extends EventEmitter {
|
|
|
993
1035
|
if (response.error) {
|
|
994
1036
|
clearTimeout(timeout);
|
|
995
1037
|
resolved = true;
|
|
996
|
-
socket.
|
|
1038
|
+
socket.destroy();
|
|
997
1039
|
reject(new Error(response.error));
|
|
998
1040
|
return;
|
|
999
1041
|
}
|
|
1000
|
-
if (response.status === 'processing') {
|
|
1042
|
+
if (response.status === 'working' || response.status === 'processing') {
|
|
1001
1043
|
continue; // Wait for actual result
|
|
1002
1044
|
}
|
|
1003
1045
|
if (response.embeddings && Array.isArray(response.embeddings)) {
|
|
1004
1046
|
clearTimeout(timeout);
|
|
1005
1047
|
resolved = true;
|
|
1006
|
-
socket.
|
|
1048
|
+
socket.destroy();
|
|
1007
1049
|
resolve(response.embeddings);
|
|
1008
1050
|
return;
|
|
1009
1051
|
}
|
|
@@ -1017,6 +1059,7 @@ export class EmbeddingServerManager extends EventEmitter {
|
|
|
1017
1059
|
clearTimeout(timeout);
|
|
1018
1060
|
if (!resolved) {
|
|
1019
1061
|
resolved = true;
|
|
1062
|
+
socket.destroy();
|
|
1020
1063
|
reject(err);
|
|
1021
1064
|
}
|
|
1022
1065
|
});
|
|
@@ -1073,11 +1116,11 @@ export class EmbeddingServerManager extends EventEmitter {
|
|
|
1073
1116
|
if (response.error) {
|
|
1074
1117
|
clearTimeout(timeout);
|
|
1075
1118
|
resolved = true;
|
|
1076
|
-
socket.
|
|
1119
|
+
socket.destroy();
|
|
1077
1120
|
reject(new Error(response.error));
|
|
1078
1121
|
return;
|
|
1079
1122
|
}
|
|
1080
|
-
if (response.status === 'processing') {
|
|
1123
|
+
if (response.status === 'working' || response.status === 'processing') {
|
|
1081
1124
|
logger.debug('[EmbeddingServerManager] Server-side processing in progress...');
|
|
1082
1125
|
continue;
|
|
1083
1126
|
}
|
|
@@ -1085,7 +1128,7 @@ export class EmbeddingServerManager extends EventEmitter {
|
|
|
1085
1128
|
if (response.total_processed !== undefined || response.processed !== undefined) {
|
|
1086
1129
|
clearTimeout(timeout);
|
|
1087
1130
|
resolved = true;
|
|
1088
|
-
socket.
|
|
1131
|
+
socket.destroy();
|
|
1089
1132
|
logger.info({ response }, '[EmbeddingServerManager] Server-side processing complete');
|
|
1090
1133
|
resolve(response);
|
|
1091
1134
|
return;
|
|
@@ -1119,6 +1162,8 @@ export class EmbeddingServerManager extends EventEmitter {
|
|
|
1119
1162
|
this.isShuttingDown = true;
|
|
1120
1163
|
this.isStarting = false; // Reset starting flag on stop
|
|
1121
1164
|
this.startupGraceUntil = 0; // Clear any active grace period
|
|
1165
|
+
// Refresh socket path to find actual socket before stopping
|
|
1166
|
+
this.refreshSocketPath();
|
|
1122
1167
|
logger.info('[EmbeddingServerManager] Stopping embedding server...');
|
|
1123
1168
|
// Stop health monitoring
|
|
1124
1169
|
this.stopHealthMonitoring();
|
|
@@ -1186,6 +1231,8 @@ export class EmbeddingServerManager extends EventEmitter {
|
|
|
1186
1231
|
*/
|
|
1187
1232
|
async healthCheck() {
|
|
1188
1233
|
const startTime = Date.now();
|
|
1234
|
+
// Refresh socket path to find socket that may have appeared after construction
|
|
1235
|
+
this.refreshSocketPath();
|
|
1189
1236
|
// Quick check: socket must exist
|
|
1190
1237
|
if (!existsSync(this.socketPath)) {
|
|
1191
1238
|
return {
|
|
@@ -1300,11 +1347,54 @@ export class EmbeddingServerManager extends EventEmitter {
|
|
|
1300
1347
|
/**
|
|
1301
1348
|
* Get current server status
|
|
1302
1349
|
*/
|
|
1350
|
+
/**
|
|
1351
|
+
* Refresh socketPath by re-scanning known locations.
|
|
1352
|
+
* Ensures we pick up a socket that appeared after construction.
|
|
1353
|
+
*/
|
|
1354
|
+
refreshSocketPath() {
|
|
1355
|
+
try {
|
|
1356
|
+
const freshPath = getEmbeddingSocketPath();
|
|
1357
|
+
if (freshPath && freshPath !== this.socketPath) {
|
|
1358
|
+
logger.debug({ old: this.socketPath, new: freshPath }, '[EmbeddingServerManager] Socket path updated');
|
|
1359
|
+
this.socketPath = freshPath;
|
|
1360
|
+
this.pidFilePath = join(dirname(freshPath), 'embedding.pid');
|
|
1361
|
+
this.stoppedFlagPath = join(dirname(freshPath), 'embedding.stopped');
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
catch (err) {
|
|
1365
|
+
logger.debug({ error: err.message }, '[EmbeddingServerManager] refreshSocketPath failed');
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
/**
|
|
1369
|
+
* Check if the PID from the PID file is alive (signal 0 check).
|
|
1370
|
+
*/
|
|
1371
|
+
isPidAlive(pid) {
|
|
1372
|
+
if (!pid || pid <= 0)
|
|
1373
|
+
return false;
|
|
1374
|
+
try {
|
|
1375
|
+
process.kill(pid, 0);
|
|
1376
|
+
return true;
|
|
1377
|
+
}
|
|
1378
|
+
catch {
|
|
1379
|
+
return false;
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1303
1382
|
getStatus() {
|
|
1383
|
+
// Refresh socket path to pick up sockets that appeared after construction
|
|
1384
|
+
this.refreshSocketPath();
|
|
1304
1385
|
const pid = this.readPidFile();
|
|
1305
1386
|
const socketExists = existsSync(this.socketPath);
|
|
1387
|
+
// Determine running state: use in-memory flag OR check PID file + process alive
|
|
1388
|
+
// This handles the case where server is running but manager was freshly created
|
|
1389
|
+
const running = this.isRunning || (pid != null && this.isPidAlive(pid) && socketExists);
|
|
1390
|
+
// Sync in-memory flag if we detected a running server via PID
|
|
1391
|
+
if (running && !this.isRunning && pid != null) {
|
|
1392
|
+
this.isRunning = true;
|
|
1393
|
+
this.startTime = this.startTime || Date.now();
|
|
1394
|
+
logger.info({ pid }, '[EmbeddingServerManager] Detected running server from PID file, syncing state');
|
|
1395
|
+
}
|
|
1306
1396
|
return {
|
|
1307
|
-
running
|
|
1397
|
+
running,
|
|
1308
1398
|
pid,
|
|
1309
1399
|
socketPath: this.socketPath,
|
|
1310
1400
|
socketExists,
|
|
@@ -1874,10 +1964,38 @@ export class EmbeddingServerManager extends EventEmitter {
|
|
|
1874
1964
|
const envVars = environ.split('\0');
|
|
1875
1965
|
const projectPath = this.projectPath || process.cwd();
|
|
1876
1966
|
const socketPath = this.socketPath;
|
|
1967
|
+
let foundInstanceId = null;
|
|
1877
1968
|
for (const v of envVars) {
|
|
1878
|
-
if
|
|
1879
|
-
if (v.startsWith('
|
|
1880
|
-
|
|
1969
|
+
// FIX: Check SPECMEM_INSTANCE_ID - if both have it and they match, it's duplicate
|
|
1970
|
+
if (v.startsWith('SPECMEM_INSTANCE_ID=')) {
|
|
1971
|
+
foundInstanceId = v.replace('SPECMEM_INSTANCE_ID=', '');
|
|
1972
|
+
}
|
|
1973
|
+
}
|
|
1974
|
+
// If both have instance IDs and they match → definitely same instance = duplicate
|
|
1975
|
+
if (foundInstanceId && this.instanceId && foundInstanceId === this.instanceId) {
|
|
1976
|
+
return true;
|
|
1977
|
+
}
|
|
1978
|
+
// If instance IDs differ → different installations → NOT duplicate
|
|
1979
|
+
if (foundInstanceId && this.instanceId && foundInstanceId !== this.instanceId) {
|
|
1980
|
+
return false;
|
|
1981
|
+
}
|
|
1982
|
+
// At least one is legacy (no instance ID) → use path-based matching
|
|
1983
|
+
for (const v of envVars) {
|
|
1984
|
+
// FIX: Use exact path match or path separator to prevent false positives
|
|
1985
|
+
// Previously used .includes() which matched partial paths like /specmem/abc matching /specmem
|
|
1986
|
+
if (v.startsWith('SPECMEM_PROJECT_PATH=')) {
|
|
1987
|
+
const envProjectPath = v.replace('SPECMEM_PROJECT_PATH=', '');
|
|
1988
|
+
// Match exact path or child path (with / separator)
|
|
1989
|
+
if (envProjectPath === projectPath || envProjectPath.startsWith(projectPath + '/')) return true;
|
|
1990
|
+
}
|
|
1991
|
+
if (v.startsWith('SPECMEM_SOCKET_PATH=')) {
|
|
1992
|
+
const envSocketPath = v.replace('SPECMEM_SOCKET_PATH=', '');
|
|
1993
|
+
if (envSocketPath === socketPath || envSocketPath.startsWith(projectPath + '/')) return true;
|
|
1994
|
+
}
|
|
1995
|
+
if (v.startsWith('SPECMEM_EMBEDDING_SOCKET=') && socketPath) {
|
|
1996
|
+
const envEmbSocket = v.replace('SPECMEM_EMBEDDING_SOCKET=', '');
|
|
1997
|
+
if (envEmbSocket === socketPath) return true;
|
|
1998
|
+
}
|
|
1881
1999
|
}
|
|
1882
2000
|
return false;
|
|
1883
2001
|
}
|
|
@@ -2009,6 +2127,9 @@ export class EmbeddingServerManager extends EventEmitter {
|
|
|
2009
2127
|
// Phase 1: Wait for socket file to appear (use 50% of timeout for file appearance)
|
|
2010
2128
|
const fileWaitDeadline = startTime + (this.config.startupTimeoutMs * 0.5);
|
|
2011
2129
|
while (Date.now() < fileWaitDeadline) {
|
|
2130
|
+
// Refresh socket path each iteration — the socket may appear at a path
|
|
2131
|
+
// different from what was computed at construction time
|
|
2132
|
+
this.refreshSocketPath();
|
|
2012
2133
|
if (existsSync(this.socketPath)) {
|
|
2013
2134
|
logger.debug({ elapsed: Date.now() - startTime }, '[EmbeddingServerManager] Socket file appeared, starting health check polling');
|
|
2014
2135
|
break;
|
|
@@ -2340,9 +2461,14 @@ export class EmbeddingServerManager extends EventEmitter {
|
|
|
2340
2461
|
}
|
|
2341
2462
|
}
|
|
2342
2463
|
}
|
|
2464
|
+
// FIX: Increment health check cycle counter for throttling duplicate detection
|
|
2465
|
+
this.healthCheckCycle++;
|
|
2466
|
+
// FIX: Only run duplicate detection every N health checks (default every 5th = 2.5 min)
|
|
2467
|
+
const shouldCheckDuplicates = this.healthCheckCycle % this.config.duplicateCheckInterval === 0;
|
|
2343
2468
|
// FIX 4: Duplicate process detection during health monitoring
|
|
2344
2469
|
// Check for multiple embedding server processes FOR THIS PROJECT and kill extras
|
|
2345
2470
|
// PROJECT ISOLATION: Filter to only this project's processes before killing duplicates
|
|
2471
|
+
if (!shouldCheckDuplicates) return; // Throttle: skip this cycle
|
|
2346
2472
|
try {
|
|
2347
2473
|
const runningServers = await this.findRunningEmbeddingServers();
|
|
2348
2474
|
// PROJECT ISOLATION: Only consider processes belonging to this project
|
|
@@ -2672,6 +2798,14 @@ export function getEmbeddingServerManager(config) {
|
|
|
2672
2798
|
}
|
|
2673
2799
|
// Create new manager for this project
|
|
2674
2800
|
const manager = new EmbeddingServerManager(config);
|
|
2801
|
+
// Recover state from PID file if server is already running externally
|
|
2802
|
+
// This handles the case where manager is recreated but server is still alive
|
|
2803
|
+
const existingPid = manager.readPidFile();
|
|
2804
|
+
if (existingPid && manager.isPidAlive(existingPid) && existsSync(manager.socketPath)) {
|
|
2805
|
+
manager.isRunning = true;
|
|
2806
|
+
manager.startTime = Date.now();
|
|
2807
|
+
logger.info({ pid: existingPid, socketPath: manager.socketPath }, '[EmbeddingServerManager] Recovered running state from existing PID file');
|
|
2808
|
+
}
|
|
2675
2809
|
embeddingManagersByProject.set(projectPath, manager);
|
|
2676
2810
|
logger.info({ projectPath, totalManagers: embeddingManagersByProject.size }, '[EmbeddingServerManager] Created new per-project manager');
|
|
2677
2811
|
return manager;
|
|
@@ -388,8 +388,13 @@ export class MCPProtocolHandler {
|
|
|
388
388
|
// validate inputs if we have a schema
|
|
389
389
|
const schema = TOOL_SCHEMAS[toolName];
|
|
390
390
|
let validatedArgs = args;
|
|
391
|
+
// Strip proxy artifacts before validation — compaction proxy sometimes injects _stripped
|
|
392
|
+
if (validatedArgs && typeof validatedArgs === 'object' && '_stripped' in validatedArgs) {
|
|
393
|
+
const { _stripped, ...clean } = validatedArgs;
|
|
394
|
+
validatedArgs = clean;
|
|
395
|
+
}
|
|
391
396
|
if (schema) {
|
|
392
|
-
const result = schema.safeParse(
|
|
397
|
+
const result = schema.safeParse(validatedArgs);
|
|
393
398
|
if (!result.success) {
|
|
394
399
|
throw new Error(`validation failed fr: ${result.error.message}`);
|
|
395
400
|
}
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
* @author hardwicksoftwareservices
|
|
29
29
|
*/
|
|
30
30
|
import { spawn, execSync } from 'child_process';
|
|
31
|
-
import { existsSync, readFileSync, writeFileSync, unlinkSync, mkdirSync } from 'fs';
|
|
31
|
+
import { existsSync, readFileSync, writeFileSync, unlinkSync, mkdirSync, renameSync } from 'fs';
|
|
32
32
|
import { join, dirname } from 'path';
|
|
33
33
|
import { createConnection } from 'net';
|
|
34
34
|
import { EventEmitter } from 'events';
|
|
@@ -220,6 +220,18 @@ export class MiniCOTServerManager extends EventEmitter {
|
|
|
220
220
|
this.startTime = Date.now();
|
|
221
221
|
this.consecutiveFailures = 0;
|
|
222
222
|
this.isStarting = false;
|
|
223
|
+
// Clean stale PID file — we don't own this process
|
|
224
|
+
const stalePid = this.readPidFile();
|
|
225
|
+
if (stalePid) {
|
|
226
|
+
try {
|
|
227
|
+
process.kill(stalePid, 0);
|
|
228
|
+
}
|
|
229
|
+
catch (_e) {
|
|
230
|
+
// Stale PID — remove it
|
|
231
|
+
this.removePidFile();
|
|
232
|
+
logger.debug({ stalePid }, '[MiniCOTServerManager] Removed stale PID file for dead process while adopting external server');
|
|
233
|
+
}
|
|
234
|
+
}
|
|
223
235
|
this.emit('started', { pid: null, external: true });
|
|
224
236
|
return true;
|
|
225
237
|
}
|
|
@@ -231,6 +243,17 @@ export class MiniCOTServerManager extends EventEmitter {
|
|
|
231
243
|
catch (err) {
|
|
232
244
|
logger.warn({ error: err }, '[MiniCOTServerManager] Failed to remove old socket');
|
|
233
245
|
}
|
|
246
|
+
// Also clean stale PID file if process is dead
|
|
247
|
+
const stalePid = this.readPidFile();
|
|
248
|
+
if (stalePid) {
|
|
249
|
+
try {
|
|
250
|
+
process.kill(stalePid, 0);
|
|
251
|
+
}
|
|
252
|
+
catch (_e) {
|
|
253
|
+
this.removePidFile();
|
|
254
|
+
logger.debug({ stalePid }, '[MiniCOTServerManager] Removed stale PID file for dead process during startup cleanup');
|
|
255
|
+
}
|
|
256
|
+
}
|
|
234
257
|
}
|
|
235
258
|
// Find the Mini COT script
|
|
236
259
|
const miniCOTScript = this.findMiniCOTScript();
|
|
@@ -376,7 +399,21 @@ export class MiniCOTServerManager extends EventEmitter {
|
|
|
376
399
|
return true;
|
|
377
400
|
}
|
|
378
401
|
catch (err) {
|
|
379
|
-
|
|
402
|
+
const errMsg = err?.message || String(err);
|
|
403
|
+
const isEADDRINUSE = errMsg.includes('EADDRINUSE') || errMsg.includes('address already in use');
|
|
404
|
+
const isENOENT = errMsg.includes('ENOENT') || errMsg.includes('no such file');
|
|
405
|
+
const isEACCES = errMsg.includes('EACCES') || errMsg.includes('permission denied');
|
|
406
|
+
let hint = '';
|
|
407
|
+
if (isEADDRINUSE) {
|
|
408
|
+
hint = ` (socket ${this.socketPath} already in use — another Mini COT instance may be running)`;
|
|
409
|
+
}
|
|
410
|
+
else if (isENOENT) {
|
|
411
|
+
hint = ` (Mini COT script not found — check installation)`;
|
|
412
|
+
}
|
|
413
|
+
else if (isEACCES) {
|
|
414
|
+
hint = ` (permission denied — check file permissions on script or socket directory)`;
|
|
415
|
+
}
|
|
416
|
+
logger.error({ error: err, socketPath: this.socketPath }, `[MiniCOTServerManager] Failed to start server${hint}`);
|
|
380
417
|
this.isStarting = false;
|
|
381
418
|
return false;
|
|
382
419
|
}
|
|
@@ -424,11 +461,12 @@ export class MiniCOTServerManager extends EventEmitter {
|
|
|
424
461
|
logger.debug({ error: err }, '[MiniCOTServerManager] Failed to remove socket');
|
|
425
462
|
}
|
|
426
463
|
}
|
|
464
|
+
const stoppedPid = this.process?.pid ?? null;
|
|
427
465
|
this.process = null;
|
|
428
466
|
this.isRunning = false;
|
|
429
467
|
this.startTime = null;
|
|
430
468
|
logger.info('[MiniCOTServerManager] Server stopped');
|
|
431
|
-
this.emit('stopped', { pid:
|
|
469
|
+
this.emit('stopped', { pid: stoppedPid });
|
|
432
470
|
}
|
|
433
471
|
/**
|
|
434
472
|
* Perform a health check on the Mini COT server
|
|
@@ -529,12 +567,31 @@ export class MiniCOTServerManager extends EventEmitter {
|
|
|
529
567
|
getStatus() {
|
|
530
568
|
const pid = this.readPidFile();
|
|
531
569
|
const socketExists = existsSync(this.socketPath);
|
|
570
|
+
// Check actual process liveness, not just in-memory flag
|
|
571
|
+
let processAlive = false;
|
|
572
|
+
if (pid) {
|
|
573
|
+
try {
|
|
574
|
+
process.kill(pid, 0); // signal 0 = existence check
|
|
575
|
+
processAlive = true;
|
|
576
|
+
}
|
|
577
|
+
catch (_e) {
|
|
578
|
+
// Process does not exist
|
|
579
|
+
processAlive = false;
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
// Reconcile: if isRunning but process is dead, correct the state
|
|
583
|
+
const actuallyRunning = this.isRunning && processAlive;
|
|
584
|
+
if (this.isRunning && !processAlive && pid) {
|
|
585
|
+
logger.warn({ pid }, '[MiniCOTServerManager] getStatus: process dead but isRunning was true, correcting state');
|
|
586
|
+
this.isRunning = false;
|
|
587
|
+
}
|
|
532
588
|
return {
|
|
533
|
-
running:
|
|
589
|
+
running: actuallyRunning,
|
|
534
590
|
pid,
|
|
591
|
+
processAlive,
|
|
535
592
|
socketPath: this.socketPath,
|
|
536
593
|
socketExists,
|
|
537
|
-
healthy:
|
|
594
|
+
healthy: actuallyRunning && socketExists && this.consecutiveFailures === 0,
|
|
538
595
|
lastHealthCheck: this.healthCheckTimer ? Date.now() : null,
|
|
539
596
|
consecutiveFailures: this.consecutiveFailures,
|
|
540
597
|
restartCount: this.restartCount,
|
|
@@ -1184,6 +1241,14 @@ export class MiniCOTServerManager extends EventEmitter {
|
|
|
1184
1241
|
logger.debug({ pid: healthInfo.pid }, '[MiniCOTServerManager] Process from PID file no longer exists');
|
|
1185
1242
|
}
|
|
1186
1243
|
this.removePidFile();
|
|
1244
|
+
// Also clean up stale socket
|
|
1245
|
+
try {
|
|
1246
|
+
if (existsSync(this.socketPath)) {
|
|
1247
|
+
unlinkSync(this.socketPath);
|
|
1248
|
+
logger.debug('[MiniCOTServerManager] Cleaned up stale socket in killByPidFile');
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
catch (_e) { /* ignore */ }
|
|
1187
1252
|
}
|
|
1188
1253
|
/**
|
|
1189
1254
|
* Find the Mini COT script path
|
|
@@ -1237,6 +1302,8 @@ export class MiniCOTServerManager extends EventEmitter {
|
|
|
1237
1302
|
if (this.process) {
|
|
1238
1303
|
this.process = null;
|
|
1239
1304
|
}
|
|
1305
|
+
// Always clean up PID file on exit to prevent stale PIDs
|
|
1306
|
+
this.removePidFile();
|
|
1240
1307
|
if (this.isShuttingDown) {
|
|
1241
1308
|
// Expected exit during shutdown
|
|
1242
1309
|
return;
|
|
@@ -1392,9 +1459,11 @@ export class MiniCOTServerManager extends EventEmitter {
|
|
|
1392
1459
|
if (!existsSync(pidDir)) {
|
|
1393
1460
|
mkdirSync(pidDir, { recursive: true });
|
|
1394
1461
|
}
|
|
1395
|
-
// Format: PID:TIMESTAMP
|
|
1396
|
-
|
|
1397
|
-
|
|
1462
|
+
// Format: PID:TIMESTAMP — atomic write via rename to prevent partial reads
|
|
1463
|
+
const tmpPath = this.pidFilePath + '.tmp';
|
|
1464
|
+
writeFileSync(tmpPath, `${pid}:${Date.now()}`, 'utf8');
|
|
1465
|
+
renameSync(tmpPath, this.pidFilePath);
|
|
1466
|
+
logger.debug({ pid, path: this.pidFilePath }, '[MiniCOTServerManager] Wrote PID file (atomic)');
|
|
1398
1467
|
}
|
|
1399
1468
|
catch (err) {
|
|
1400
1469
|
logger.error({ error: err }, '[MiniCOTServerManager] Failed to write PID file');
|
|
@@ -1425,6 +1494,11 @@ export class MiniCOTServerManager extends EventEmitter {
|
|
|
1425
1494
|
unlinkSync(this.pidFilePath);
|
|
1426
1495
|
logger.debug('[MiniCOTServerManager] Removed PID file');
|
|
1427
1496
|
}
|
|
1497
|
+
// Also clean up any leftover .tmp from atomic write
|
|
1498
|
+
const tmpPath = this.pidFilePath + '.tmp';
|
|
1499
|
+
if (existsSync(tmpPath)) {
|
|
1500
|
+
unlinkSync(tmpPath);
|
|
1501
|
+
}
|
|
1428
1502
|
}
|
|
1429
1503
|
catch (err) {
|
|
1430
1504
|
logger.debug({ error: err }, '[MiniCOTServerManager] Failed to remove PID file');
|
|
@@ -85,7 +85,9 @@ import { getSkillScanner } from '../skills/skillScanner.js';
|
|
|
85
85
|
import { getSkillResourceProvider } from '../skills/skillsResource.js';
|
|
86
86
|
import { getSkillReminder } from '../reminders/skillReminder.js';
|
|
87
87
|
// Compaction proxy — MITM token stripper for faster compaction
|
|
88
|
-
import { startCompactionProxy, stopCompactionProxy, killCompactionProxy } from './compactionProxy.js';
|
|
88
|
+
import { startCompactionProxy, stopCompactionProxy, killCompactionProxy, registerWithDaemon, checkDaemonHealth } from './compactionProxy.js';
|
|
89
|
+
// Context vault — token-saving stash for thicc tool outputs (auto-stash + search)
|
|
90
|
+
import { initContextVault, stashTheGoods as vaultStash, formatVaultReceipt, VAULT_THRESHOLD, VAULT_SKIP_TOOLS } from './contextVault.js';
|
|
89
91
|
// Command loader - load command .md files as MCP prompts
|
|
90
92
|
import { getCommandLoader } from '../commands/commandLoader.js';
|
|
91
93
|
// CLI Notifications - centralized notification system
|
|
@@ -586,6 +588,25 @@ export class SpecMemServer {
|
|
|
586
588
|
__debugLog('[MCP DEBUG]', Date.now(), 'TOOL_SLOW_WARNING', { callId, toolName: name, durationMs: duration, threshold: 100 });
|
|
587
589
|
logger.warn({ duration, tool: name }, 'tool execution kinda slow ngl');
|
|
588
590
|
}
|
|
591
|
+
// === Auto-vault large tool results to save tokens ===
|
|
592
|
+
// If a tool returns >5KB, stash it and hand back a compact receipt.
|
|
593
|
+
// Claude can dig_in_the_vault later for specific data.
|
|
594
|
+
if (!VAULT_SKIP_TOOLS.has(name)) {
|
|
595
|
+
try {
|
|
596
|
+
const resultStr = typeof result === 'string' ? result : JSON.stringify(result, null, 2);
|
|
597
|
+
if (resultStr.length > VAULT_THRESHOLD) {
|
|
598
|
+
const vaultResult = await vaultStash(resultStr, { tool: name, callId });
|
|
599
|
+
if (!vaultResult.deduplicated) {
|
|
600
|
+
startupLog(`[VAULT] Auto-stashed ${name}: ${resultStr.length} chars → receipt (vault:${vaultResult.vaultId})`);
|
|
601
|
+
}
|
|
602
|
+
result = formatVaultReceipt(vaultResult);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
catch (vaultErr) {
|
|
606
|
+
// Non-fatal — fall through with original result
|
|
607
|
+
__debugLog('[MCP DEBUG]', Date.now(), 'AUTO_VAULT_FAILED', { callId, toolName: name, error: vaultErr.message });
|
|
608
|
+
}
|
|
609
|
+
}
|
|
589
610
|
__debugLog('[MCP DEBUG]', Date.now(), 'FORMATTING_RESPONSE', { callId, toolName: name });
|
|
590
611
|
const formattedResponse = this.formatResponse(result);
|
|
591
612
|
__debugLog('[MCP DEBUG]', Date.now(), 'RESPONSE_FORMATTED', {
|
|
@@ -1250,20 +1271,23 @@ export class SpecMemServer {
|
|
|
1250
1271
|
// Each step waits for the previous to complete + yields to event loop
|
|
1251
1272
|
// This prevents CPU spikes that cause MCP connection timeouts
|
|
1252
1273
|
logger.info('SpecMem MCP server connected — lazy init starting');
|
|
1253
|
-
// Step 2.5:
|
|
1254
|
-
//
|
|
1255
|
-
//
|
|
1274
|
+
// Step 2.5: Register with compaction proxy daemon (DO NOT spawn here)
|
|
1275
|
+
// The proxy daemon is started by specmem-init BEFORE Claude launches.
|
|
1276
|
+
// By the time oninitialized fires, Claude is already running and routing
|
|
1277
|
+
// traffic through ANTHROPIC_BASE_URL — spawning here is too late.
|
|
1278
|
+
// We just register this project so the daemon knows we're alive.
|
|
1256
1279
|
try {
|
|
1257
|
-
const
|
|
1258
|
-
if (
|
|
1259
|
-
|
|
1260
|
-
|
|
1280
|
+
const healthy = await checkDaemonHealth();
|
|
1281
|
+
if (healthy) {
|
|
1282
|
+
registerWithDaemon(process.env.SPECMEM_PROJECT_PATH, process.pid);
|
|
1283
|
+
startupLog('Registered with existing compaction proxy daemon');
|
|
1284
|
+
logger.info('Registered with compaction proxy daemon');
|
|
1261
1285
|
} else {
|
|
1262
|
-
startupLog(
|
|
1286
|
+
startupLog('Compaction proxy daemon not running — skipping registration (must be started by specmem-init)');
|
|
1263
1287
|
}
|
|
1264
1288
|
} catch (proxyErr) {
|
|
1265
1289
|
// Non-fatal — proxy is optional, compaction still works without it
|
|
1266
|
-
startupLog('Compaction proxy
|
|
1290
|
+
startupLog('Compaction proxy registration check failed (non-fatal)', proxyErr);
|
|
1267
1291
|
}
|
|
1268
1292
|
// Step 3: Database init (deferred, tools wait via waitForReady())
|
|
1269
1293
|
startupLog('Starting deferred database initialization...');
|
|
@@ -1928,6 +1952,17 @@ export class SpecMemServer {
|
|
|
1928
1952
|
// Non-fatal - tools can still work with in-memory fallback
|
|
1929
1953
|
logger.warn({ error: teamCommsError }, 'Team comms DB init failed - using in-memory fallback');
|
|
1930
1954
|
}
|
|
1955
|
+
// Initialize context vault for token-saving auto-stash
|
|
1956
|
+
try {
|
|
1957
|
+
if (pool) {
|
|
1958
|
+
initContextVault(pool, process.env['SPECMEM_PROJECT_PATH'] || '/');
|
|
1959
|
+
logger.info('Context vault initialized — auto-stash ready');
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1962
|
+
catch (vaultError) {
|
|
1963
|
+
// Non-fatal — auto-stash disabled, manual tools still work
|
|
1964
|
+
logger.warn({ error: vaultError }, 'Context vault init failed — auto-stash disabled');
|
|
1965
|
+
}
|
|
1931
1966
|
// FIX 1.02: Create CommandHandler AFTER DB is initialized
|
|
1932
1967
|
// Previously created in constructor with uninitialized db
|
|
1933
1968
|
// ORDERING INVARIANT (CRIT-1): commandHandler MUST be created here in deferredInit(),
|
package/dist/mcp/toolRegistry.js
CHANGED
|
@@ -57,6 +57,9 @@ import { GetMemoryFull } from '../tools/goofy/getMemoryFull.js';
|
|
|
57
57
|
// Import project memory import/export tools - carry context across projects
|
|
58
58
|
import { ImportProjectMemories } from '../tools/goofy/importProjectMemories.js';
|
|
59
59
|
import { ExportProjectMemories } from '../tools/goofy/exportProjectMemories.js';
|
|
60
|
+
// Import context vault tools - token-saving stash for thicc tool outputs
|
|
61
|
+
import { StashTheGoods } from '../tools/goofy/stashTheGoods.js';
|
|
62
|
+
import { DigInTheVault } from '../tools/goofy/digInTheVault.js';
|
|
60
63
|
// Import MCP-based team communication tools (NEW - replaces HTTP team member comms)
|
|
61
64
|
import { createTeamCommTools } from './tools/teamComms.js';
|
|
62
65
|
// Import embedding server control tools (Phase 4 - user start/stop/status)
|
|
@@ -543,6 +546,9 @@ export function createToolRegistry(db, embeddingProvider) {
|
|
|
543
546
|
// Project memory import/export tools - carry context across projects
|
|
544
547
|
registry.register(new ImportProjectMemories(db, cachingProvider));
|
|
545
548
|
registry.register(new ExportProjectMemories(db));
|
|
549
|
+
// Context vault tools - token-saving stash + search for thicc tool outputs
|
|
550
|
+
registry.register(new StashTheGoods());
|
|
551
|
+
registry.register(new DigInTheVault());
|
|
546
552
|
// Team communication tools - multi-team member coordination
|
|
547
553
|
const teamCommTools = createTeamCommTools();
|
|
548
554
|
for (const tool of teamCommTools) {
|
|
@@ -183,6 +183,16 @@ export async function triggerBackgroundIndexing(embeddingProvider, options = {})
|
|
|
183
183
|
}
|
|
184
184
|
state.indexingInProgress = true;
|
|
185
185
|
state.lastIndexingCheck = new Date();
|
|
186
|
+
// Pause file watcher queue during indexing to avoid resource contention
|
|
187
|
+
let watcherQueue = null;
|
|
188
|
+
try {
|
|
189
|
+
const { getWatcherManager } = await import('../mcp/watcherIntegration.js');
|
|
190
|
+
const mgr = getWatcherManager();
|
|
191
|
+
if (mgr?.queue) {
|
|
192
|
+
watcherQueue = mgr.queue;
|
|
193
|
+
watcherQueue.pause('background indexing');
|
|
194
|
+
}
|
|
195
|
+
} catch { /* watcher not initialized yet — that's fine */ }
|
|
186
196
|
try {
|
|
187
197
|
const db = getDatabase();
|
|
188
198
|
if (!db) {
|
|
@@ -241,6 +251,10 @@ export async function triggerBackgroundIndexing(embeddingProvider, options = {})
|
|
|
241
251
|
}
|
|
242
252
|
finally {
|
|
243
253
|
state.indexingInProgress = false;
|
|
254
|
+
// Resume watcher queue after indexing completes (or fails)
|
|
255
|
+
if (watcherQueue) {
|
|
256
|
+
watcherQueue.resume();
|
|
257
|
+
}
|
|
244
258
|
}
|
|
245
259
|
}
|
|
246
260
|
/**
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Agent Orchestrator - Smart Work Distribution System
|
|
3
3
|
*
|
|
4
|
-
* Provides intelligent
|
|
5
|
-
* -
|
|
4
|
+
* Provides intelligent agent distribution to teamMembers:
|
|
5
|
+
* - Agent queue with priority management
|
|
6
6
|
* - Capability-based task matching
|
|
7
7
|
* - Load balancing across team members
|
|
8
8
|
* - Automatic failover and task reassignment
|