@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.
@@ -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',
@@ -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
- autoWorkspaceSwitchPromise = (async () => {
560
+ const switchPromise = (async () => {
561
+ const latestWorkspace = normalizePathForCompare(config.searchDirectory);
514
562
  console.info(
515
- `[Server] Auto-switching workspace from ${currentWorkspace} to ${targetWorkspacePath} (${source || 'auto'})`
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 autoWorkspaceSwitchPromise;
578
+ await switchPromise;
530
579
  } finally {
531
- autoWorkspaceSwitchPromise = null;
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. Reload the IDE window for this workspace or use the primary instance to rebuild the cache.`
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. Reload the IDE window for this workspace or run indexing from the primary instance.`,
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(async () => {
1273
- if (typeof unloadMainEmbedder === 'function') {
1274
- try {
1275
- await unloadMainEmbedder();
1276
- } catch (err) {
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
- 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
- });
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
- 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;
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
- const backgroundTaskPromise = startBackgroundTasks().catch((err) => {
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 = isCrashShutdownReason(signal) ? 1 : 0;
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
- 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
- }
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].endsWith('heuristic-mcp') ||
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
- 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
  }
@@ -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
- fileIndex: i,
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.fileIndex];
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
- 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,
@@ -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) {
@@ -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.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",