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.
Files changed (71) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +11 -15
  3. package/bin/specmem-autoclaude.cjs +12 -1
  4. package/bin/specmem-cli.cjs +1077 -11
  5. package/bin/specmem-console.cjs +890 -63
  6. package/bootstrap.cjs +10 -2
  7. package/claude-hooks/agent-loading-hook.cjs +16 -16
  8. package/claude-hooks/agent-loading-hook.js +28 -21
  9. package/claude-hooks/agent-type-matcher.js +1 -1
  10. package/claude-hooks/background-completion-silencer.js +1 -1
  11. package/claude-hooks/file-claim-enforcer.cjs +37 -36
  12. package/claude-hooks/output-cleaner.cjs +1 -1
  13. package/claude-hooks/refusal-detector-hook.cjs +53 -0
  14. package/claude-hooks/settings.json +64 -4
  15. package/claude-hooks/smart-search-interceptor.js +1 -1
  16. package/claude-hooks/specmem-search-enforcer.cjs +2 -11
  17. package/claude-hooks/specmem-team-member-inject.js +1 -1
  18. package/claude-hooks/specmem-unified-hook.py +1 -1
  19. package/claude-hooks/subagent-loading-hook.cjs +1 -1
  20. package/claude-hooks/task-progress-hook.cjs +7 -7
  21. package/claude-hooks/task-progress-hook.js +3 -3
  22. package/claude-hooks/team-comms-enforcer.cjs +113 -47
  23. package/claude-hooks/use-code-pointers.cjs +1 -1
  24. package/dist/claude-sessions/sessionParser.js +5 -0
  25. package/dist/cli/deploy-to-claude.js +9 -2
  26. package/dist/codebase/codebaseIndexer.js +48 -17
  27. package/dist/codebase/exclusions.js +3 -4
  28. package/dist/codebase/index.js +4 -0
  29. package/dist/codebase/pdfExtractor.js +298 -0
  30. package/dist/dashboard/api/taskTeamMembers.js +2 -2
  31. package/dist/db/bigBrainMigrations.js +29 -0
  32. package/dist/hooks/hookManager.js +4 -4
  33. package/dist/hooks/teamFramingCli.js +1 -1
  34. package/dist/hooks/teamMemberPrepromptHook.js +5 -5
  35. package/dist/index.js +49 -12
  36. package/dist/init/claudeConfigInjector.js +27 -8
  37. package/dist/installer/autoInstall.js +7 -1
  38. package/dist/mcp/compactionProxy.js +1052 -192
  39. package/dist/mcp/compactionProxyDaemon.js +112 -37
  40. package/dist/mcp/contextVault.js +439 -0
  41. package/dist/mcp/embeddingServerManager.js +151 -17
  42. package/dist/mcp/mcpProtocolHandler.js +6 -1
  43. package/dist/mcp/miniCOTServerManager.js +82 -8
  44. package/dist/mcp/specMemServer.js +45 -10
  45. package/dist/mcp/toolRegistry.js +6 -0
  46. package/dist/startup/startupIndexing.js +14 -0
  47. package/dist/team-members/taskOrchestrator.js +3 -3
  48. package/dist/team-members/taskTeamMemberLogger.js +2 -2
  49. package/dist/tools/goofy/deployTeamMember.js +3 -3
  50. package/dist/tools/goofy/digInTheVault.js +81 -0
  51. package/dist/tools/goofy/findCodePointers.js +17 -0
  52. package/dist/tools/goofy/findWhatISaid.js +19 -0
  53. package/dist/tools/goofy/stashTheGoods.js +56 -0
  54. package/dist/tools/teamMemberDeployer.js +2 -2
  55. package/dist/watcher/changeHandler.js +65 -8
  56. package/dist/watcher/changeQueue.js +20 -1
  57. package/embedding-sandbox/frankenstein-embeddings.py +4 -3
  58. package/embedding-sandbox/mini-cot-service.py +11 -13
  59. package/embedding-sandbox/pdf-text-extract.py +208 -0
  60. package/package.json +1 -1
  61. package/scripts/deploy-hooks.cjs +12 -4
  62. package/scripts/fast-batch-embedder.cjs +2 -2
  63. package/scripts/force-retry.cjs +34 -0
  64. package/scripts/global-postinstall.cjs +97 -4
  65. package/scripts/poetic-abliteration.cjs +379 -0
  66. package/scripts/refusal-enforcer.cjs +88 -0
  67. package/scripts/specmem-init.cjs +222 -41
  68. package/specmem/model-config.json +6 -6
  69. package/specmem/supervisord.conf +1 -1
  70. package/svg-sections/readme-token-compaction.svg +246 -0
  71. 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
- killStaleOnStart: process.env['SPECMEM_EMBEDDING_KILL_STALE'] !== 'false',
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
- // Step 1: Kill any stale processes
160
- if (this.config.killStaleOnStart) {
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.end();
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.end();
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.end();
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.end();
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.end();
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.end();
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: this.isRunning,
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 (v.startsWith('SPECMEM_PROJECT_PATH=') && v.includes(projectPath)) return true;
1879
- if (v.startsWith('SPECMEM_SOCKET_PATH=') && v.includes(projectPath)) return true;
1880
- if (v.startsWith('SPECMEM_EMBEDDING_SOCKET=') && socketPath && v.includes(socketPath)) return true;
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(args);
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
- logger.error({ error: err }, '[MiniCOTServerManager] Failed to start server');
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: this.process?.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: this.isRunning,
589
+ running: actuallyRunning,
534
590
  pid,
591
+ processAlive,
535
592
  socketPath: this.socketPath,
536
593
  socketExists,
537
- healthy: this.isRunning && socketExists && this.consecutiveFailures === 0,
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
- writeFileSync(this.pidFilePath, `${pid}:${Date.now()}`, 'utf8');
1397
- logger.debug({ pid, path: this.pidFilePath }, '[MiniCOTServerManager] Wrote PID file');
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: Compaction Proxy lightweight HTTP server, starts in <100ms
1254
- // Sits between Claude Code and Anthropic API, strips tool_result bodies
1255
- // from compaction requests (50-80% token reduction = faster compaction)
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 proxyResult = await startCompactionProxy();
1258
- if (proxyResult.started) {
1259
- startupLog(`Compaction proxy started on port ${proxyResult.port}`);
1260
- logger.info({ port: proxyResult.port }, 'Compaction proxy started');
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(`Compaction proxy skipped: ${proxyResult.reason}`);
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 failed to start (non-fatal)', proxyErr);
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(),
@@ -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
- * Task Orchestrator - Smart Work Distribution System
2
+ * Agent Orchestrator - Smart Work Distribution System
3
3
  *
4
- * Provides intelligent task distribution to teamMembers:
5
- * - Task queue with priority management
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