@softerist/heuristic-mcp 3.2.5 → 3.2.7
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 +125 -55
- package/lib/cache.js +9 -10
- package/lib/embed-query-process.js +82 -32
- package/lib/embedding-worker.js +3 -3
- package/lib/logging.js +13 -5
- package/lib/server-lifecycle.js +35 -2
- package/lib/utils.js +4 -2
- package/package.json +1 -1
- package/scripts/clear-cache.js +0 -2
- package/scripts/download-model.js +0 -5
- package/scripts/postinstall.js +0 -2
|
@@ -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',
|
|
@@ -240,7 +242,17 @@ function formatCrashDetail(detail) {
|
|
|
240
242
|
}
|
|
241
243
|
|
|
242
244
|
function isBrokenPipeError(detail) {
|
|
243
|
-
|
|
245
|
+
if (!detail) return false;
|
|
246
|
+
if (typeof detail === 'string') {
|
|
247
|
+
return /(?:^|[\s:])EPIPE(?:[\s:]|$)|broken pipe/i.test(detail);
|
|
248
|
+
}
|
|
249
|
+
if (typeof detail === 'object') {
|
|
250
|
+
if (detail.code === 'EPIPE') return true;
|
|
251
|
+
if (typeof detail.message === 'string') {
|
|
252
|
+
return /(?:^|[\s:])EPIPE(?:[\s:]|$)|broken pipe/i.test(detail.message);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return false;
|
|
244
256
|
}
|
|
245
257
|
|
|
246
258
|
function isCrashShutdownReason(reason) {
|
|
@@ -248,6 +260,13 @@ function isCrashShutdownReason(reason) {
|
|
|
248
260
|
return normalized.includes('uncaughtexception') || normalized.includes('unhandledrejection');
|
|
249
261
|
}
|
|
250
262
|
|
|
263
|
+
function getShutdownExitCode(reason) {
|
|
264
|
+
const normalized = String(reason || '').trim().toUpperCase();
|
|
265
|
+
if (normalized === 'SIGINT') return 130;
|
|
266
|
+
if (normalized === 'SIGTERM') return 143;
|
|
267
|
+
return isCrashShutdownReason(reason) ? 1 : 0;
|
|
268
|
+
}
|
|
269
|
+
|
|
251
270
|
function registerProcessDiagnostics({ isServerMode, requestShutdown, getShutdownReason }) {
|
|
252
271
|
if (!isServerMode) return;
|
|
253
272
|
|
|
@@ -286,6 +305,44 @@ function registerProcessDiagnostics({ isServerMode, requestShutdown, getShutdown
|
|
|
286
305
|
});
|
|
287
306
|
}
|
|
288
307
|
|
|
308
|
+
function registerStdioShutdownHandlers(requestShutdown) {
|
|
309
|
+
if (stdioShutdownHandlers) return;
|
|
310
|
+
|
|
311
|
+
const onStdinEnd = () => requestShutdown('stdin-end');
|
|
312
|
+
const onStdinClose = () => requestShutdown('stdin-close');
|
|
313
|
+
const onStdoutError = (err) => {
|
|
314
|
+
if (err?.code === 'EPIPE') {
|
|
315
|
+
requestShutdown('stdout-epipe');
|
|
316
|
+
}
|
|
317
|
+
};
|
|
318
|
+
const onStderrError = (err) => {
|
|
319
|
+
if (err?.code === 'EPIPE') {
|
|
320
|
+
requestShutdown('stderr-epipe');
|
|
321
|
+
}
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
process.stdin?.on?.('end', onStdinEnd);
|
|
325
|
+
process.stdin?.on?.('close', onStdinClose);
|
|
326
|
+
process.stdout?.on?.('error', onStdoutError);
|
|
327
|
+
process.stderr?.on?.('error', onStderrError);
|
|
328
|
+
|
|
329
|
+
stdioShutdownHandlers = {
|
|
330
|
+
onStdinEnd,
|
|
331
|
+
onStdinClose,
|
|
332
|
+
onStdoutError,
|
|
333
|
+
onStderrError,
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function unregisterStdioShutdownHandlers() {
|
|
338
|
+
if (!stdioShutdownHandlers) return;
|
|
339
|
+
process.stdin?.off?.('end', stdioShutdownHandlers.onStdinEnd);
|
|
340
|
+
process.stdin?.off?.('close', stdioShutdownHandlers.onStdinClose);
|
|
341
|
+
process.stdout?.off?.('error', stdioShutdownHandlers.onStdoutError);
|
|
342
|
+
process.stderr?.off?.('error', stdioShutdownHandlers.onStderrError);
|
|
343
|
+
stdioShutdownHandlers = null;
|
|
344
|
+
}
|
|
345
|
+
|
|
289
346
|
async function resolveWorkspaceFromEnvValue(rawValue) {
|
|
290
347
|
if (!rawValue || rawValue.includes('${')) return null;
|
|
291
348
|
const resolved = path.resolve(rawValue);
|
|
@@ -510,9 +567,10 @@ async function maybeAutoSwitchWorkspaceToPath(
|
|
|
510
567
|
if (targetNow === currentNow) return;
|
|
511
568
|
}
|
|
512
569
|
|
|
513
|
-
|
|
570
|
+
const switchPromise = (async () => {
|
|
571
|
+
const latestWorkspace = normalizePathForCompare(config.searchDirectory);
|
|
514
572
|
console.info(
|
|
515
|
-
`[Server] Auto-switching workspace from ${
|
|
573
|
+
`[Server] Auto-switching workspace from ${latestWorkspace} to ${targetWorkspacePath} (${source || 'auto'})`
|
|
516
574
|
);
|
|
517
575
|
const result = await setWorkspaceFeatureInstance.execute({
|
|
518
576
|
workspacePath: targetWorkspacePath,
|
|
@@ -524,11 +582,14 @@ async function maybeAutoSwitchWorkspaceToPath(
|
|
|
524
582
|
}
|
|
525
583
|
trustWorkspacePath(targetWorkspacePath);
|
|
526
584
|
})();
|
|
585
|
+
autoWorkspaceSwitchPromise = switchPromise;
|
|
527
586
|
|
|
528
587
|
try {
|
|
529
|
-
await
|
|
588
|
+
await switchPromise;
|
|
530
589
|
} finally {
|
|
531
|
-
autoWorkspaceSwitchPromise
|
|
590
|
+
if (autoWorkspaceSwitchPromise === switchPromise) {
|
|
591
|
+
autoWorkspaceSwitchPromise = null;
|
|
592
|
+
}
|
|
532
593
|
}
|
|
533
594
|
}
|
|
534
595
|
|
|
@@ -916,7 +977,7 @@ async function initialize(workspaceDir) {
|
|
|
916
977
|
);
|
|
917
978
|
} else {
|
|
918
979
|
console.warn(
|
|
919
|
-
`[Server] Cache corruption detected while ${context}. This server is secondary read-only and cannot re-index.
|
|
980
|
+
`[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
981
|
);
|
|
921
982
|
}
|
|
922
983
|
return true;
|
|
@@ -1159,7 +1220,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1159
1220
|
content: [
|
|
1160
1221
|
{
|
|
1161
1222
|
type: 'text',
|
|
1162
|
-
text: `Attached cache for ${normalizedPath}, but it is corrupt. This secondary read-only instance cannot rebuild it.
|
|
1223
|
+
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
1224
|
},
|
|
1164
1225
|
],
|
|
1165
1226
|
isError: true,
|
|
@@ -1269,15 +1330,15 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1269
1330
|
|
|
1270
1331
|
const searchTools = ['a_semantic_search', 'd_find_similar_code'];
|
|
1271
1332
|
if (config.unloadModelAfterSearch && searchTools.includes(toolDef.name)) {
|
|
1272
|
-
setImmediate(
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1333
|
+
setImmediate(() => {
|
|
1334
|
+
const unloadFn = unloadMainEmbedder;
|
|
1335
|
+
if (typeof unloadFn !== 'function') return;
|
|
1336
|
+
void Promise.resolve()
|
|
1337
|
+
.then(() => unloadFn())
|
|
1338
|
+
.catch((err) => {
|
|
1277
1339
|
const message = err instanceof Error ? err.message : String(err);
|
|
1278
1340
|
console.warn(`[Server] Post-search model unload failed: ${message}`);
|
|
1279
|
-
}
|
|
1280
|
-
}
|
|
1341
|
+
});
|
|
1281
1342
|
});
|
|
1282
1343
|
}
|
|
1283
1344
|
|
|
@@ -1491,18 +1552,7 @@ export async function main(argv = process.argv) {
|
|
|
1491
1552
|
await server.connect(transport);
|
|
1492
1553
|
console.info('[Server] MCP transport connected.');
|
|
1493
1554
|
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
|
-
});
|
|
1555
|
+
registerStdioShutdownHandlers(requestShutdown);
|
|
1506
1556
|
}
|
|
1507
1557
|
|
|
1508
1558
|
const detectedRoot = await detectedRootPromise;
|
|
@@ -1511,27 +1561,24 @@ export async function main(argv = process.argv) {
|
|
|
1511
1561
|
if (detectedRoot) {
|
|
1512
1562
|
console.info(`[Server] Using workspace from MCP roots: ${detectedRoot}`);
|
|
1513
1563
|
}
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
throw err;
|
|
1524
|
-
});
|
|
1525
|
-
const { startBackgroundTasks } = await initWithResolve;
|
|
1564
|
+
let startBackgroundTasks;
|
|
1565
|
+
try {
|
|
1566
|
+
const initResult = await initialize(effectiveWorkspace);
|
|
1567
|
+
startBackgroundTasks = initResult.startBackgroundTasks;
|
|
1568
|
+
} catch (err) {
|
|
1569
|
+
configInitError = err;
|
|
1570
|
+
configReadyResolve();
|
|
1571
|
+
throw err;
|
|
1572
|
+
}
|
|
1526
1573
|
|
|
1527
1574
|
console.info('[Server] Heuristic MCP server started.');
|
|
1528
1575
|
|
|
1529
|
-
|
|
1576
|
+
try {
|
|
1577
|
+
await startBackgroundTasks();
|
|
1578
|
+
} catch (err) {
|
|
1530
1579
|
console.error(`[Server] Background task error: ${err.message}`);
|
|
1531
|
-
});
|
|
1532
|
-
if (isTestEnv) {
|
|
1533
|
-
await backgroundTaskPromise;
|
|
1534
1580
|
}
|
|
1581
|
+
configReadyResolve();
|
|
1535
1582
|
// Keep-Alive mechanism: ensure the process stays alive even if StdioServerTransport
|
|
1536
1583
|
// temporarily loses its active handle status or during complex async chains.
|
|
1537
1584
|
if (isServerMode && !isTestEnv && !keepAliveTimer) {
|
|
@@ -1546,7 +1593,7 @@ export async function main(argv = process.argv) {
|
|
|
1546
1593
|
|
|
1547
1594
|
async function gracefulShutdown(signal) {
|
|
1548
1595
|
console.info(`[Server] Received ${signal}, shutting down gracefully...`);
|
|
1549
|
-
const exitCode =
|
|
1596
|
+
const exitCode = getShutdownExitCode(signal);
|
|
1550
1597
|
|
|
1551
1598
|
if (keepAliveTimer) {
|
|
1552
1599
|
clearInterval(keepAliveTimer);
|
|
@@ -1575,30 +1622,53 @@ async function gracefulShutdown(signal) {
|
|
|
1575
1622
|
}
|
|
1576
1623
|
|
|
1577
1624
|
if (cache) {
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
.save()
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1625
|
+
cleanupTasks.push(
|
|
1626
|
+
(async () => {
|
|
1627
|
+
if (!workspaceLockAcquired) {
|
|
1628
|
+
console.info('[Server] Secondary/fallback mode: skipping cache save.');
|
|
1629
|
+
} else {
|
|
1630
|
+
await cache.save();
|
|
1631
|
+
console.info('[Server] Cache saved');
|
|
1632
|
+
}
|
|
1633
|
+
if (typeof cache.close === 'function') {
|
|
1634
|
+
await cache.close();
|
|
1635
|
+
}
|
|
1636
|
+
})().catch((err) => console.error(`[Server] Cache shutdown cleanup failed: ${err.message}`))
|
|
1637
|
+
);
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
if (workspaceLockAcquired && config?.cacheDirectory) {
|
|
1641
|
+
cleanupTasks.push(
|
|
1642
|
+
releaseWorkspaceLock({ cacheDirectory: config.cacheDirectory }).catch((err) =>
|
|
1643
|
+
console.warn(`[Server] Failed to release workspace lock: ${err.message}`)
|
|
1644
|
+
)
|
|
1645
|
+
);
|
|
1588
1646
|
}
|
|
1589
1647
|
|
|
1590
1648
|
await Promise.allSettled(cleanupTasks);
|
|
1591
1649
|
console.info('[Server] Goodbye!');
|
|
1650
|
+
|
|
1651
|
+
unregisterStdioShutdownHandlers();
|
|
1592
1652
|
await flushLogsSafely({ close: true, timeoutMs: 1500 });
|
|
1593
1653
|
|
|
1594
1654
|
setTimeout(() => process.exit(exitCode), 100);
|
|
1595
1655
|
}
|
|
1596
1656
|
|
|
1657
|
+
function isLikelyCliEntrypoint(argvPath) {
|
|
1658
|
+
const base = path.basename(argvPath || '').toLowerCase();
|
|
1659
|
+
return (
|
|
1660
|
+
base === 'heuristic-mcp' ||
|
|
1661
|
+
base === 'heuristic-mcp.js' ||
|
|
1662
|
+
base === 'heuristic-mcp.mjs' ||
|
|
1663
|
+
base === 'heuristic-mcp.cjs' ||
|
|
1664
|
+
base === 'heuristic-mcp.cmd'
|
|
1665
|
+
);
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1597
1668
|
const isMain =
|
|
1598
1669
|
process.argv[1] &&
|
|
1599
1670
|
(path.resolve(process.argv[1]).toLowerCase() === fileURLToPath(import.meta.url).toLowerCase() ||
|
|
1600
|
-
process.argv[1]
|
|
1601
|
-
process.argv[1].endsWith('heuristic-mcp.js')) &&
|
|
1671
|
+
isLikelyCliEntrypoint(process.argv[1])) &&
|
|
1602
1672
|
!(process.env.VITEST === 'true' || process.env.NODE_ENV === 'test');
|
|
1603
1673
|
|
|
1604
1674
|
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
|
@@ -7,7 +7,6 @@ import { configureNativeOnnxBackend } from './onnx-backend.js';
|
|
|
7
7
|
import { smartChunk, hashContent } from './utils.js';
|
|
8
8
|
import { extractCallData } from './call-graph.js';
|
|
9
9
|
|
|
10
|
-
// Helper to get global cache dir (duplicated from config.js to avoid full config load in worker)
|
|
11
10
|
function getGlobalCacheDir() {
|
|
12
11
|
if (process.platform === 'win32') {
|
|
13
12
|
return process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local');
|
|
@@ -713,10 +712,11 @@ parentPort.on('message', async (message) => {
|
|
|
713
712
|
maybeRunGc();
|
|
714
713
|
}
|
|
715
714
|
|
|
715
|
+
const taskIndex = fileTasks.length;
|
|
716
716
|
if (chunks.length > 0) {
|
|
717
717
|
for (const c of chunks) {
|
|
718
718
|
allPendingChunks.push({
|
|
719
|
-
|
|
719
|
+
fileTaskIndex: taskIndex,
|
|
720
720
|
text: c.text,
|
|
721
721
|
startLine: c.startLine,
|
|
722
722
|
endLine: c.endLine,
|
|
@@ -847,7 +847,7 @@ parentPort.on('message', async (message) => {
|
|
|
847
847
|
|
|
848
848
|
for (const chunkItem of allPendingChunks) {
|
|
849
849
|
if (chunkItem.vectorBuffer) {
|
|
850
|
-
const task = fileTasks[chunkItem.
|
|
850
|
+
const task = fileTasks[chunkItem.fileTaskIndex];
|
|
851
851
|
task.results.push({
|
|
852
852
|
startLine: chunkItem.startLine,
|
|
853
853
|
endLine: chunkItem.endLine,
|
package/lib/logging.js
CHANGED
|
@@ -6,14 +6,25 @@ 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,
|
|
13
14
|
};
|
|
14
15
|
|
|
15
16
|
function isBrokenPipeError(error) {
|
|
16
|
-
|
|
17
|
+
if (!error) return false;
|
|
18
|
+
if (typeof error === 'string') {
|
|
19
|
+
return /(?:^|[\s:])EPIPE(?:[\s:]|$)|broken pipe/i.test(error);
|
|
20
|
+
}
|
|
21
|
+
if (typeof error === 'object') {
|
|
22
|
+
if (error.code === 'EPIPE') return true;
|
|
23
|
+
if (typeof error.message === 'string') {
|
|
24
|
+
return /(?:^|[\s:])EPIPE(?:[\s:]|$)|broken pipe/i.test(error.message);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return false;
|
|
17
28
|
}
|
|
18
29
|
|
|
19
30
|
function writeToOriginalStderr(...args) {
|
|
@@ -36,9 +47,6 @@ export function enableStderrOnlyLogging() {
|
|
|
36
47
|
console.info = redirect;
|
|
37
48
|
console.warn = redirect;
|
|
38
49
|
console.error = redirect;
|
|
39
|
-
// eslint-disable-next-line no-console
|
|
40
|
-
console.log = redirect;
|
|
41
|
-
console.info = redirect;
|
|
42
50
|
}
|
|
43
51
|
|
|
44
52
|
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.7",
|
|
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",
|
package/scripts/clear-cache.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
1
|
import fs from 'fs/promises';
|
|
3
2
|
import { loadConfig } from '../lib/config.js';
|
|
4
3
|
|
|
@@ -7,7 +6,6 @@ async function clearCache() {
|
|
|
7
6
|
const config = await loadConfig(process.cwd());
|
|
8
7
|
const cacheDir = config.cacheDirectory;
|
|
9
8
|
|
|
10
|
-
// Remove cache directory
|
|
11
9
|
await fs.rm(cacheDir, { recursive: true, force: true });
|
|
12
10
|
console.info(`Cache cleared successfully: ${cacheDir}`);
|
|
13
11
|
console.info('Next startup will perform a full reindex.');
|
|
@@ -9,15 +9,11 @@ async function downloadModel() {
|
|
|
9
9
|
const transformers = await import('@huggingface/transformers');
|
|
10
10
|
const { pipeline, env } = transformers;
|
|
11
11
|
|
|
12
|
-
// Force cache directory to global location
|
|
13
12
|
env.cacheDir = globalCacheDir;
|
|
14
13
|
|
|
15
14
|
console.info(`[Model Setup] Pre-caching model to: ${globalCacheDir}`);
|
|
16
|
-
// Check if network is available by pinging HF (simple check)
|
|
17
|
-
// Actually, pipeline() will fail fast if network is down
|
|
18
15
|
console.info(`[Model Setup] Downloading 'jinaai/jina-embeddings-v2-base-code'...`);
|
|
19
16
|
|
|
20
|
-
// This will download the model to the cache directory
|
|
21
17
|
await pipeline('feature-extraction', 'jinaai/jina-embeddings-v2-base-code');
|
|
22
18
|
|
|
23
19
|
console.info(`[Model Setup] ✅ Model cached successfully!`);
|
|
@@ -36,7 +32,6 @@ async function downloadModel() {
|
|
|
36
32
|
'[Model Setup] This is okay! The server will attempt to download it when started.'
|
|
37
33
|
);
|
|
38
34
|
console.warn(`[Model Setup] Error details: ${error.message}`);
|
|
39
|
-
// Don't fail the install, just warn
|
|
40
35
|
}
|
|
41
36
|
}
|
|
42
37
|
|
package/scripts/postinstall.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { register } from '../features/register.js';
|
|
2
2
|
|
|
3
|
-
// Run the registration process - MUST await to ensure file writes complete
|
|
4
3
|
console.info('[PostInstall] Running Heuristic MCP registration...');
|
|
5
4
|
|
|
6
5
|
try {
|
|
@@ -8,5 +7,4 @@ try {
|
|
|
8
7
|
console.info('[PostInstall] Registration complete.');
|
|
9
8
|
} catch (err) {
|
|
10
9
|
console.error('[PostInstall] Registration failed:', err.message);
|
|
11
|
-
// Don't fail the install if registration fails, just warn
|
|
12
10
|
}
|