@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.
@@ -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 featureInstance.execute({
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
- return Boolean(detail && typeof detail === 'object' && detail.code === 'EPIPE');
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
- autoWorkspaceSwitchPromise = (async () => {
570
+ const switchPromise = (async () => {
571
+ const latestWorkspace = normalizePathForCompare(config.searchDirectory);
514
572
  console.info(
515
- `[Server] Auto-switching workspace from ${currentWorkspace} to ${targetWorkspacePath} (${source || 'auto'})`
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 autoWorkspaceSwitchPromise;
588
+ await switchPromise;
530
589
  } finally {
531
- autoWorkspaceSwitchPromise = null;
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. Reload the IDE window for this workspace or use the primary instance to rebuild the cache.`
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. Reload the IDE window for this workspace or run indexing from the primary instance.`,
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(async () => {
1273
- if (typeof unloadMainEmbedder === 'function') {
1274
- try {
1275
- await unloadMainEmbedder();
1276
- } catch (err) {
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
- process.stdin?.on?.('end', () => requestShutdown('stdin-end'));
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
- const initPromise = initialize(effectiveWorkspace);
1515
- const initWithResolve = initPromise
1516
- .then((result) => {
1517
- configReadyResolve();
1518
- return result;
1519
- })
1520
- .catch((err) => {
1521
- configInitError = err;
1522
- configReadyResolve();
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
- const backgroundTaskPromise = startBackgroundTasks().catch((err) => {
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 = isCrashShutdownReason(signal) ? 1 : 0;
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
- if (!workspaceLockAcquired) {
1579
- console.info('[Server] Secondary/fallback mode: skipping cache save.');
1580
- } else {
1581
- cleanupTasks.push(
1582
- cache
1583
- .save()
1584
- .then(() => console.info('[Server] Cache saved'))
1585
- .catch((err) => console.error(`[Server] Failed to save cache: ${err.message}`))
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].endsWith('heuristic-mcp') ||
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
- if (!entry) return null;
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) => Number.isInteger(label) && label >= 0 && label < this.vectorStore.length
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
- if (pendingRequests.length > 0) {
47
- const { resolve, reject, startTime } = pendingRequests.shift();
48
- try {
49
- const result = JSON.parse(line);
50
- const elapsed = ((Date.now() - startTime) / 1000).toFixed(2);
51
-
52
- if (!result.results || result.results.length === 0) {
53
- reject(new Error('Embedding child process returned no results'));
54
- return;
55
- }
56
-
57
- const embResult = result.results[0];
58
- if (!embResult.success) {
59
- reject(new Error(`Embedding failed: ${embResult.error}`));
60
- return;
61
- }
62
-
63
- const vector = new Float32Array(embResult.vector);
64
-
65
- if (currentConfig?.verbose) {
66
- console.info(`[Search] Query embedding (persistent child) completed in ${elapsed}s`);
67
- }
68
-
69
- resolve(vector);
70
- } catch (err) {
71
- reject(new Error(`Failed to parse embedding result: ${err.message}`));
72
- } finally {
73
- isProcessingRequest = false;
74
- processNextRequest();
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[0];
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 } = pendingRequests.shift();
222
+ const { reject } = request;
174
223
  reject(err);
224
+ activeRequest = null;
175
225
  isProcessingRequest = false;
176
226
  processNextRequest();
177
227
  }
@@ -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
- fileIndex: i,
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.fileIndex];
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
- log: console.info,
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
- return Boolean(error && typeof error === 'object' && error.code === 'EPIPE');
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) {
@@ -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
- process.on('SIGINT', () => handler('SIGINT'));
94
- process.on('SIGTERM', () => handler('SIGTERM'));
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: i,
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: i,
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.5",
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",
@@ -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
 
@@ -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
  }