@softerist/heuristic-mcp 3.2.4 → 3.2.6
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/find-similar-code.js +19 -0
- package/features/hybrid-search.js +20 -1
- package/features/index-codebase.js +7 -1
- package/features/set-workspace.js +6 -2
- package/index.js +210 -78
- package/lib/cache.js +9 -10
- package/lib/embed-query-process.js +82 -32
- package/lib/embedding-worker.js +3 -2
- package/lib/logging.js +24 -9
- package/lib/server-lifecycle.js +86 -24
- package/lib/utils.js +4 -2
- package/package.json +1 -1
|
@@ -1,6 +1,24 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
2
|
import { dotSimilarity, smartChunk, estimateTokens, getModelTokenLimit } from '../lib/utils.js';
|
|
3
3
|
|
|
4
|
+
function alignQueryVectorDimension(vector, targetDim) {
|
|
5
|
+
if (!(vector instanceof Float32Array)) {
|
|
6
|
+
vector = new Float32Array(vector);
|
|
7
|
+
}
|
|
8
|
+
if (!Number.isInteger(targetDim) || targetDim <= 0 || vector.length <= targetDim) {
|
|
9
|
+
return vector;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const sliced = vector.slice(0, targetDim);
|
|
13
|
+
let mag = 0;
|
|
14
|
+
for (let i = 0; i < sliced.length; i += 1) mag += sliced[i] * sliced[i];
|
|
15
|
+
mag = Math.sqrt(mag);
|
|
16
|
+
if (mag > 0) {
|
|
17
|
+
for (let i = 0; i < sliced.length; i += 1) sliced[i] /= mag;
|
|
18
|
+
}
|
|
19
|
+
return sliced;
|
|
20
|
+
}
|
|
21
|
+
|
|
4
22
|
export class FindSimilarCode {
|
|
5
23
|
constructor(embedder, cache, config) {
|
|
6
24
|
this.embedder = embedder;
|
|
@@ -84,6 +102,7 @@ export class FindSimilarCode {
|
|
|
84
102
|
} catch {}
|
|
85
103
|
}
|
|
86
104
|
}
|
|
105
|
+
codeVector = alignQueryVectorDimension(codeVector, this.config.embeddingDimension);
|
|
87
106
|
|
|
88
107
|
let candidates = vectorStore;
|
|
89
108
|
let usedAnn = false;
|
|
@@ -9,6 +9,24 @@ import {
|
|
|
9
9
|
PARTIAL_MATCH_BOOST,
|
|
10
10
|
} from '../lib/constants.js';
|
|
11
11
|
|
|
12
|
+
function alignQueryVectorDimension(vector, targetDim) {
|
|
13
|
+
if (!(vector instanceof Float32Array)) {
|
|
14
|
+
vector = new Float32Array(vector);
|
|
15
|
+
}
|
|
16
|
+
if (!Number.isInteger(targetDim) || targetDim <= 0 || vector.length <= targetDim) {
|
|
17
|
+
return vector;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const sliced = vector.slice(0, targetDim);
|
|
21
|
+
let mag = 0;
|
|
22
|
+
for (let i = 0; i < sliced.length; i += 1) mag += sliced[i] * sliced[i];
|
|
23
|
+
mag = Math.sqrt(mag);
|
|
24
|
+
if (mag > 0) {
|
|
25
|
+
for (let i = 0; i < sliced.length; i += 1) sliced[i] /= mag;
|
|
26
|
+
}
|
|
27
|
+
return sliced;
|
|
28
|
+
}
|
|
29
|
+
|
|
12
30
|
export class HybridSearch {
|
|
13
31
|
constructor(embedder, cache, config) {
|
|
14
32
|
this.embedder = embedder;
|
|
@@ -134,6 +152,7 @@ export class HybridSearch {
|
|
|
134
152
|
}
|
|
135
153
|
}
|
|
136
154
|
}
|
|
155
|
+
queryVector = alignQueryVectorDimension(queryVector, this.config.embeddingDimension);
|
|
137
156
|
|
|
138
157
|
let candidateIndices = null;
|
|
139
158
|
let usedAnn = false;
|
|
@@ -336,7 +355,7 @@ export class HybridSearch {
|
|
|
336
355
|
for (let k = 0; k < queryWordCount; k++) {
|
|
337
356
|
if (lowerContent.includes(queryWords[k])) matchedWords++;
|
|
338
357
|
}
|
|
339
|
-
chunk.score += (matchedWords / queryWordCount) *
|
|
358
|
+
chunk.score += (matchedWords / queryWordCount) * PARTIAL_MATCH_BOOST;
|
|
340
359
|
}
|
|
341
360
|
|
|
342
361
|
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,
|
|
@@ -167,6 +167,9 @@ export class SetWorkspaceFeature {
|
|
|
167
167
|
|
|
168
168
|
if (this.cache && typeof this.cache.load === 'function') {
|
|
169
169
|
try {
|
|
170
|
+
if (typeof this.cache.clearInMemoryState === 'function') {
|
|
171
|
+
this.cache.clearInMemoryState();
|
|
172
|
+
}
|
|
170
173
|
if (this.config.vectorStoreFormat === 'binary') {
|
|
171
174
|
await cleanupStaleBinaryArtifacts(newCacheDir);
|
|
172
175
|
}
|
|
@@ -201,11 +204,12 @@ export class SetWorkspaceFeature {
|
|
|
201
204
|
}
|
|
202
205
|
|
|
203
206
|
export function createHandleToolCall(featureInstance) {
|
|
204
|
-
return async (request) => {
|
|
207
|
+
return async (request, instance) => {
|
|
208
|
+
const activeInstance = instance ?? featureInstance;
|
|
205
209
|
const args = request.params?.arguments || {};
|
|
206
210
|
const { workspacePath, reindex } = args;
|
|
207
211
|
|
|
208
|
-
const result = await
|
|
212
|
+
const result = await activeInstance.execute({
|
|
209
213
|
workspacePath,
|
|
210
214
|
reindex: reindex !== false,
|
|
211
215
|
});
|
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
|
}
|
|
@@ -50,6 +47,7 @@ import {
|
|
|
50
47
|
registerSignalHandlers,
|
|
51
48
|
setupPidFile,
|
|
52
49
|
acquireWorkspaceLock,
|
|
50
|
+
releaseWorkspaceLock,
|
|
53
51
|
stopOtherHeuristicServers,
|
|
54
52
|
} from './lib/server-lifecycle.js';
|
|
55
53
|
|
|
@@ -79,6 +77,13 @@ import {
|
|
|
79
77
|
} from './lib/constants.js';
|
|
80
78
|
const PID_FILE_NAME = '.heuristic-mcp.pid';
|
|
81
79
|
|
|
80
|
+
function isTestRuntime() {
|
|
81
|
+
return (
|
|
82
|
+
process.env.VITEST === 'true' ||
|
|
83
|
+
process.env.NODE_ENV === 'test'
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
82
87
|
async function readLogTail(logPath, maxLines = 2000) {
|
|
83
88
|
const data = await fs.readFile(logPath, 'utf-8');
|
|
84
89
|
if (!data) return [];
|
|
@@ -134,6 +139,36 @@ async function printMemorySnapshot(workspaceDir) {
|
|
|
134
139
|
return true;
|
|
135
140
|
}
|
|
136
141
|
|
|
142
|
+
async function flushLogsSafely(options) {
|
|
143
|
+
if (typeof flushLogs !== 'function') {
|
|
144
|
+
console.warn('[Logs] flushLogs helper is unavailable; skipping log flush.');
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
await flushLogs(options);
|
|
150
|
+
} catch (error) {
|
|
151
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
152
|
+
console.warn(`[Logs] Failed to flush logs: ${message}`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function assertCacheContract(cacheInstance) {
|
|
157
|
+
const requiredMethods = [
|
|
158
|
+
'load',
|
|
159
|
+
'save',
|
|
160
|
+
'consumeAutoReindex',
|
|
161
|
+
'clearInMemoryState',
|
|
162
|
+
'getStoreSize',
|
|
163
|
+
];
|
|
164
|
+
const missing = requiredMethods.filter((name) => typeof cacheInstance?.[name] !== 'function');
|
|
165
|
+
if (missing.length > 0) {
|
|
166
|
+
throw new Error(
|
|
167
|
+
`[Server] Cache implementation contract violation: missing method(s): ${missing.join(', ')}`
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
137
172
|
let embedder = null;
|
|
138
173
|
let unloadMainEmbedder = null;
|
|
139
174
|
let cache = null;
|
|
@@ -150,6 +185,10 @@ let setWorkspaceFeatureInstance = null;
|
|
|
150
185
|
let autoWorkspaceSwitchPromise = null;
|
|
151
186
|
let rootsCapabilitySupported = null;
|
|
152
187
|
let rootsProbeInFlight = null;
|
|
188
|
+
let lastRootsProbeTime = 0;
|
|
189
|
+
let keepAliveTimer = null;
|
|
190
|
+
let stdioShutdownHandlers = null;
|
|
191
|
+
const ROOTS_PROBE_COOLDOWN_MS = 2000;
|
|
153
192
|
const WORKSPACE_BOUND_TOOL_NAMES = new Set([
|
|
154
193
|
'a_semantic_search',
|
|
155
194
|
'b_index_codebase',
|
|
@@ -202,11 +241,22 @@ function formatCrashDetail(detail) {
|
|
|
202
241
|
}
|
|
203
242
|
}
|
|
204
243
|
|
|
244
|
+
function isBrokenPipeError(detail) {
|
|
245
|
+
return Boolean(detail && typeof detail === 'object' && detail.code === 'EPIPE');
|
|
246
|
+
}
|
|
247
|
+
|
|
205
248
|
function isCrashShutdownReason(reason) {
|
|
206
249
|
const normalized = String(reason || '').toLowerCase();
|
|
207
250
|
return normalized.includes('uncaughtexception') || normalized.includes('unhandledrejection');
|
|
208
251
|
}
|
|
209
252
|
|
|
253
|
+
function getShutdownExitCode(reason) {
|
|
254
|
+
const normalized = String(reason || '').trim().toUpperCase();
|
|
255
|
+
if (normalized === 'SIGINT') return 130;
|
|
256
|
+
if (normalized === 'SIGTERM') return 143;
|
|
257
|
+
return isCrashShutdownReason(reason) ? 1 : 0;
|
|
258
|
+
}
|
|
259
|
+
|
|
210
260
|
function registerProcessDiagnostics({ isServerMode, requestShutdown, getShutdownReason }) {
|
|
211
261
|
if (!isServerMode) return;
|
|
212
262
|
|
|
@@ -223,6 +273,10 @@ function registerProcessDiagnostics({ isServerMode, requestShutdown, getShutdown
|
|
|
223
273
|
let fatalHandled = false;
|
|
224
274
|
const handleFatalError = (reason, detail) => {
|
|
225
275
|
if (fatalHandled) return;
|
|
276
|
+
if (isBrokenPipeError(detail)) {
|
|
277
|
+
requestShutdown('stdio-epipe');
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
226
280
|
fatalHandled = true;
|
|
227
281
|
console.error(`[Server] Fatal ${reason}: ${formatCrashDetail(detail)}`);
|
|
228
282
|
requestShutdown(reason);
|
|
@@ -241,6 +295,44 @@ function registerProcessDiagnostics({ isServerMode, requestShutdown, getShutdown
|
|
|
241
295
|
});
|
|
242
296
|
}
|
|
243
297
|
|
|
298
|
+
function registerStdioShutdownHandlers(requestShutdown) {
|
|
299
|
+
if (stdioShutdownHandlers) return;
|
|
300
|
+
|
|
301
|
+
const onStdinEnd = () => requestShutdown('stdin-end');
|
|
302
|
+
const onStdinClose = () => requestShutdown('stdin-close');
|
|
303
|
+
const onStdoutError = (err) => {
|
|
304
|
+
if (err?.code === 'EPIPE') {
|
|
305
|
+
requestShutdown('stdout-epipe');
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
const onStderrError = (err) => {
|
|
309
|
+
if (err?.code === 'EPIPE') {
|
|
310
|
+
requestShutdown('stderr-epipe');
|
|
311
|
+
}
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
process.stdin?.on?.('end', onStdinEnd);
|
|
315
|
+
process.stdin?.on?.('close', onStdinClose);
|
|
316
|
+
process.stdout?.on?.('error', onStdoutError);
|
|
317
|
+
process.stderr?.on?.('error', onStderrError);
|
|
318
|
+
|
|
319
|
+
stdioShutdownHandlers = {
|
|
320
|
+
onStdinEnd,
|
|
321
|
+
onStdinClose,
|
|
322
|
+
onStdoutError,
|
|
323
|
+
onStderrError,
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function unregisterStdioShutdownHandlers() {
|
|
328
|
+
if (!stdioShutdownHandlers) return;
|
|
329
|
+
process.stdin?.off?.('end', stdioShutdownHandlers.onStdinEnd);
|
|
330
|
+
process.stdin?.off?.('close', stdioShutdownHandlers.onStdinClose);
|
|
331
|
+
process.stdout?.off?.('error', stdioShutdownHandlers.onStdoutError);
|
|
332
|
+
process.stderr?.off?.('error', stdioShutdownHandlers.onStderrError);
|
|
333
|
+
stdioShutdownHandlers = null;
|
|
334
|
+
}
|
|
335
|
+
|
|
244
336
|
async function resolveWorkspaceFromEnvValue(rawValue) {
|
|
245
337
|
if (!rawValue || rawValue.includes('${')) return null;
|
|
246
338
|
const resolved = path.resolve(rawValue);
|
|
@@ -460,12 +552,15 @@ async function maybeAutoSwitchWorkspaceToPath(
|
|
|
460
552
|
|
|
461
553
|
if (autoWorkspaceSwitchPromise) {
|
|
462
554
|
await autoWorkspaceSwitchPromise;
|
|
463
|
-
|
|
555
|
+
const currentNow = normalizePathForCompare(config.searchDirectory);
|
|
556
|
+
const targetNow = normalizePathForCompare(targetWorkspacePath);
|
|
557
|
+
if (targetNow === currentNow) return;
|
|
464
558
|
}
|
|
465
559
|
|
|
466
|
-
|
|
560
|
+
const switchPromise = (async () => {
|
|
561
|
+
const latestWorkspace = normalizePathForCompare(config.searchDirectory);
|
|
467
562
|
console.info(
|
|
468
|
-
`[Server] Auto-switching workspace from ${
|
|
563
|
+
`[Server] Auto-switching workspace from ${latestWorkspace} to ${targetWorkspacePath} (${source || 'auto'})`
|
|
469
564
|
);
|
|
470
565
|
const result = await setWorkspaceFeatureInstance.execute({
|
|
471
566
|
workspacePath: targetWorkspacePath,
|
|
@@ -477,11 +572,14 @@ async function maybeAutoSwitchWorkspaceToPath(
|
|
|
477
572
|
}
|
|
478
573
|
trustWorkspacePath(targetWorkspacePath);
|
|
479
574
|
})();
|
|
575
|
+
autoWorkspaceSwitchPromise = switchPromise;
|
|
480
576
|
|
|
481
577
|
try {
|
|
482
|
-
await
|
|
578
|
+
await switchPromise;
|
|
483
579
|
} finally {
|
|
484
|
-
autoWorkspaceSwitchPromise
|
|
580
|
+
if (autoWorkspaceSwitchPromise === switchPromise) {
|
|
581
|
+
autoWorkspaceSwitchPromise = null;
|
|
582
|
+
}
|
|
485
583
|
}
|
|
486
584
|
}
|
|
487
585
|
|
|
@@ -494,6 +592,9 @@ async function maybeAutoSwitchWorkspaceFromRoots(request) {
|
|
|
494
592
|
if (rootsProbeInFlight) {
|
|
495
593
|
return await rootsProbeInFlight;
|
|
496
594
|
}
|
|
595
|
+
const now = Date.now();
|
|
596
|
+
if (now - lastRootsProbeTime < ROOTS_PROBE_COOLDOWN_MS) return null;
|
|
597
|
+
lastRootsProbeTime = now;
|
|
497
598
|
|
|
498
599
|
rootsProbeInFlight = (async () => {
|
|
499
600
|
const rootWorkspace = await detectWorkspaceFromRoots({ quiet: true });
|
|
@@ -566,7 +667,7 @@ async function initialize(workspaceDir) {
|
|
|
566
667
|
}
|
|
567
668
|
}
|
|
568
669
|
|
|
569
|
-
const isTest =
|
|
670
|
+
const isTest = isTestRuntime();
|
|
570
671
|
if (config.enableExplicitGc && typeof global.gc !== 'function' && !isTest) {
|
|
571
672
|
console.warn(
|
|
572
673
|
'[Server] enableExplicitGc=true but this process was not started with --expose-gc; continuing with explicit GC disabled.'
|
|
@@ -820,6 +921,7 @@ async function initialize(workspaceDir) {
|
|
|
820
921
|
}
|
|
821
922
|
|
|
822
923
|
cache = new EmbeddingsCache(config);
|
|
924
|
+
assertCacheContract(cache);
|
|
823
925
|
console.info(`[Server] Cache directory: ${config.cacheDirectory}`);
|
|
824
926
|
|
|
825
927
|
indexer = new CodebaseIndexer(embedder, cache, config, server);
|
|
@@ -865,7 +967,7 @@ async function initialize(workspaceDir) {
|
|
|
865
967
|
);
|
|
866
968
|
} else {
|
|
867
969
|
console.warn(
|
|
868
|
-
`[Server] Cache corruption detected while ${context}. This server is secondary read-only and cannot re-index.
|
|
970
|
+
`[Server] Cache corruption detected while ${context}. This server is secondary read-only and cannot re-index. Restart the MCP client session for this workspace or use the primary instance to rebuild the cache.`
|
|
869
971
|
);
|
|
870
972
|
}
|
|
871
973
|
return true;
|
|
@@ -936,7 +1038,8 @@ async function initialize(workspaceDir) {
|
|
|
936
1038
|
context: 'loading cache in secondary read-only mode',
|
|
937
1039
|
canReindex: false,
|
|
938
1040
|
});
|
|
939
|
-
|
|
1041
|
+
const storeSize = cache.getStoreSize();
|
|
1042
|
+
if (storeSize === 0) {
|
|
940
1043
|
await tryAutoAttachWorkspaceCache('secondary-empty-cache', { canReindex: false });
|
|
941
1044
|
}
|
|
942
1045
|
if (config.verbose) {
|
|
@@ -1107,7 +1210,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1107
1210
|
content: [
|
|
1108
1211
|
{
|
|
1109
1212
|
type: 'text',
|
|
1110
|
-
text: `Attached cache for ${normalizedPath}, but it is corrupt. This secondary read-only instance cannot rebuild it.
|
|
1213
|
+
text: `Attached cache for ${normalizedPath}, but it is corrupt. This secondary read-only instance cannot rebuild it. Restart the MCP client session for this workspace or run indexing from the primary instance.`,
|
|
1111
1214
|
},
|
|
1112
1215
|
],
|
|
1113
1216
|
isError: true,
|
|
@@ -1217,10 +1320,15 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1217
1320
|
|
|
1218
1321
|
const searchTools = ['a_semantic_search', 'd_find_similar_code'];
|
|
1219
1322
|
if (config.unloadModelAfterSearch && searchTools.includes(toolDef.name)) {
|
|
1220
|
-
setImmediate(
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1323
|
+
setImmediate(() => {
|
|
1324
|
+
const unloadFn = unloadMainEmbedder;
|
|
1325
|
+
if (typeof unloadFn !== 'function') return;
|
|
1326
|
+
void Promise.resolve()
|
|
1327
|
+
.then(() => unloadFn())
|
|
1328
|
+
.catch((err) => {
|
|
1329
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1330
|
+
console.warn(`[Server] Post-search model unload failed: ${message}`);
|
|
1331
|
+
});
|
|
1224
1332
|
});
|
|
1225
1333
|
}
|
|
1226
1334
|
|
|
@@ -1270,13 +1378,14 @@ export async function main(argv = process.argv) {
|
|
|
1270
1378
|
console.info(`[Server] Shutdown requested (${reason}).`);
|
|
1271
1379
|
void gracefulShutdown(reason);
|
|
1272
1380
|
};
|
|
1381
|
+
const isTestEnv = isTestRuntime();
|
|
1273
1382
|
registerProcessDiagnostics({
|
|
1274
1383
|
isServerMode,
|
|
1275
1384
|
requestShutdown,
|
|
1276
1385
|
getShutdownReason: () => shutdownReason,
|
|
1277
1386
|
});
|
|
1278
1387
|
|
|
1279
|
-
if (isServerMode && !
|
|
1388
|
+
if (isServerMode && !isTestEnv) {
|
|
1280
1389
|
enableStderrOnlyLogging();
|
|
1281
1390
|
}
|
|
1282
1391
|
if (wantsVersion) {
|
|
@@ -1403,41 +1512,37 @@ export async function main(argv = process.argv) {
|
|
|
1403
1512
|
|
|
1404
1513
|
registerSignalHandlers(requestShutdown);
|
|
1405
1514
|
|
|
1406
|
-
const detectedRootPromise =
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1515
|
+
const detectedRootPromise = isTestEnv
|
|
1516
|
+
? Promise.resolve(null)
|
|
1517
|
+
: new Promise((resolve) => {
|
|
1518
|
+
const HANDSHAKE_TIMEOUT_MS = 1000;
|
|
1519
|
+
let settled = false;
|
|
1520
|
+
const resolveOnce = (value) => {
|
|
1521
|
+
if (settled) return;
|
|
1522
|
+
settled = true;
|
|
1523
|
+
resolve(value);
|
|
1524
|
+
};
|
|
1414
1525
|
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1526
|
+
const timer = setTimeout(() => {
|
|
1527
|
+
console.warn(
|
|
1528
|
+
`[Server] MCP handshake timed out after ${HANDSHAKE_TIMEOUT_MS}ms, proceeding without roots.`
|
|
1529
|
+
);
|
|
1530
|
+
resolveOnce(null);
|
|
1531
|
+
}, HANDSHAKE_TIMEOUT_MS);
|
|
1532
|
+
|
|
1533
|
+
server.oninitialized = async () => {
|
|
1534
|
+
clearTimeout(timer);
|
|
1535
|
+
console.info('[Server] MCP handshake complete.');
|
|
1536
|
+
const root = await detectWorkspaceFromRoots();
|
|
1537
|
+
resolveOnce(root);
|
|
1538
|
+
};
|
|
1539
|
+
});
|
|
1429
1540
|
|
|
1430
1541
|
const transport = new StdioServerTransport();
|
|
1431
1542
|
await server.connect(transport);
|
|
1432
1543
|
console.info('[Server] MCP transport connected.');
|
|
1433
1544
|
if (isServerMode) {
|
|
1434
|
-
|
|
1435
|
-
process.stdin?.on?.('close', () => requestShutdown('stdin-close'));
|
|
1436
|
-
process.stdout?.on?.('error', (err) => {
|
|
1437
|
-
if (err?.code === 'EPIPE') {
|
|
1438
|
-
requestShutdown('stdout-epipe');
|
|
1439
|
-
}
|
|
1440
|
-
});
|
|
1545
|
+
registerStdioShutdownHandlers(requestShutdown);
|
|
1441
1546
|
}
|
|
1442
1547
|
|
|
1443
1548
|
const detectedRoot = await detectedRootPromise;
|
|
@@ -1446,28 +1551,28 @@ export async function main(argv = process.argv) {
|
|
|
1446
1551
|
if (detectedRoot) {
|
|
1447
1552
|
console.info(`[Server] Using workspace from MCP roots: ${detectedRoot}`);
|
|
1448
1553
|
}
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
throw err;
|
|
1459
|
-
});
|
|
1460
|
-
const { startBackgroundTasks } = await initWithResolve;
|
|
1554
|
+
let startBackgroundTasks;
|
|
1555
|
+
try {
|
|
1556
|
+
const initResult = await initialize(effectiveWorkspace);
|
|
1557
|
+
startBackgroundTasks = initResult.startBackgroundTasks;
|
|
1558
|
+
} catch (err) {
|
|
1559
|
+
configInitError = err;
|
|
1560
|
+
configReadyResolve();
|
|
1561
|
+
throw err;
|
|
1562
|
+
}
|
|
1461
1563
|
|
|
1462
1564
|
console.info('[Server] Heuristic MCP server started.');
|
|
1463
1565
|
|
|
1464
|
-
|
|
1566
|
+
try {
|
|
1567
|
+
await startBackgroundTasks();
|
|
1568
|
+
} catch (err) {
|
|
1465
1569
|
console.error(`[Server] Background task error: ${err.message}`);
|
|
1466
|
-
}
|
|
1570
|
+
}
|
|
1571
|
+
configReadyResolve();
|
|
1467
1572
|
// Keep-Alive mechanism: ensure the process stays alive even if StdioServerTransport
|
|
1468
1573
|
// temporarily loses its active handle status or during complex async chains.
|
|
1469
|
-
if (isServerMode) {
|
|
1470
|
-
setInterval(() => {
|
|
1574
|
+
if (isServerMode && !isTestEnv && !keepAliveTimer) {
|
|
1575
|
+
keepAliveTimer = setInterval(() => {
|
|
1471
1576
|
// Logic to keep event loop active.
|
|
1472
1577
|
// We don't need to do anything, just the presence of the timer is enough.
|
|
1473
1578
|
}, SERVER_KEEP_ALIVE_INTERVAL_MS);
|
|
@@ -1478,7 +1583,14 @@ export async function main(argv = process.argv) {
|
|
|
1478
1583
|
|
|
1479
1584
|
async function gracefulShutdown(signal) {
|
|
1480
1585
|
console.info(`[Server] Received ${signal}, shutting down gracefully...`);
|
|
1481
|
-
const exitCode =
|
|
1586
|
+
const exitCode = getShutdownExitCode(signal);
|
|
1587
|
+
|
|
1588
|
+
if (keepAliveTimer) {
|
|
1589
|
+
clearInterval(keepAliveTimer);
|
|
1590
|
+
keepAliveTimer = null;
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
unregisterStdioShutdownHandlers();
|
|
1482
1594
|
|
|
1483
1595
|
const cleanupTasks = [];
|
|
1484
1596
|
|
|
@@ -1502,37 +1614,57 @@ async function gracefulShutdown(signal) {
|
|
|
1502
1614
|
}
|
|
1503
1615
|
|
|
1504
1616
|
if (cache) {
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
.save()
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1617
|
+
cleanupTasks.push(
|
|
1618
|
+
(async () => {
|
|
1619
|
+
if (!workspaceLockAcquired) {
|
|
1620
|
+
console.info('[Server] Secondary/fallback mode: skipping cache save.');
|
|
1621
|
+
} else {
|
|
1622
|
+
await cache.save();
|
|
1623
|
+
console.info('[Server] Cache saved');
|
|
1624
|
+
}
|
|
1625
|
+
if (typeof cache.close === 'function') {
|
|
1626
|
+
await cache.close();
|
|
1627
|
+
}
|
|
1628
|
+
})().catch((err) => console.error(`[Server] Cache shutdown cleanup failed: ${err.message}`))
|
|
1629
|
+
);
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
if (workspaceLockAcquired && config?.cacheDirectory) {
|
|
1633
|
+
cleanupTasks.push(
|
|
1634
|
+
releaseWorkspaceLock({ cacheDirectory: config.cacheDirectory }).catch((err) =>
|
|
1635
|
+
console.warn(`[Server] Failed to release workspace lock: ${err.message}`)
|
|
1636
|
+
)
|
|
1637
|
+
);
|
|
1515
1638
|
}
|
|
1516
1639
|
|
|
1517
1640
|
await Promise.allSettled(cleanupTasks);
|
|
1518
1641
|
console.info('[Server] Goodbye!');
|
|
1519
|
-
await
|
|
1642
|
+
await flushLogsSafely({ close: true, timeoutMs: 1500 });
|
|
1520
1643
|
|
|
1521
1644
|
setTimeout(() => process.exit(exitCode), 100);
|
|
1522
1645
|
}
|
|
1523
1646
|
|
|
1647
|
+
function isLikelyCliEntrypoint(argvPath) {
|
|
1648
|
+
const base = path.basename(argvPath || '').toLowerCase();
|
|
1649
|
+
return (
|
|
1650
|
+
base === 'heuristic-mcp' ||
|
|
1651
|
+
base === 'heuristic-mcp.js' ||
|
|
1652
|
+
base === 'heuristic-mcp.mjs' ||
|
|
1653
|
+
base === 'heuristic-mcp.cjs' ||
|
|
1654
|
+
base === 'heuristic-mcp.cmd'
|
|
1655
|
+
);
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1524
1658
|
const isMain =
|
|
1525
1659
|
process.argv[1] &&
|
|
1526
1660
|
(path.resolve(process.argv[1]).toLowerCase() === fileURLToPath(import.meta.url).toLowerCase() ||
|
|
1527
|
-
process.argv[1]
|
|
1528
|
-
process.argv[1].endsWith('heuristic-mcp.js') ||
|
|
1529
|
-
path.basename(process.argv[1]) === 'index.js') &&
|
|
1661
|
+
isLikelyCliEntrypoint(process.argv[1])) &&
|
|
1530
1662
|
!(process.env.VITEST === 'true' || process.env.NODE_ENV === 'test');
|
|
1531
1663
|
|
|
1532
1664
|
if (isMain) {
|
|
1533
1665
|
main().catch(async (err) => {
|
|
1534
1666
|
console.error(err);
|
|
1535
|
-
await
|
|
1667
|
+
await flushLogsSafely({ close: true, timeoutMs: 500 });
|
|
1536
1668
|
process.exit(1);
|
|
1537
1669
|
});
|
|
1538
1670
|
}
|
package/lib/cache.js
CHANGED
|
@@ -223,14 +223,7 @@ function normalizeFileHashEntry(entry) {
|
|
|
223
223
|
}
|
|
224
224
|
|
|
225
225
|
function serializeFileHashEntry(entry) {
|
|
226
|
-
|
|
227
|
-
if (typeof entry === 'string') return { hash: entry };
|
|
228
|
-
if (typeof entry !== 'object') return null;
|
|
229
|
-
if (typeof entry.hash !== 'string') return null;
|
|
230
|
-
const serialized = { hash: entry.hash };
|
|
231
|
-
if (Number.isFinite(entry.mtimeMs)) serialized.mtimeMs = entry.mtimeMs;
|
|
232
|
-
if (Number.isFinite(entry.size)) serialized.size = entry.size;
|
|
233
|
-
return serialized;
|
|
226
|
+
return normalizeFileHashEntry(entry);
|
|
234
227
|
}
|
|
235
228
|
|
|
236
229
|
function computeAnnCapacity(total, config) {
|
|
@@ -444,7 +437,7 @@ export class EmbeddingsCache {
|
|
|
444
437
|
await Promise.race([
|
|
445
438
|
waiterPromise,
|
|
446
439
|
new Promise((resolve) => {
|
|
447
|
-
setTimeout(() => {
|
|
440
|
+
const timer = setTimeout(() => {
|
|
448
441
|
if (!resolved) {
|
|
449
442
|
resolved = true;
|
|
450
443
|
timedOut = true;
|
|
@@ -454,6 +447,7 @@ export class EmbeddingsCache {
|
|
|
454
447
|
resolve();
|
|
455
448
|
}
|
|
456
449
|
}, timeoutMs);
|
|
450
|
+
timer.unref?.();
|
|
457
451
|
}),
|
|
458
452
|
]);
|
|
459
453
|
if (timedOut) {
|
|
@@ -789,6 +783,7 @@ export class EmbeddingsCache {
|
|
|
789
783
|
this._savePromise = null;
|
|
790
784
|
});
|
|
791
785
|
}, debounceMs);
|
|
786
|
+
this._saveTimer.unref?.();
|
|
792
787
|
});
|
|
793
788
|
|
|
794
789
|
return this._savePromise;
|
|
@@ -1528,7 +1523,11 @@ export class EmbeddingsCache {
|
|
|
1528
1523
|
if (labels.length === 0) return [];
|
|
1529
1524
|
|
|
1530
1525
|
const filtered = labels.filter(
|
|
1531
|
-
(label) =>
|
|
1526
|
+
(label) =>
|
|
1527
|
+
Number.isInteger(label) &&
|
|
1528
|
+
label >= 0 &&
|
|
1529
|
+
label < this.vectorStore.length &&
|
|
1530
|
+
Boolean(this.vectorStore[label])
|
|
1532
1531
|
);
|
|
1533
1532
|
|
|
1534
1533
|
return filtered;
|
|
@@ -13,6 +13,7 @@ let idleTimer = null;
|
|
|
13
13
|
let currentConfig = null;
|
|
14
14
|
let pendingRequests = [];
|
|
15
15
|
let isProcessingRequest = false;
|
|
16
|
+
let activeRequest = null;
|
|
16
17
|
|
|
17
18
|
const DEFAULT_IDLE_TIMEOUT_MS = EMBEDDING_POOL_IDLE_TIMEOUT_MS;
|
|
18
19
|
|
|
@@ -43,36 +44,59 @@ function getOrCreateChild(config) {
|
|
|
43
44
|
childReadline.on('line', (line) => {
|
|
44
45
|
if (!line.trim()) return;
|
|
45
46
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
47
|
+
let result;
|
|
48
|
+
try {
|
|
49
|
+
result = JSON.parse(line);
|
|
50
|
+
} catch {
|
|
51
|
+
if (currentConfig?.verbose) {
|
|
52
|
+
console.warn('[EmbedPool] Ignoring non-JSON stdout from embedding child');
|
|
53
|
+
}
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (!result || typeof result !== 'object' || !Object.prototype.hasOwnProperty.call(result, 'results')) {
|
|
58
|
+
if (currentConfig?.verbose) {
|
|
59
|
+
console.warn('[EmbedPool] Ignoring unexpected stdout payload from embedding child');
|
|
60
|
+
}
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (!activeRequest) {
|
|
65
|
+
if (currentConfig?.verbose) {
|
|
66
|
+
console.warn('[EmbedPool] Received embedding response with no active request');
|
|
67
|
+
}
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const { resolve, reject, startTime } = activeRequest;
|
|
72
|
+
activeRequest = null;
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
const elapsed = ((Date.now() - startTime) / 1000).toFixed(2);
|
|
76
|
+
|
|
77
|
+
if (!result.results || result.results.length === 0) {
|
|
78
|
+
reject(new Error('Embedding child process returned no results'));
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const embResult = result.results[0];
|
|
83
|
+
if (!embResult.success) {
|
|
84
|
+
reject(new Error(`Embedding failed: ${embResult.error}`));
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const vector = new Float32Array(embResult.vector);
|
|
89
|
+
|
|
90
|
+
if (currentConfig?.verbose) {
|
|
91
|
+
console.info(`[Search] Query embedding (persistent child) completed in ${elapsed}s`);
|
|
75
92
|
}
|
|
93
|
+
|
|
94
|
+
resolve(vector);
|
|
95
|
+
} catch (err) {
|
|
96
|
+
reject(new Error(`Failed to parse embedding result: ${err.message}`));
|
|
97
|
+
} finally {
|
|
98
|
+
isProcessingRequest = false;
|
|
99
|
+
processNextRequest();
|
|
76
100
|
}
|
|
77
101
|
});
|
|
78
102
|
|
|
@@ -84,8 +108,14 @@ function getOrCreateChild(config) {
|
|
|
84
108
|
|
|
85
109
|
persistentChild.on('error', (err) => {
|
|
86
110
|
console.error(`[EmbedPool] Child process error: ${err.message}`);
|
|
111
|
+
const inflight = activeRequest;
|
|
87
112
|
cleanupChild();
|
|
88
113
|
|
|
114
|
+
if (inflight) {
|
|
115
|
+
inflight.reject(new Error(`Child process error: ${err.message}`));
|
|
116
|
+
activeRequest = null;
|
|
117
|
+
}
|
|
118
|
+
|
|
89
119
|
for (const { reject } of pendingRequests) {
|
|
90
120
|
reject(new Error(`Child process error: ${err.message}`));
|
|
91
121
|
}
|
|
@@ -96,8 +126,14 @@ function getOrCreateChild(config) {
|
|
|
96
126
|
if (currentConfig?.verbose) {
|
|
97
127
|
console.info(`[EmbedPool] Child process exited with code ${code}`);
|
|
98
128
|
}
|
|
129
|
+
const inflight = activeRequest;
|
|
99
130
|
cleanupChild();
|
|
100
131
|
|
|
132
|
+
if (inflight) {
|
|
133
|
+
inflight.reject(new Error(`Child process exited unexpectedly with code ${code}`));
|
|
134
|
+
activeRequest = null;
|
|
135
|
+
}
|
|
136
|
+
|
|
101
137
|
for (const { reject } of pendingRequests) {
|
|
102
138
|
reject(new Error(`Child process exited unexpectedly with code ${code}`));
|
|
103
139
|
}
|
|
@@ -132,6 +168,9 @@ function resetIdleTimer(config) {
|
|
|
132
168
|
shutdownChild();
|
|
133
169
|
}
|
|
134
170
|
}, timeout);
|
|
171
|
+
if (typeof idleTimer.unref === 'function') {
|
|
172
|
+
idleTimer.unref();
|
|
173
|
+
}
|
|
135
174
|
}
|
|
136
175
|
|
|
137
176
|
function cleanupChild() {
|
|
@@ -148,6 +187,15 @@ function cleanupChild() {
|
|
|
148
187
|
}
|
|
149
188
|
|
|
150
189
|
function shutdownChild() {
|
|
190
|
+
if (activeRequest) {
|
|
191
|
+
activeRequest.reject(new Error('Embedding pool shutdown requested'));
|
|
192
|
+
activeRequest = null;
|
|
193
|
+
}
|
|
194
|
+
for (const { reject } of pendingRequests) {
|
|
195
|
+
reject(new Error('Embedding pool shutdown requested'));
|
|
196
|
+
}
|
|
197
|
+
pendingRequests = [];
|
|
198
|
+
|
|
151
199
|
if (persistentChild && !persistentChild.killed) {
|
|
152
200
|
try {
|
|
153
201
|
persistentChild.stdin.write(JSON.stringify({ type: 'shutdown' }) + '\n');
|
|
@@ -159,19 +207,21 @@ function shutdownChild() {
|
|
|
159
207
|
}
|
|
160
208
|
|
|
161
209
|
function processNextRequest() {
|
|
162
|
-
if (isProcessingRequest || pendingRequests.length === 0) return;
|
|
210
|
+
if (isProcessingRequest || activeRequest || pendingRequests.length === 0) return;
|
|
163
211
|
|
|
164
|
-
const request = pendingRequests
|
|
212
|
+
const request = pendingRequests.shift();
|
|
165
213
|
if (!request) return;
|
|
166
214
|
|
|
167
215
|
isProcessingRequest = true;
|
|
216
|
+
activeRequest = request;
|
|
168
217
|
|
|
169
218
|
try {
|
|
170
219
|
const child = getOrCreateChild(request.config);
|
|
171
220
|
child.stdin.write(JSON.stringify(request.payload) + '\n');
|
|
172
221
|
} catch (err) {
|
|
173
|
-
const { reject } =
|
|
222
|
+
const { reject } = request;
|
|
174
223
|
reject(err);
|
|
224
|
+
activeRequest = null;
|
|
175
225
|
isProcessingRequest = false;
|
|
176
226
|
processNextRequest();
|
|
177
227
|
}
|
package/lib/embedding-worker.js
CHANGED
|
@@ -713,10 +713,11 @@ parentPort.on('message', async (message) => {
|
|
|
713
713
|
maybeRunGc();
|
|
714
714
|
}
|
|
715
715
|
|
|
716
|
+
const taskIndex = fileTasks.length;
|
|
716
717
|
if (chunks.length > 0) {
|
|
717
718
|
for (const c of chunks) {
|
|
718
719
|
allPendingChunks.push({
|
|
719
|
-
|
|
720
|
+
fileTaskIndex: taskIndex,
|
|
720
721
|
text: c.text,
|
|
721
722
|
startLine: c.startLine,
|
|
722
723
|
endLine: c.endLine,
|
|
@@ -847,7 +848,7 @@ parentPort.on('message', async (message) => {
|
|
|
847
848
|
|
|
848
849
|
for (const chunkItem of allPendingChunks) {
|
|
849
850
|
if (chunkItem.vectorBuffer) {
|
|
850
|
-
const task = fileTasks[chunkItem.
|
|
851
|
+
const task = fileTasks[chunkItem.fileTaskIndex];
|
|
851
852
|
task.results.push({
|
|
852
853
|
startLine: chunkItem.startLine,
|
|
853
854
|
endLine: chunkItem.endLine,
|
package/lib/logging.js
CHANGED
|
@@ -4,23 +4,39 @@ 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
|
+
// eslint-disable-next-line no-console
|
|
10
|
+
log: console.log,
|
|
9
11
|
warn: console.warn,
|
|
10
12
|
error: console.error,
|
|
11
13
|
info: console.info,
|
|
12
14
|
};
|
|
13
15
|
|
|
16
|
+
function isBrokenPipeError(error) {
|
|
17
|
+
return Boolean(error && typeof error === 'object' && error.code === 'EPIPE');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function writeToOriginalStderr(...args) {
|
|
21
|
+
if (!stderrWritable) return;
|
|
22
|
+
try {
|
|
23
|
+
originalConsole.error(...args);
|
|
24
|
+
} catch (error) {
|
|
25
|
+
if (isBrokenPipeError(error)) {
|
|
26
|
+
stderrWritable = false;
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
throw error;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
14
33
|
export function enableStderrOnlyLogging() {
|
|
15
|
-
const redirect = (...args) =>
|
|
34
|
+
const redirect = (...args) => writeToOriginalStderr(...args);
|
|
16
35
|
// eslint-disable-next-line no-console
|
|
17
36
|
console.log = redirect;
|
|
18
37
|
console.info = redirect;
|
|
19
38
|
console.warn = redirect;
|
|
20
39
|
console.error = redirect;
|
|
21
|
-
// eslint-disable-next-line no-console
|
|
22
|
-
console.log = redirect;
|
|
23
|
-
console.info = redirect;
|
|
24
40
|
}
|
|
25
41
|
|
|
26
42
|
export async function setupFileLogging(config) {
|
|
@@ -49,10 +65,9 @@ export async function setupFileLogging(config) {
|
|
|
49
65
|
};
|
|
50
66
|
|
|
51
67
|
const wrap = (method, level) => {
|
|
52
|
-
const originalError = originalConsole.error;
|
|
53
68
|
// eslint-disable-next-line no-console
|
|
54
69
|
console[method] = (...args) => {
|
|
55
|
-
|
|
70
|
+
writeToOriginalStderr(...args);
|
|
56
71
|
writeLine(level, args);
|
|
57
72
|
};
|
|
58
73
|
};
|
|
@@ -63,7 +78,7 @@ export async function setupFileLogging(config) {
|
|
|
63
78
|
wrap('info', 'INFO');
|
|
64
79
|
|
|
65
80
|
logStream.on('error', (err) => {
|
|
66
|
-
|
|
81
|
+
writeToOriginalStderr(`[Logs] Failed to write log file: ${err.message}`);
|
|
67
82
|
});
|
|
68
83
|
|
|
69
84
|
process.on('exit', () => {
|
|
@@ -72,7 +87,7 @@ export async function setupFileLogging(config) {
|
|
|
72
87
|
|
|
73
88
|
return logPath;
|
|
74
89
|
} catch (err) {
|
|
75
|
-
|
|
90
|
+
writeToOriginalStderr(`[Logs] Failed to initialize log file: ${err.message}`);
|
|
76
91
|
return null;
|
|
77
92
|
}
|
|
78
93
|
}
|
package/lib/server-lifecycle.js
CHANGED
|
@@ -4,10 +4,54 @@ 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
|
+
let registeredSignalHandlers = null;
|
|
12
|
+
const LOCK_RETRY_BASE_DELAY_MS = 50;
|
|
13
|
+
const LOCK_RETRY_MAX_DELAY_MS = 500;
|
|
14
|
+
|
|
7
15
|
function isTestEnv() {
|
|
8
16
|
return process.env.VITEST === 'true' || process.env.NODE_ENV === 'test';
|
|
9
17
|
}
|
|
10
18
|
|
|
19
|
+
function cleanupPidFilesOnExit() {
|
|
20
|
+
for (const pidPath of pidFilesToCleanup) {
|
|
21
|
+
try {
|
|
22
|
+
fsSync.unlinkSync(pidPath);
|
|
23
|
+
} catch {}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function registerPidFileCleanup(pidPath) {
|
|
28
|
+
if (!pidPath) return;
|
|
29
|
+
pidFilesToCleanup.add(pidPath);
|
|
30
|
+
if (pidExitCleanupRegistered) return;
|
|
31
|
+
process.on('exit', cleanupPidFilesOnExit);
|
|
32
|
+
pidExitCleanupRegistered = true;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function cleanupWorkspaceLocksOnExit() {
|
|
36
|
+
for (const lockPath of workspaceLocksToCleanup) {
|
|
37
|
+
try {
|
|
38
|
+
const raw = fsSync.readFileSync(lockPath, 'utf-8');
|
|
39
|
+
const current = JSON.parse(raw);
|
|
40
|
+
if (current && current.pid === process.pid) {
|
|
41
|
+
fsSync.unlinkSync(lockPath);
|
|
42
|
+
}
|
|
43
|
+
} catch {}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function registerWorkspaceLockCleanup(lockPath) {
|
|
48
|
+
if (!lockPath) return;
|
|
49
|
+
workspaceLocksToCleanup.add(lockPath);
|
|
50
|
+
if (workspaceLockExitCleanupRegistered) return;
|
|
51
|
+
process.on('exit', cleanupWorkspaceLocksOnExit);
|
|
52
|
+
workspaceLockExitCleanupRegistered = true;
|
|
53
|
+
}
|
|
54
|
+
|
|
11
55
|
function getPidFilePath({ pidFileName, cacheDirectory }) {
|
|
12
56
|
if (cacheDirectory) {
|
|
13
57
|
return path.join(cacheDirectory, pidFileName);
|
|
@@ -44,22 +88,41 @@ export async function setupPidFile({
|
|
|
44
88
|
return null;
|
|
45
89
|
}
|
|
46
90
|
|
|
47
|
-
|
|
48
|
-
try {
|
|
49
|
-
fsSync.unlinkSync(pidPath);
|
|
50
|
-
} catch {}
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
process.on('exit', cleanup);
|
|
91
|
+
registerPidFileCleanup(pidPath);
|
|
54
92
|
return pidPath;
|
|
55
93
|
}
|
|
56
94
|
|
|
57
95
|
export function registerSignalHandlers(handler) {
|
|
58
|
-
|
|
59
|
-
|
|
96
|
+
if (typeof handler !== 'function') {
|
|
97
|
+
return () => {};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (registeredSignalHandlers) {
|
|
101
|
+
process.off('SIGINT', registeredSignalHandlers.onSigint);
|
|
102
|
+
process.off('SIGTERM', registeredSignalHandlers.onSigterm);
|
|
103
|
+
registeredSignalHandlers = null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const onSigint = () => handler('SIGINT');
|
|
107
|
+
const onSigterm = () => handler('SIGTERM');
|
|
108
|
+
process.on('SIGINT', onSigint);
|
|
109
|
+
process.on('SIGTERM', onSigterm);
|
|
110
|
+
|
|
111
|
+
registeredSignalHandlers = { onSigint, onSigterm };
|
|
112
|
+
|
|
113
|
+
return () => {
|
|
114
|
+
if (registeredSignalHandlers?.onSigint === onSigint) {
|
|
115
|
+
process.off('SIGINT', onSigint);
|
|
116
|
+
process.off('SIGTERM', onSigterm);
|
|
117
|
+
registeredSignalHandlers = null;
|
|
118
|
+
}
|
|
119
|
+
};
|
|
60
120
|
}
|
|
61
121
|
|
|
62
122
|
function isProcessRunning(pid) {
|
|
123
|
+
if (!Number.isInteger(pid) || pid <= 0) {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
63
126
|
try {
|
|
64
127
|
process.kill(pid, 0);
|
|
65
128
|
return true;
|
|
@@ -71,7 +134,8 @@ function isProcessRunning(pid) {
|
|
|
71
134
|
}
|
|
72
135
|
}
|
|
73
136
|
|
|
74
|
-
export async function acquireWorkspaceLock({ cacheDirectory, workspaceDir = null } = {}) {
|
|
137
|
+
export async function acquireWorkspaceLock({ cacheDirectory, workspaceDir = null, _retryCount = 0 } = {}) {
|
|
138
|
+
const MAX_LOCK_RETRIES = 3;
|
|
75
139
|
if (!cacheDirectory || isTestEnv()) {
|
|
76
140
|
return { acquired: true, lockPath: null };
|
|
77
141
|
}
|
|
@@ -137,24 +201,21 @@ export async function acquireWorkspaceLock({ cacheDirectory, workspaceDir = null
|
|
|
137
201
|
} else {
|
|
138
202
|
await fs.unlink(lockPath).catch(() => {});
|
|
139
203
|
}
|
|
140
|
-
|
|
204
|
+
if (_retryCount >= MAX_LOCK_RETRIES) {
|
|
205
|
+
console.warn(`[Server] Lock acquisition failed after ${MAX_LOCK_RETRIES} retries; starting in secondary mode.`);
|
|
206
|
+
return { acquired: false, lockPath, ownerPid: null };
|
|
207
|
+
}
|
|
208
|
+
const retryDelayMs = Math.min(
|
|
209
|
+
LOCK_RETRY_MAX_DELAY_MS,
|
|
210
|
+
LOCK_RETRY_BASE_DELAY_MS * 2 ** _retryCount
|
|
211
|
+
);
|
|
212
|
+
await delay(retryDelayMs);
|
|
213
|
+
return acquireWorkspaceLock({ cacheDirectory, workspaceDir, _retryCount: _retryCount + 1 });
|
|
141
214
|
}
|
|
142
215
|
throw err;
|
|
143
216
|
}
|
|
144
217
|
|
|
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);
|
|
218
|
+
registerWorkspaceLockCleanup(lockPath);
|
|
158
219
|
|
|
159
220
|
return { acquired: true, lockPath };
|
|
160
221
|
}
|
|
@@ -169,6 +230,7 @@ export async function releaseWorkspaceLock({ cacheDirectory } = {}) {
|
|
|
169
230
|
const current = JSON.parse(raw);
|
|
170
231
|
if (current && current.pid === process.pid) {
|
|
171
232
|
await fs.unlink(lockPath).catch(() => {});
|
|
233
|
+
workspaceLocksToCleanup.delete(lockPath);
|
|
172
234
|
}
|
|
173
235
|
} catch {}
|
|
174
236
|
}
|
package/lib/utils.js
CHANGED
|
@@ -241,10 +241,11 @@ export function smartChunk(content, file, config) {
|
|
|
241
241
|
if (currentChunk.length > 0) {
|
|
242
242
|
const chunkText = currentChunk.join('\n');
|
|
243
243
|
if (chunkText.trim().length > MIN_CHUNK_TEXT_LENGTH) {
|
|
244
|
+
const endLine = chunkStartLine + currentChunk.length;
|
|
244
245
|
chunks.push({
|
|
245
246
|
text: chunkText,
|
|
246
247
|
startLine: chunkStartLine + 1,
|
|
247
|
-
endLine
|
|
248
|
+
endLine,
|
|
248
249
|
tokenCount: currentTokenCount + SPECIAL_TOKENS,
|
|
249
250
|
});
|
|
250
251
|
}
|
|
@@ -295,10 +296,11 @@ export function smartChunk(content, file, config) {
|
|
|
295
296
|
if (shouldSplit && safeToSplit && currentChunk.length > 0) {
|
|
296
297
|
const chunkText = currentChunk.join('\n');
|
|
297
298
|
if (chunkText.trim().length > MIN_CHUNK_TEXT_LENGTH) {
|
|
299
|
+
const endLine = chunkStartLine + currentChunk.length;
|
|
298
300
|
chunks.push({
|
|
299
301
|
text: chunkText,
|
|
300
302
|
startLine: chunkStartLine + 1,
|
|
301
|
-
endLine
|
|
303
|
+
endLine,
|
|
302
304
|
tokenCount: currentTokenCount,
|
|
303
305
|
});
|
|
304
306
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@softerist/heuristic-mcp",
|
|
3
|
-
"version": "3.2.
|
|
3
|
+
"version": "3.2.6",
|
|
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",
|