@softerist/heuristic-mcp 3.2.4 → 3.2.5
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/features/hybrid-search.js +1 -1
- package/features/index-codebase.js +7 -1
- package/index.js +109 -37
- package/lib/logging.js +22 -5
- package/lib/server-lifecycle.js +51 -22
- package/package.json +1 -1
|
@@ -336,7 +336,7 @@ export class HybridSearch {
|
|
|
336
336
|
for (let k = 0; k < queryWordCount; k++) {
|
|
337
337
|
if (lowerContent.includes(queryWords[k])) matchedWords++;
|
|
338
338
|
}
|
|
339
|
-
chunk.score += (matchedWords / queryWordCount) *
|
|
339
|
+
chunk.score += (matchedWords / queryWordCount) * PARTIAL_MATCH_BOOST;
|
|
340
340
|
}
|
|
341
341
|
|
|
342
342
|
if (chunk.content === undefined) {
|
|
@@ -2769,7 +2769,13 @@ export class CodebaseIndexer {
|
|
|
2769
2769
|
lastCheckpointIntervalMs: checkpointIntervalMs,
|
|
2770
2770
|
lastCheckpointSaves: checkpointSaveCount,
|
|
2771
2771
|
});
|
|
2772
|
-
|
|
2772
|
+
try {
|
|
2773
|
+
await this.cache.save({ throwOnError: true });
|
|
2774
|
+
} catch (error) {
|
|
2775
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2776
|
+
console.error(`[Indexer] Final cache save failed after embedding pass: ${message}`);
|
|
2777
|
+
throw error;
|
|
2778
|
+
}
|
|
2773
2779
|
|
|
2774
2780
|
this.sendProgress(
|
|
2775
2781
|
100,
|
package/index.js
CHANGED
|
@@ -16,9 +16,6 @@ async function getTransformers() {
|
|
|
16
16
|
if (transformersModule?.env) {
|
|
17
17
|
transformersModule.env.cacheDir = path.join(getGlobalCacheDir(), 'xenova');
|
|
18
18
|
}
|
|
19
|
-
if (transformersModule?.env) {
|
|
20
|
-
transformersModule.env.cacheDir = path.join(getGlobalCacheDir(), 'xenova');
|
|
21
|
-
}
|
|
22
19
|
}
|
|
23
20
|
return transformersModule;
|
|
24
21
|
}
|
|
@@ -79,6 +76,13 @@ import {
|
|
|
79
76
|
} from './lib/constants.js';
|
|
80
77
|
const PID_FILE_NAME = '.heuristic-mcp.pid';
|
|
81
78
|
|
|
79
|
+
function isTestRuntime() {
|
|
80
|
+
return (
|
|
81
|
+
process.env.VITEST === 'true' ||
|
|
82
|
+
process.env.NODE_ENV === 'test'
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
82
86
|
async function readLogTail(logPath, maxLines = 2000) {
|
|
83
87
|
const data = await fs.readFile(logPath, 'utf-8');
|
|
84
88
|
if (!data) return [];
|
|
@@ -134,6 +138,36 @@ async function printMemorySnapshot(workspaceDir) {
|
|
|
134
138
|
return true;
|
|
135
139
|
}
|
|
136
140
|
|
|
141
|
+
async function flushLogsSafely(options) {
|
|
142
|
+
if (typeof flushLogs !== 'function') {
|
|
143
|
+
console.warn('[Logs] flushLogs helper is unavailable; skipping log flush.');
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
await flushLogs(options);
|
|
149
|
+
} catch (error) {
|
|
150
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
151
|
+
console.warn(`[Logs] Failed to flush logs: ${message}`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function assertCacheContract(cacheInstance) {
|
|
156
|
+
const requiredMethods = [
|
|
157
|
+
'load',
|
|
158
|
+
'save',
|
|
159
|
+
'consumeAutoReindex',
|
|
160
|
+
'clearInMemoryState',
|
|
161
|
+
'getStoreSize',
|
|
162
|
+
];
|
|
163
|
+
const missing = requiredMethods.filter((name) => typeof cacheInstance?.[name] !== 'function');
|
|
164
|
+
if (missing.length > 0) {
|
|
165
|
+
throw new Error(
|
|
166
|
+
`[Server] Cache implementation contract violation: missing method(s): ${missing.join(', ')}`
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
137
171
|
let embedder = null;
|
|
138
172
|
let unloadMainEmbedder = null;
|
|
139
173
|
let cache = null;
|
|
@@ -150,6 +184,9 @@ let setWorkspaceFeatureInstance = null;
|
|
|
150
184
|
let autoWorkspaceSwitchPromise = null;
|
|
151
185
|
let rootsCapabilitySupported = null;
|
|
152
186
|
let rootsProbeInFlight = null;
|
|
187
|
+
let lastRootsProbeTime = 0;
|
|
188
|
+
let keepAliveTimer = null;
|
|
189
|
+
const ROOTS_PROBE_COOLDOWN_MS = 2000;
|
|
153
190
|
const WORKSPACE_BOUND_TOOL_NAMES = new Set([
|
|
154
191
|
'a_semantic_search',
|
|
155
192
|
'b_index_codebase',
|
|
@@ -202,6 +239,10 @@ function formatCrashDetail(detail) {
|
|
|
202
239
|
}
|
|
203
240
|
}
|
|
204
241
|
|
|
242
|
+
function isBrokenPipeError(detail) {
|
|
243
|
+
return Boolean(detail && typeof detail === 'object' && detail.code === 'EPIPE');
|
|
244
|
+
}
|
|
245
|
+
|
|
205
246
|
function isCrashShutdownReason(reason) {
|
|
206
247
|
const normalized = String(reason || '').toLowerCase();
|
|
207
248
|
return normalized.includes('uncaughtexception') || normalized.includes('unhandledrejection');
|
|
@@ -223,6 +264,10 @@ function registerProcessDiagnostics({ isServerMode, requestShutdown, getShutdown
|
|
|
223
264
|
let fatalHandled = false;
|
|
224
265
|
const handleFatalError = (reason, detail) => {
|
|
225
266
|
if (fatalHandled) return;
|
|
267
|
+
if (isBrokenPipeError(detail)) {
|
|
268
|
+
requestShutdown('stdio-epipe');
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
226
271
|
fatalHandled = true;
|
|
227
272
|
console.error(`[Server] Fatal ${reason}: ${formatCrashDetail(detail)}`);
|
|
228
273
|
requestShutdown(reason);
|
|
@@ -460,7 +505,9 @@ async function maybeAutoSwitchWorkspaceToPath(
|
|
|
460
505
|
|
|
461
506
|
if (autoWorkspaceSwitchPromise) {
|
|
462
507
|
await autoWorkspaceSwitchPromise;
|
|
463
|
-
|
|
508
|
+
const currentNow = normalizePathForCompare(config.searchDirectory);
|
|
509
|
+
const targetNow = normalizePathForCompare(targetWorkspacePath);
|
|
510
|
+
if (targetNow === currentNow) return;
|
|
464
511
|
}
|
|
465
512
|
|
|
466
513
|
autoWorkspaceSwitchPromise = (async () => {
|
|
@@ -494,6 +541,9 @@ async function maybeAutoSwitchWorkspaceFromRoots(request) {
|
|
|
494
541
|
if (rootsProbeInFlight) {
|
|
495
542
|
return await rootsProbeInFlight;
|
|
496
543
|
}
|
|
544
|
+
const now = Date.now();
|
|
545
|
+
if (now - lastRootsProbeTime < ROOTS_PROBE_COOLDOWN_MS) return null;
|
|
546
|
+
lastRootsProbeTime = now;
|
|
497
547
|
|
|
498
548
|
rootsProbeInFlight = (async () => {
|
|
499
549
|
const rootWorkspace = await detectWorkspaceFromRoots({ quiet: true });
|
|
@@ -566,7 +616,7 @@ async function initialize(workspaceDir) {
|
|
|
566
616
|
}
|
|
567
617
|
}
|
|
568
618
|
|
|
569
|
-
const isTest =
|
|
619
|
+
const isTest = isTestRuntime();
|
|
570
620
|
if (config.enableExplicitGc && typeof global.gc !== 'function' && !isTest) {
|
|
571
621
|
console.warn(
|
|
572
622
|
'[Server] enableExplicitGc=true but this process was not started with --expose-gc; continuing with explicit GC disabled.'
|
|
@@ -820,6 +870,7 @@ async function initialize(workspaceDir) {
|
|
|
820
870
|
}
|
|
821
871
|
|
|
822
872
|
cache = new EmbeddingsCache(config);
|
|
873
|
+
assertCacheContract(cache);
|
|
823
874
|
console.info(`[Server] Cache directory: ${config.cacheDirectory}`);
|
|
824
875
|
|
|
825
876
|
indexer = new CodebaseIndexer(embedder, cache, config, server);
|
|
@@ -936,7 +987,8 @@ async function initialize(workspaceDir) {
|
|
|
936
987
|
context: 'loading cache in secondary read-only mode',
|
|
937
988
|
canReindex: false,
|
|
938
989
|
});
|
|
939
|
-
|
|
990
|
+
const storeSize = cache.getStoreSize();
|
|
991
|
+
if (storeSize === 0) {
|
|
940
992
|
await tryAutoAttachWorkspaceCache('secondary-empty-cache', { canReindex: false });
|
|
941
993
|
}
|
|
942
994
|
if (config.verbose) {
|
|
@@ -1219,7 +1271,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1219
1271
|
if (config.unloadModelAfterSearch && searchTools.includes(toolDef.name)) {
|
|
1220
1272
|
setImmediate(async () => {
|
|
1221
1273
|
if (typeof unloadMainEmbedder === 'function') {
|
|
1222
|
-
|
|
1274
|
+
try {
|
|
1275
|
+
await unloadMainEmbedder();
|
|
1276
|
+
} catch (err) {
|
|
1277
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1278
|
+
console.warn(`[Server] Post-search model unload failed: ${message}`);
|
|
1279
|
+
}
|
|
1223
1280
|
}
|
|
1224
1281
|
});
|
|
1225
1282
|
}
|
|
@@ -1270,13 +1327,14 @@ export async function main(argv = process.argv) {
|
|
|
1270
1327
|
console.info(`[Server] Shutdown requested (${reason}).`);
|
|
1271
1328
|
void gracefulShutdown(reason);
|
|
1272
1329
|
};
|
|
1330
|
+
const isTestEnv = isTestRuntime();
|
|
1273
1331
|
registerProcessDiagnostics({
|
|
1274
1332
|
isServerMode,
|
|
1275
1333
|
requestShutdown,
|
|
1276
1334
|
getShutdownReason: () => shutdownReason,
|
|
1277
1335
|
});
|
|
1278
1336
|
|
|
1279
|
-
if (isServerMode && !
|
|
1337
|
+
if (isServerMode && !isTestEnv) {
|
|
1280
1338
|
enableStderrOnlyLogging();
|
|
1281
1339
|
}
|
|
1282
1340
|
if (wantsVersion) {
|
|
@@ -1403,29 +1461,31 @@ export async function main(argv = process.argv) {
|
|
|
1403
1461
|
|
|
1404
1462
|
registerSignalHandlers(requestShutdown);
|
|
1405
1463
|
|
|
1406
|
-
const detectedRootPromise =
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1464
|
+
const detectedRootPromise = isTestEnv
|
|
1465
|
+
? Promise.resolve(null)
|
|
1466
|
+
: new Promise((resolve) => {
|
|
1467
|
+
const HANDSHAKE_TIMEOUT_MS = 1000;
|
|
1468
|
+
let settled = false;
|
|
1469
|
+
const resolveOnce = (value) => {
|
|
1470
|
+
if (settled) return;
|
|
1471
|
+
settled = true;
|
|
1472
|
+
resolve(value);
|
|
1473
|
+
};
|
|
1414
1474
|
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1475
|
+
const timer = setTimeout(() => {
|
|
1476
|
+
console.warn(
|
|
1477
|
+
`[Server] MCP handshake timed out after ${HANDSHAKE_TIMEOUT_MS}ms, proceeding without roots.`
|
|
1478
|
+
);
|
|
1479
|
+
resolveOnce(null);
|
|
1480
|
+
}, HANDSHAKE_TIMEOUT_MS);
|
|
1481
|
+
|
|
1482
|
+
server.oninitialized = async () => {
|
|
1483
|
+
clearTimeout(timer);
|
|
1484
|
+
console.info('[Server] MCP handshake complete.');
|
|
1485
|
+
const root = await detectWorkspaceFromRoots();
|
|
1486
|
+
resolveOnce(root);
|
|
1487
|
+
};
|
|
1488
|
+
});
|
|
1429
1489
|
|
|
1430
1490
|
const transport = new StdioServerTransport();
|
|
1431
1491
|
await server.connect(transport);
|
|
@@ -1438,6 +1498,11 @@ export async function main(argv = process.argv) {
|
|
|
1438
1498
|
requestShutdown('stdout-epipe');
|
|
1439
1499
|
}
|
|
1440
1500
|
});
|
|
1501
|
+
process.stderr?.on?.('error', (err) => {
|
|
1502
|
+
if (err?.code === 'EPIPE') {
|
|
1503
|
+
requestShutdown('stderr-epipe');
|
|
1504
|
+
}
|
|
1505
|
+
});
|
|
1441
1506
|
}
|
|
1442
1507
|
|
|
1443
1508
|
const detectedRoot = await detectedRootPromise;
|
|
@@ -1461,13 +1526,16 @@ export async function main(argv = process.argv) {
|
|
|
1461
1526
|
|
|
1462
1527
|
console.info('[Server] Heuristic MCP server started.');
|
|
1463
1528
|
|
|
1464
|
-
|
|
1529
|
+
const backgroundTaskPromise = startBackgroundTasks().catch((err) => {
|
|
1465
1530
|
console.error(`[Server] Background task error: ${err.message}`);
|
|
1466
1531
|
});
|
|
1532
|
+
if (isTestEnv) {
|
|
1533
|
+
await backgroundTaskPromise;
|
|
1534
|
+
}
|
|
1467
1535
|
// Keep-Alive mechanism: ensure the process stays alive even if StdioServerTransport
|
|
1468
1536
|
// temporarily loses its active handle status or during complex async chains.
|
|
1469
|
-
if (isServerMode) {
|
|
1470
|
-
setInterval(() => {
|
|
1537
|
+
if (isServerMode && !isTestEnv && !keepAliveTimer) {
|
|
1538
|
+
keepAliveTimer = setInterval(() => {
|
|
1471
1539
|
// Logic to keep event loop active.
|
|
1472
1540
|
// We don't need to do anything, just the presence of the timer is enough.
|
|
1473
1541
|
}, SERVER_KEEP_ALIVE_INTERVAL_MS);
|
|
@@ -1480,6 +1548,11 @@ async function gracefulShutdown(signal) {
|
|
|
1480
1548
|
console.info(`[Server] Received ${signal}, shutting down gracefully...`);
|
|
1481
1549
|
const exitCode = isCrashShutdownReason(signal) ? 1 : 0;
|
|
1482
1550
|
|
|
1551
|
+
if (keepAliveTimer) {
|
|
1552
|
+
clearInterval(keepAliveTimer);
|
|
1553
|
+
keepAliveTimer = null;
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1483
1556
|
const cleanupTasks = [];
|
|
1484
1557
|
|
|
1485
1558
|
if (indexer && indexer.watcher) {
|
|
@@ -1516,7 +1589,7 @@ async function gracefulShutdown(signal) {
|
|
|
1516
1589
|
|
|
1517
1590
|
await Promise.allSettled(cleanupTasks);
|
|
1518
1591
|
console.info('[Server] Goodbye!');
|
|
1519
|
-
await
|
|
1592
|
+
await flushLogsSafely({ close: true, timeoutMs: 1500 });
|
|
1520
1593
|
|
|
1521
1594
|
setTimeout(() => process.exit(exitCode), 100);
|
|
1522
1595
|
}
|
|
@@ -1525,14 +1598,13 @@ const isMain =
|
|
|
1525
1598
|
process.argv[1] &&
|
|
1526
1599
|
(path.resolve(process.argv[1]).toLowerCase() === fileURLToPath(import.meta.url).toLowerCase() ||
|
|
1527
1600
|
process.argv[1].endsWith('heuristic-mcp') ||
|
|
1528
|
-
process.argv[1].endsWith('heuristic-mcp.js')
|
|
1529
|
-
path.basename(process.argv[1]) === 'index.js') &&
|
|
1601
|
+
process.argv[1].endsWith('heuristic-mcp.js')) &&
|
|
1530
1602
|
!(process.env.VITEST === 'true' || process.env.NODE_ENV === 'test');
|
|
1531
1603
|
|
|
1532
1604
|
if (isMain) {
|
|
1533
1605
|
main().catch(async (err) => {
|
|
1534
1606
|
console.error(err);
|
|
1535
|
-
await
|
|
1607
|
+
await flushLogsSafely({ close: true, timeoutMs: 500 });
|
|
1536
1608
|
process.exit(1);
|
|
1537
1609
|
});
|
|
1538
1610
|
}
|
package/lib/logging.js
CHANGED
|
@@ -4,6 +4,7 @@ import path from 'path';
|
|
|
4
4
|
import util from 'util';
|
|
5
5
|
|
|
6
6
|
let logStream = null;
|
|
7
|
+
let stderrWritable = true;
|
|
7
8
|
const originalConsole = {
|
|
8
9
|
log: console.info,
|
|
9
10
|
warn: console.warn,
|
|
@@ -11,8 +12,25 @@ const originalConsole = {
|
|
|
11
12
|
info: console.info,
|
|
12
13
|
};
|
|
13
14
|
|
|
15
|
+
function isBrokenPipeError(error) {
|
|
16
|
+
return Boolean(error && typeof error === 'object' && error.code === 'EPIPE');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function writeToOriginalStderr(...args) {
|
|
20
|
+
if (!stderrWritable) return;
|
|
21
|
+
try {
|
|
22
|
+
originalConsole.error(...args);
|
|
23
|
+
} catch (error) {
|
|
24
|
+
if (isBrokenPipeError(error)) {
|
|
25
|
+
stderrWritable = false;
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
throw error;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
14
32
|
export function enableStderrOnlyLogging() {
|
|
15
|
-
const redirect = (...args) =>
|
|
33
|
+
const redirect = (...args) => writeToOriginalStderr(...args);
|
|
16
34
|
// eslint-disable-next-line no-console
|
|
17
35
|
console.log = redirect;
|
|
18
36
|
console.info = redirect;
|
|
@@ -49,10 +67,9 @@ export async function setupFileLogging(config) {
|
|
|
49
67
|
};
|
|
50
68
|
|
|
51
69
|
const wrap = (method, level) => {
|
|
52
|
-
const originalError = originalConsole.error;
|
|
53
70
|
// eslint-disable-next-line no-console
|
|
54
71
|
console[method] = (...args) => {
|
|
55
|
-
|
|
72
|
+
writeToOriginalStderr(...args);
|
|
56
73
|
writeLine(level, args);
|
|
57
74
|
};
|
|
58
75
|
};
|
|
@@ -63,7 +80,7 @@ export async function setupFileLogging(config) {
|
|
|
63
80
|
wrap('info', 'INFO');
|
|
64
81
|
|
|
65
82
|
logStream.on('error', (err) => {
|
|
66
|
-
|
|
83
|
+
writeToOriginalStderr(`[Logs] Failed to write log file: ${err.message}`);
|
|
67
84
|
});
|
|
68
85
|
|
|
69
86
|
process.on('exit', () => {
|
|
@@ -72,7 +89,7 @@ export async function setupFileLogging(config) {
|
|
|
72
89
|
|
|
73
90
|
return logPath;
|
|
74
91
|
} catch (err) {
|
|
75
|
-
|
|
92
|
+
writeToOriginalStderr(`[Logs] Failed to initialize log file: ${err.message}`);
|
|
76
93
|
return null;
|
|
77
94
|
}
|
|
78
95
|
}
|
package/lib/server-lifecycle.js
CHANGED
|
@@ -4,10 +4,51 @@ import path from 'path';
|
|
|
4
4
|
import os from 'os';
|
|
5
5
|
import { setTimeout as delay } from 'timers/promises';
|
|
6
6
|
|
|
7
|
+
const pidFilesToCleanup = new Set();
|
|
8
|
+
const workspaceLocksToCleanup = new Set();
|
|
9
|
+
let pidExitCleanupRegistered = false;
|
|
10
|
+
let workspaceLockExitCleanupRegistered = false;
|
|
11
|
+
|
|
7
12
|
function isTestEnv() {
|
|
8
13
|
return process.env.VITEST === 'true' || process.env.NODE_ENV === 'test';
|
|
9
14
|
}
|
|
10
15
|
|
|
16
|
+
function cleanupPidFilesOnExit() {
|
|
17
|
+
for (const pidPath of pidFilesToCleanup) {
|
|
18
|
+
try {
|
|
19
|
+
fsSync.unlinkSync(pidPath);
|
|
20
|
+
} catch {}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function registerPidFileCleanup(pidPath) {
|
|
25
|
+
if (!pidPath) return;
|
|
26
|
+
pidFilesToCleanup.add(pidPath);
|
|
27
|
+
if (pidExitCleanupRegistered) return;
|
|
28
|
+
process.on('exit', cleanupPidFilesOnExit);
|
|
29
|
+
pidExitCleanupRegistered = true;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function cleanupWorkspaceLocksOnExit() {
|
|
33
|
+
for (const lockPath of workspaceLocksToCleanup) {
|
|
34
|
+
try {
|
|
35
|
+
const raw = fsSync.readFileSync(lockPath, 'utf-8');
|
|
36
|
+
const current = JSON.parse(raw);
|
|
37
|
+
if (current && current.pid === process.pid) {
|
|
38
|
+
fsSync.unlinkSync(lockPath);
|
|
39
|
+
}
|
|
40
|
+
} catch {}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function registerWorkspaceLockCleanup(lockPath) {
|
|
45
|
+
if (!lockPath) return;
|
|
46
|
+
workspaceLocksToCleanup.add(lockPath);
|
|
47
|
+
if (workspaceLockExitCleanupRegistered) return;
|
|
48
|
+
process.on('exit', cleanupWorkspaceLocksOnExit);
|
|
49
|
+
workspaceLockExitCleanupRegistered = true;
|
|
50
|
+
}
|
|
51
|
+
|
|
11
52
|
function getPidFilePath({ pidFileName, cacheDirectory }) {
|
|
12
53
|
if (cacheDirectory) {
|
|
13
54
|
return path.join(cacheDirectory, pidFileName);
|
|
@@ -44,13 +85,7 @@ export async function setupPidFile({
|
|
|
44
85
|
return null;
|
|
45
86
|
}
|
|
46
87
|
|
|
47
|
-
|
|
48
|
-
try {
|
|
49
|
-
fsSync.unlinkSync(pidPath);
|
|
50
|
-
} catch {}
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
process.on('exit', cleanup);
|
|
88
|
+
registerPidFileCleanup(pidPath);
|
|
54
89
|
return pidPath;
|
|
55
90
|
}
|
|
56
91
|
|
|
@@ -71,7 +106,8 @@ function isProcessRunning(pid) {
|
|
|
71
106
|
}
|
|
72
107
|
}
|
|
73
108
|
|
|
74
|
-
export async function acquireWorkspaceLock({ cacheDirectory, workspaceDir = null } = {}) {
|
|
109
|
+
export async function acquireWorkspaceLock({ cacheDirectory, workspaceDir = null, _retryCount = 0 } = {}) {
|
|
110
|
+
const MAX_LOCK_RETRIES = 3;
|
|
75
111
|
if (!cacheDirectory || isTestEnv()) {
|
|
76
112
|
return { acquired: true, lockPath: null };
|
|
77
113
|
}
|
|
@@ -137,24 +173,16 @@ export async function acquireWorkspaceLock({ cacheDirectory, workspaceDir = null
|
|
|
137
173
|
} else {
|
|
138
174
|
await fs.unlink(lockPath).catch(() => {});
|
|
139
175
|
}
|
|
140
|
-
|
|
176
|
+
if (_retryCount >= MAX_LOCK_RETRIES) {
|
|
177
|
+
console.warn(`[Server] Lock acquisition failed after ${MAX_LOCK_RETRIES} retries; starting in secondary mode.`);
|
|
178
|
+
return { acquired: false, lockPath, ownerPid: null };
|
|
179
|
+
}
|
|
180
|
+
return acquireWorkspaceLock({ cacheDirectory, workspaceDir, _retryCount: _retryCount + 1 });
|
|
141
181
|
}
|
|
142
182
|
throw err;
|
|
143
183
|
}
|
|
144
184
|
|
|
145
|
-
|
|
146
|
-
try {
|
|
147
|
-
const raw = fsSync.readFileSync(lockPath, 'utf-8');
|
|
148
|
-
const current = JSON.parse(raw);
|
|
149
|
-
if (current && current.pid === process.pid) {
|
|
150
|
-
fsSync.unlinkSync(lockPath);
|
|
151
|
-
}
|
|
152
|
-
} catch {}
|
|
153
|
-
};
|
|
154
|
-
|
|
155
|
-
process.on('exit', cleanup);
|
|
156
|
-
process.on('SIGINT', cleanup);
|
|
157
|
-
process.on('SIGTERM', cleanup);
|
|
185
|
+
registerWorkspaceLockCleanup(lockPath);
|
|
158
186
|
|
|
159
187
|
return { acquired: true, lockPath };
|
|
160
188
|
}
|
|
@@ -169,6 +197,7 @@ export async function releaseWorkspaceLock({ cacheDirectory } = {}) {
|
|
|
169
197
|
const current = JSON.parse(raw);
|
|
170
198
|
if (current && current.pid === process.pid) {
|
|
171
199
|
await fs.unlink(lockPath).catch(() => {});
|
|
200
|
+
workspaceLocksToCleanup.delete(lockPath);
|
|
172
201
|
}
|
|
173
202
|
} catch {}
|
|
174
203
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@softerist/heuristic-mcp",
|
|
3
|
-
"version": "3.2.
|
|
3
|
+
"version": "3.2.5",
|
|
4
4
|
"description": "An enhanced MCP server providing intelligent semantic code search with find-similar-code, recency ranking, and improved chunking. Fork of smart-coding-mcp.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|