@softerist/heuristic-mcp 3.2.5 → 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 +19 -0
- package/features/set-workspace.js +6 -2
- package/index.js +114 -54
- 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 +2 -4
- package/lib/server-lifecycle.js +35 -2
- 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;
|
|
@@ -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
|
@@ -47,6 +47,7 @@ import {
|
|
|
47
47
|
registerSignalHandlers,
|
|
48
48
|
setupPidFile,
|
|
49
49
|
acquireWorkspaceLock,
|
|
50
|
+
releaseWorkspaceLock,
|
|
50
51
|
stopOtherHeuristicServers,
|
|
51
52
|
} from './lib/server-lifecycle.js';
|
|
52
53
|
|
|
@@ -186,6 +187,7 @@ let rootsCapabilitySupported = null;
|
|
|
186
187
|
let rootsProbeInFlight = null;
|
|
187
188
|
let lastRootsProbeTime = 0;
|
|
188
189
|
let keepAliveTimer = null;
|
|
190
|
+
let stdioShutdownHandlers = null;
|
|
189
191
|
const ROOTS_PROBE_COOLDOWN_MS = 2000;
|
|
190
192
|
const WORKSPACE_BOUND_TOOL_NAMES = new Set([
|
|
191
193
|
'a_semantic_search',
|
|
@@ -248,6 +250,13 @@ function isCrashShutdownReason(reason) {
|
|
|
248
250
|
return normalized.includes('uncaughtexception') || normalized.includes('unhandledrejection');
|
|
249
251
|
}
|
|
250
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
|
+
|
|
251
260
|
function registerProcessDiagnostics({ isServerMode, requestShutdown, getShutdownReason }) {
|
|
252
261
|
if (!isServerMode) return;
|
|
253
262
|
|
|
@@ -286,6 +295,44 @@ function registerProcessDiagnostics({ isServerMode, requestShutdown, getShutdown
|
|
|
286
295
|
});
|
|
287
296
|
}
|
|
288
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
|
+
|
|
289
336
|
async function resolveWorkspaceFromEnvValue(rawValue) {
|
|
290
337
|
if (!rawValue || rawValue.includes('${')) return null;
|
|
291
338
|
const resolved = path.resolve(rawValue);
|
|
@@ -510,9 +557,10 @@ async function maybeAutoSwitchWorkspaceToPath(
|
|
|
510
557
|
if (targetNow === currentNow) return;
|
|
511
558
|
}
|
|
512
559
|
|
|
513
|
-
|
|
560
|
+
const switchPromise = (async () => {
|
|
561
|
+
const latestWorkspace = normalizePathForCompare(config.searchDirectory);
|
|
514
562
|
console.info(
|
|
515
|
-
`[Server] Auto-switching workspace from ${
|
|
563
|
+
`[Server] Auto-switching workspace from ${latestWorkspace} to ${targetWorkspacePath} (${source || 'auto'})`
|
|
516
564
|
);
|
|
517
565
|
const result = await setWorkspaceFeatureInstance.execute({
|
|
518
566
|
workspacePath: targetWorkspacePath,
|
|
@@ -524,11 +572,14 @@ async function maybeAutoSwitchWorkspaceToPath(
|
|
|
524
572
|
}
|
|
525
573
|
trustWorkspacePath(targetWorkspacePath);
|
|
526
574
|
})();
|
|
575
|
+
autoWorkspaceSwitchPromise = switchPromise;
|
|
527
576
|
|
|
528
577
|
try {
|
|
529
|
-
await
|
|
578
|
+
await switchPromise;
|
|
530
579
|
} finally {
|
|
531
|
-
autoWorkspaceSwitchPromise
|
|
580
|
+
if (autoWorkspaceSwitchPromise === switchPromise) {
|
|
581
|
+
autoWorkspaceSwitchPromise = null;
|
|
582
|
+
}
|
|
532
583
|
}
|
|
533
584
|
}
|
|
534
585
|
|
|
@@ -916,7 +967,7 @@ async function initialize(workspaceDir) {
|
|
|
916
967
|
);
|
|
917
968
|
} else {
|
|
918
969
|
console.warn(
|
|
919
|
-
`[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.`
|
|
920
971
|
);
|
|
921
972
|
}
|
|
922
973
|
return true;
|
|
@@ -1159,7 +1210,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1159
1210
|
content: [
|
|
1160
1211
|
{
|
|
1161
1212
|
type: 'text',
|
|
1162
|
-
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.`,
|
|
1163
1214
|
},
|
|
1164
1215
|
],
|
|
1165
1216
|
isError: true,
|
|
@@ -1269,15 +1320,15 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1269
1320
|
|
|
1270
1321
|
const searchTools = ['a_semantic_search', 'd_find_similar_code'];
|
|
1271
1322
|
if (config.unloadModelAfterSearch && searchTools.includes(toolDef.name)) {
|
|
1272
|
-
setImmediate(
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1323
|
+
setImmediate(() => {
|
|
1324
|
+
const unloadFn = unloadMainEmbedder;
|
|
1325
|
+
if (typeof unloadFn !== 'function') return;
|
|
1326
|
+
void Promise.resolve()
|
|
1327
|
+
.then(() => unloadFn())
|
|
1328
|
+
.catch((err) => {
|
|
1277
1329
|
const message = err instanceof Error ? err.message : String(err);
|
|
1278
1330
|
console.warn(`[Server] Post-search model unload failed: ${message}`);
|
|
1279
|
-
}
|
|
1280
|
-
}
|
|
1331
|
+
});
|
|
1281
1332
|
});
|
|
1282
1333
|
}
|
|
1283
1334
|
|
|
@@ -1491,18 +1542,7 @@ export async function main(argv = process.argv) {
|
|
|
1491
1542
|
await server.connect(transport);
|
|
1492
1543
|
console.info('[Server] MCP transport connected.');
|
|
1493
1544
|
if (isServerMode) {
|
|
1494
|
-
|
|
1495
|
-
process.stdin?.on?.('close', () => requestShutdown('stdin-close'));
|
|
1496
|
-
process.stdout?.on?.('error', (err) => {
|
|
1497
|
-
if (err?.code === 'EPIPE') {
|
|
1498
|
-
requestShutdown('stdout-epipe');
|
|
1499
|
-
}
|
|
1500
|
-
});
|
|
1501
|
-
process.stderr?.on?.('error', (err) => {
|
|
1502
|
-
if (err?.code === 'EPIPE') {
|
|
1503
|
-
requestShutdown('stderr-epipe');
|
|
1504
|
-
}
|
|
1505
|
-
});
|
|
1545
|
+
registerStdioShutdownHandlers(requestShutdown);
|
|
1506
1546
|
}
|
|
1507
1547
|
|
|
1508
1548
|
const detectedRoot = await detectedRootPromise;
|
|
@@ -1511,27 +1551,24 @@ export async function main(argv = process.argv) {
|
|
|
1511
1551
|
if (detectedRoot) {
|
|
1512
1552
|
console.info(`[Server] Using workspace from MCP roots: ${detectedRoot}`);
|
|
1513
1553
|
}
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
throw err;
|
|
1524
|
-
});
|
|
1525
|
-
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
|
+
}
|
|
1526
1563
|
|
|
1527
1564
|
console.info('[Server] Heuristic MCP server started.');
|
|
1528
1565
|
|
|
1529
|
-
|
|
1566
|
+
try {
|
|
1567
|
+
await startBackgroundTasks();
|
|
1568
|
+
} catch (err) {
|
|
1530
1569
|
console.error(`[Server] Background task error: ${err.message}`);
|
|
1531
|
-
});
|
|
1532
|
-
if (isTestEnv) {
|
|
1533
|
-
await backgroundTaskPromise;
|
|
1534
1570
|
}
|
|
1571
|
+
configReadyResolve();
|
|
1535
1572
|
// Keep-Alive mechanism: ensure the process stays alive even if StdioServerTransport
|
|
1536
1573
|
// temporarily loses its active handle status or during complex async chains.
|
|
1537
1574
|
if (isServerMode && !isTestEnv && !keepAliveTimer) {
|
|
@@ -1546,13 +1583,15 @@ export async function main(argv = process.argv) {
|
|
|
1546
1583
|
|
|
1547
1584
|
async function gracefulShutdown(signal) {
|
|
1548
1585
|
console.info(`[Server] Received ${signal}, shutting down gracefully...`);
|
|
1549
|
-
const exitCode =
|
|
1586
|
+
const exitCode = getShutdownExitCode(signal);
|
|
1550
1587
|
|
|
1551
1588
|
if (keepAliveTimer) {
|
|
1552
1589
|
clearInterval(keepAliveTimer);
|
|
1553
1590
|
keepAliveTimer = null;
|
|
1554
1591
|
}
|
|
1555
1592
|
|
|
1593
|
+
unregisterStdioShutdownHandlers();
|
|
1594
|
+
|
|
1556
1595
|
const cleanupTasks = [];
|
|
1557
1596
|
|
|
1558
1597
|
if (indexer && indexer.watcher) {
|
|
@@ -1575,16 +1614,27 @@ async function gracefulShutdown(signal) {
|
|
|
1575
1614
|
}
|
|
1576
1615
|
|
|
1577
1616
|
if (cache) {
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
.save()
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
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
|
+
);
|
|
1588
1638
|
}
|
|
1589
1639
|
|
|
1590
1640
|
await Promise.allSettled(cleanupTasks);
|
|
@@ -1594,11 +1644,21 @@ async function gracefulShutdown(signal) {
|
|
|
1594
1644
|
setTimeout(() => process.exit(exitCode), 100);
|
|
1595
1645
|
}
|
|
1596
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
|
+
|
|
1597
1658
|
const isMain =
|
|
1598
1659
|
process.argv[1] &&
|
|
1599
1660
|
(path.resolve(process.argv[1]).toLowerCase() === fileURLToPath(import.meta.url).toLowerCase() ||
|
|
1600
|
-
process.argv[1]
|
|
1601
|
-
process.argv[1].endsWith('heuristic-mcp.js')) &&
|
|
1661
|
+
isLikelyCliEntrypoint(process.argv[1])) &&
|
|
1602
1662
|
!(process.env.VITEST === 'true' || process.env.NODE_ENV === 'test');
|
|
1603
1663
|
|
|
1604
1664
|
if (isMain) {
|
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
|
@@ -6,7 +6,8 @@ import util from 'util';
|
|
|
6
6
|
let logStream = null;
|
|
7
7
|
let stderrWritable = true;
|
|
8
8
|
const originalConsole = {
|
|
9
|
-
|
|
9
|
+
// eslint-disable-next-line no-console
|
|
10
|
+
log: console.log,
|
|
10
11
|
warn: console.warn,
|
|
11
12
|
error: console.error,
|
|
12
13
|
info: console.info,
|
|
@@ -36,9 +37,6 @@ export function enableStderrOnlyLogging() {
|
|
|
36
37
|
console.info = redirect;
|
|
37
38
|
console.warn = redirect;
|
|
38
39
|
console.error = redirect;
|
|
39
|
-
// eslint-disable-next-line no-console
|
|
40
|
-
console.log = redirect;
|
|
41
|
-
console.info = redirect;
|
|
42
40
|
}
|
|
43
41
|
|
|
44
42
|
export async function setupFileLogging(config) {
|
package/lib/server-lifecycle.js
CHANGED
|
@@ -8,6 +8,9 @@ const pidFilesToCleanup = new Set();
|
|
|
8
8
|
const workspaceLocksToCleanup = new Set();
|
|
9
9
|
let pidExitCleanupRegistered = false;
|
|
10
10
|
let workspaceLockExitCleanupRegistered = false;
|
|
11
|
+
let registeredSignalHandlers = null;
|
|
12
|
+
const LOCK_RETRY_BASE_DELAY_MS = 50;
|
|
13
|
+
const LOCK_RETRY_MAX_DELAY_MS = 500;
|
|
11
14
|
|
|
12
15
|
function isTestEnv() {
|
|
13
16
|
return process.env.VITEST === 'true' || process.env.NODE_ENV === 'test';
|
|
@@ -90,11 +93,36 @@ export async function setupPidFile({
|
|
|
90
93
|
}
|
|
91
94
|
|
|
92
95
|
export function registerSignalHandlers(handler) {
|
|
93
|
-
|
|
94
|
-
|
|
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
|
+
};
|
|
95
120
|
}
|
|
96
121
|
|
|
97
122
|
function isProcessRunning(pid) {
|
|
123
|
+
if (!Number.isInteger(pid) || pid <= 0) {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
98
126
|
try {
|
|
99
127
|
process.kill(pid, 0);
|
|
100
128
|
return true;
|
|
@@ -177,6 +205,11 @@ export async function acquireWorkspaceLock({ cacheDirectory, workspaceDir = null
|
|
|
177
205
|
console.warn(`[Server] Lock acquisition failed after ${MAX_LOCK_RETRIES} retries; starting in secondary mode.`);
|
|
178
206
|
return { acquired: false, lockPath, ownerPid: null };
|
|
179
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);
|
|
180
213
|
return acquireWorkspaceLock({ cacheDirectory, workspaceDir, _retryCount: _retryCount + 1 });
|
|
181
214
|
}
|
|
182
215
|
throw err;
|
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",
|