specmem-hardwicksoftware 3.7.35 → 3.7.36
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-console.cjs +839 -51
- package/claude-hooks/agent-chooser-hook.js +6 -6
- package/claude-hooks/agent-loading-hook.cjs +16 -16
- package/claude-hooks/agent-loading-hook.js +18 -18
- 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/settings.json +27 -3
- 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 +49 -47
- package/dist/claude-sessions/sessionParser.js +5 -0
- 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/init/claudeConfigInjector.js +2 -2
- package/dist/mcp/compactionProxy.js +834 -186
- package/dist/mcp/compactionProxyDaemon.js +112 -37
- package/dist/mcp/contextVault.js +439 -0
- package/dist/mcp/embeddingServerManager.js +61 -1
- 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/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/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 +2 -2
- package/scripts/global-postinstall.cjs +2 -2
- package/scripts/specmem-init.cjs +130 -36
- package/specmem/model-config.json +6 -6
- package/specmem/supervisord.conf +1 -1
- package/svg-sections/readme-token-compaction.svg +246 -0
|
@@ -187,6 +187,8 @@ export class EmbeddingServerManager extends EventEmitter {
|
|
|
187
187
|
this.isRunning = true;
|
|
188
188
|
return true;
|
|
189
189
|
}
|
|
190
|
+
// Refresh socket path before start to pick up any externally-created sockets
|
|
191
|
+
this.refreshSocketPath();
|
|
190
192
|
if (this.isRunning) {
|
|
191
193
|
logger.debug('[EmbeddingServerManager] Server already running');
|
|
192
194
|
return true;
|
|
@@ -1119,6 +1121,8 @@ export class EmbeddingServerManager extends EventEmitter {
|
|
|
1119
1121
|
this.isShuttingDown = true;
|
|
1120
1122
|
this.isStarting = false; // Reset starting flag on stop
|
|
1121
1123
|
this.startupGraceUntil = 0; // Clear any active grace period
|
|
1124
|
+
// Refresh socket path to find actual socket before stopping
|
|
1125
|
+
this.refreshSocketPath();
|
|
1122
1126
|
logger.info('[EmbeddingServerManager] Stopping embedding server...');
|
|
1123
1127
|
// Stop health monitoring
|
|
1124
1128
|
this.stopHealthMonitoring();
|
|
@@ -1186,6 +1190,8 @@ export class EmbeddingServerManager extends EventEmitter {
|
|
|
1186
1190
|
*/
|
|
1187
1191
|
async healthCheck() {
|
|
1188
1192
|
const startTime = Date.now();
|
|
1193
|
+
// Refresh socket path to find socket that may have appeared after construction
|
|
1194
|
+
this.refreshSocketPath();
|
|
1189
1195
|
// Quick check: socket must exist
|
|
1190
1196
|
if (!existsSync(this.socketPath)) {
|
|
1191
1197
|
return {
|
|
@@ -1300,11 +1306,54 @@ export class EmbeddingServerManager extends EventEmitter {
|
|
|
1300
1306
|
/**
|
|
1301
1307
|
* Get current server status
|
|
1302
1308
|
*/
|
|
1309
|
+
/**
|
|
1310
|
+
* Refresh socketPath by re-scanning known locations.
|
|
1311
|
+
* Ensures we pick up a socket that appeared after construction.
|
|
1312
|
+
*/
|
|
1313
|
+
refreshSocketPath() {
|
|
1314
|
+
try {
|
|
1315
|
+
const freshPath = getEmbeddingSocketPath();
|
|
1316
|
+
if (freshPath && freshPath !== this.socketPath) {
|
|
1317
|
+
logger.debug({ old: this.socketPath, new: freshPath }, '[EmbeddingServerManager] Socket path updated');
|
|
1318
|
+
this.socketPath = freshPath;
|
|
1319
|
+
this.pidFilePath = join(dirname(freshPath), 'embedding.pid');
|
|
1320
|
+
this.stoppedFlagPath = join(dirname(freshPath), 'embedding.stopped');
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
catch (err) {
|
|
1324
|
+
logger.debug({ error: err.message }, '[EmbeddingServerManager] refreshSocketPath failed');
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
/**
|
|
1328
|
+
* Check if the PID from the PID file is alive (signal 0 check).
|
|
1329
|
+
*/
|
|
1330
|
+
isPidAlive(pid) {
|
|
1331
|
+
if (!pid || pid <= 0)
|
|
1332
|
+
return false;
|
|
1333
|
+
try {
|
|
1334
|
+
process.kill(pid, 0);
|
|
1335
|
+
return true;
|
|
1336
|
+
}
|
|
1337
|
+
catch {
|
|
1338
|
+
return false;
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1303
1341
|
getStatus() {
|
|
1342
|
+
// Refresh socket path to pick up sockets that appeared after construction
|
|
1343
|
+
this.refreshSocketPath();
|
|
1304
1344
|
const pid = this.readPidFile();
|
|
1305
1345
|
const socketExists = existsSync(this.socketPath);
|
|
1346
|
+
// Determine running state: use in-memory flag OR check PID file + process alive
|
|
1347
|
+
// This handles the case where server is running but manager was freshly created
|
|
1348
|
+
const running = this.isRunning || (pid != null && this.isPidAlive(pid) && socketExists);
|
|
1349
|
+
// Sync in-memory flag if we detected a running server via PID
|
|
1350
|
+
if (running && !this.isRunning && pid != null) {
|
|
1351
|
+
this.isRunning = true;
|
|
1352
|
+
this.startTime = this.startTime || Date.now();
|
|
1353
|
+
logger.info({ pid }, '[EmbeddingServerManager] Detected running server from PID file, syncing state');
|
|
1354
|
+
}
|
|
1306
1355
|
return {
|
|
1307
|
-
running
|
|
1356
|
+
running,
|
|
1308
1357
|
pid,
|
|
1309
1358
|
socketPath: this.socketPath,
|
|
1310
1359
|
socketExists,
|
|
@@ -2009,6 +2058,9 @@ export class EmbeddingServerManager extends EventEmitter {
|
|
|
2009
2058
|
// Phase 1: Wait for socket file to appear (use 50% of timeout for file appearance)
|
|
2010
2059
|
const fileWaitDeadline = startTime + (this.config.startupTimeoutMs * 0.5);
|
|
2011
2060
|
while (Date.now() < fileWaitDeadline) {
|
|
2061
|
+
// Refresh socket path each iteration — the socket may appear at a path
|
|
2062
|
+
// different from what was computed at construction time
|
|
2063
|
+
this.refreshSocketPath();
|
|
2012
2064
|
if (existsSync(this.socketPath)) {
|
|
2013
2065
|
logger.debug({ elapsed: Date.now() - startTime }, '[EmbeddingServerManager] Socket file appeared, starting health check polling');
|
|
2014
2066
|
break;
|
|
@@ -2672,6 +2724,14 @@ export function getEmbeddingServerManager(config) {
|
|
|
2672
2724
|
}
|
|
2673
2725
|
// Create new manager for this project
|
|
2674
2726
|
const manager = new EmbeddingServerManager(config);
|
|
2727
|
+
// Recover state from PID file if server is already running externally
|
|
2728
|
+
// This handles the case where manager is recreated but server is still alive
|
|
2729
|
+
const existingPid = manager.readPidFile();
|
|
2730
|
+
if (existingPid && manager.isPidAlive(existingPid) && existsSync(manager.socketPath)) {
|
|
2731
|
+
manager.isRunning = true;
|
|
2732
|
+
manager.startTime = Date.now();
|
|
2733
|
+
logger.info({ pid: existingPid, socketPath: manager.socketPath }, '[EmbeddingServerManager] Recovered running state from existing PID file');
|
|
2734
|
+
}
|
|
2675
2735
|
embeddingManagersByProject.set(projectPath, manager);
|
|
2676
2736
|
logger.info({ projectPath, totalManagers: embeddingManagersByProject.size }, '[EmbeddingServerManager] Created new per-project manager');
|
|
2677
2737
|
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
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* taskTeamMemberLogger.ts - Logs Code
|
|
2
|
+
* taskTeamMemberLogger.ts - Logs Code Agent team member activity to SpecMem database
|
|
3
3
|
*
|
|
4
|
-
* yo fr fr this bridges the gap between Code's
|
|
4
|
+
* yo fr fr this bridges the gap between Code's Agent tool and SpecMem tracking
|
|
5
5
|
*
|
|
6
6
|
* Problem: Task-deployed team members are invisible to SpecMem dashboard
|
|
7
7
|
* Solution: Log team member activity before/after Task deployment
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* deployTeamMember - The
|
|
2
|
+
* deployTeamMember - The Agent tool but actually works with MCP
|
|
3
3
|
*
|
|
4
4
|
* Spawns team members in screen sessions with full SpecMem MCP access.
|
|
5
|
-
* This is basically skidding Code's
|
|
5
|
+
* This is basically skidding Code's Agent tool but making it not suck.
|
|
6
6
|
*/
|
|
7
7
|
import { deployTeamMember as deployTeamMemberImpl } from '../teamMemberDeployer.js';
|
|
8
8
|
import { logger } from '../../utils/logger.js';
|
|
@@ -10,7 +10,7 @@ export class DeployTeamMember {
|
|
|
10
10
|
name = 'deployTeamMember';
|
|
11
11
|
description = `Deploy a team member with full SpecMem MCP access.
|
|
12
12
|
|
|
13
|
-
This is like the
|
|
13
|
+
This is like the Agent tool but actually works - spawned team members get full access
|
|
14
14
|
to all SpecMem MCP tools including team member communication (sayToTeamMember, listenForMessages,
|
|
15
15
|
sendHeartbeat, getActiveTeamMembers).
|
|
16
16
|
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* digInTheVault - search the context vault for stashed content
|
|
3
|
+
*
|
|
4
|
+
* when you need to find something in all that stashed content
|
|
5
|
+
* without loading it all into context. BM25-ranked via tsvector.
|
|
6
|
+
*
|
|
7
|
+
* supports:
|
|
8
|
+
* - query search across all vaults (project-scoped)
|
|
9
|
+
* - targeted search within a specific vault_id
|
|
10
|
+
* - full dump of a vault_id (get_all mode)
|
|
11
|
+
* - stats mode (no query = return vault statistics)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { digInTheVault as doTheDig, getFullStash, getVaultStats } from '../../mcp/contextVault.js';
|
|
15
|
+
|
|
16
|
+
export class DigInTheVault {
|
|
17
|
+
name = 'dig_in_the_vault';
|
|
18
|
+
description = 'Search the context vault for previously stashed content. Returns BM25-ranked chunks matching your query. Use vault_id to search a specific stash, or omit to search all. Use get_all:true to retrieve everything from a vault.';
|
|
19
|
+
|
|
20
|
+
inputSchema = {
|
|
21
|
+
type: 'object',
|
|
22
|
+
properties: {
|
|
23
|
+
query: {
|
|
24
|
+
type: 'string',
|
|
25
|
+
description: 'What to search for in the vault'
|
|
26
|
+
},
|
|
27
|
+
vault_id: {
|
|
28
|
+
type: 'string',
|
|
29
|
+
description: 'Search within a specific stash (the id from vault_receipt)'
|
|
30
|
+
},
|
|
31
|
+
limit: {
|
|
32
|
+
type: 'number',
|
|
33
|
+
description: 'Max results to return (default: 10)',
|
|
34
|
+
default: 10
|
|
35
|
+
},
|
|
36
|
+
get_all: {
|
|
37
|
+
type: 'boolean',
|
|
38
|
+
description: 'Retrieve ALL chunks for a vault_id in order (ignores query)',
|
|
39
|
+
default: false
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
async execute(params) {
|
|
45
|
+
const { query, vault_id, limit = 10, get_all = false } = params;
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
// get_all mode: dump full stash content
|
|
49
|
+
if (get_all && vault_id) {
|
|
50
|
+
const full = await getFullStash(vault_id);
|
|
51
|
+
if (!full) return `<vault_dig ok="false" error="vault ${vault_id} not found or expired"/>`;
|
|
52
|
+
return full;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// No query = return vault stats
|
|
56
|
+
if (!query) {
|
|
57
|
+
const stats = await getVaultStats();
|
|
58
|
+
return JSON.stringify(stats, null, 2);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// BM25-ranked search
|
|
62
|
+
const results = await doTheDig(query, vault_id || null, limit);
|
|
63
|
+
|
|
64
|
+
if (results.length === 0) {
|
|
65
|
+
return vault_id
|
|
66
|
+
? `<vault_dig ok="false" vault_id="${vault_id}" query="${query}" error="no matches — try different terms or get_all:true"/>`
|
|
67
|
+
: `<vault_dig ok="false" query="${query}" error="no matches in any vault"/>`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Format results compactly
|
|
71
|
+
const lines = results.map((r, i) => {
|
|
72
|
+
const header = `── [${i + 1}] vault:${r.vault_id} chunk:${r.chunk_idx} rank:${parseFloat(r.rank).toFixed(3)} ──`;
|
|
73
|
+
return `${header}\n${r.content}`;
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
return lines.join('\n\n');
|
|
77
|
+
} catch (err) {
|
|
78
|
+
return `<vault_dig ok="false" error="${err.message}"/>`;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* stashTheGoods - manually vault content for later retrieval
|
|
3
|
+
*
|
|
4
|
+
* when you got data that's too thicc for the context window
|
|
5
|
+
* but you might need it later, stash it in the vault fr
|
|
6
|
+
*
|
|
7
|
+
* auto-stash handles most cases, but this tool lets claude
|
|
8
|
+
* manually stash anything it wants to keep searchable
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { stashTheGoods as doTheStash, formatVaultReceipt } from '../../mcp/contextVault.js';
|
|
12
|
+
|
|
13
|
+
export class StashTheGoods {
|
|
14
|
+
name = 'stash_the_goods';
|
|
15
|
+
description = 'Stash large content in the context vault for token-efficient retrieval later. Content is chunked, indexed with full-text search, and retrievable via dig_in_the_vault. Auto-expires after 24h.';
|
|
16
|
+
|
|
17
|
+
inputSchema = {
|
|
18
|
+
type: 'object',
|
|
19
|
+
properties: {
|
|
20
|
+
content: {
|
|
21
|
+
type: 'string',
|
|
22
|
+
description: 'The content to stash in the vault'
|
|
23
|
+
},
|
|
24
|
+
tool_name: {
|
|
25
|
+
type: 'string',
|
|
26
|
+
description: 'Which tool produced this content (for tracking)'
|
|
27
|
+
},
|
|
28
|
+
tags: {
|
|
29
|
+
type: 'array',
|
|
30
|
+
items: { type: 'string' },
|
|
31
|
+
description: 'Optional tags for categorization'
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
required: ['content']
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
async execute(params) {
|
|
38
|
+
const { content, tool_name, tags } = params;
|
|
39
|
+
|
|
40
|
+
if (!content || content.length === 0) {
|
|
41
|
+
return '<vault_receipt ok="false" error="nothing to stash fr"/>';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
const result = await doTheStash(content, {
|
|
46
|
+
tool: tool_name || 'manual_stash',
|
|
47
|
+
tags: tags || [],
|
|
48
|
+
manual: true,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
return formatVaultReceipt(result);
|
|
52
|
+
} catch (err) {
|
|
53
|
+
return `<vault_receipt ok="false" error="${err.message}"/>`;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Team Member Deployer Tool
|
|
3
3
|
*
|
|
4
|
-
* "Skidded" version of Code's
|
|
4
|
+
* "Skidded" version of Code's Agent tool that actually works with MCP
|
|
5
5
|
* Spawns team members with full SpecMem MCP access
|
|
6
6
|
*
|
|
7
7
|
* Now integrates with TeamCommsService for team-based team member coordination:
|
|
@@ -23,7 +23,7 @@ import { getAgentsJson, isValidAgentType } from './agentDefinitions.js';
|
|
|
23
23
|
/**
|
|
24
24
|
* Deploy a team member with full SpecMem MCP access
|
|
25
25
|
*
|
|
26
|
-
* This is basically the
|
|
26
|
+
* This is basically the Agent tool but it actually fucking works
|
|
27
27
|
* because we spawn team members with MCP configured
|
|
28
28
|
*
|
|
29
29
|
* Now with team communication support for coordinated multi-team-member work
|