Skip to content

fix: MCP server crashes under parallel tool calls (#326)#349

Merged
magyargergo merged 7 commits into
abhigyanpatwari:mainfrom
RyuzakiH:fix/mcp-parallel-stability
Mar 18, 2026
Merged

fix: MCP server crashes under parallel tool calls (#326)#349
magyargergo merged 7 commits into
abhigyanpatwari:mainfrom
RyuzakiH:fix/mcp-parallel-stability

Conversation

@RyuzakiH

@RyuzakiH RyuzakiH commented Mar 18, 2026

Copy link
Copy Markdown
Contributor

Problem

The MCP server crashes silently when receiving 4 or more parallel cypher() or query() calls. The process exits without error output, leaving clients with Connection closed errors.

Simpler tools such as impact() and context() are unaffected.

Root cause

The connection pool grows lazily. When all connections are busy, createConnection() calls silenceStdout(), which replaces process.stdout.write with a no-op to suppress native module debug output.

Under parallel load, this races with in-flight MCP responses being written to stdout, causing responses to be dropped and corrupting the JSON-RPC stream.

Fix

1. Pre-warm the full connection pool (lbug-adapter.ts)

All connections are now created eagerly during initLbug(), so createConnection() is never called lazily during active query execution, eliminating the stdout race.

Also removed the now-unused INITIAL_CONNS_PER_REPO constant.

2. Safe stdout proxy (server.ts)

Capture the real process.stdout.write at startup and route it through a proxy to the MCP transport.

This ensures MCP responses still reach the client even if silenceStdout() is triggered later.

3. Crash handlers (server.ts)

  • Added an uncaughtException handler that logs to stderr and shuts the server down
  • Added an unhandledRejection handler that logs the error but remains non-fatal

This keeps the server availability-first: one missed catch should not terminate the process.

4. Stdout silence watchdog (lbug-adapter.ts)

Added a 1-second watchdog that restores stdout if it becomes stuck in the silenced state, such as after a native crash during connection construction.

Validation

Tested all reproduction patterns from the issue:

  • 6 sequential queries, including all Cypher patterns from the report — all pass
  • 6 parallel cypher() calls — all pass, no crash
  • 6 parallel query() calls (issue reports 5+ crashes) — all pass, no crash
  • 1 cypher() + 5 query() mixed parallel (exact issue repro) — all pass, no crash

Result

The server no longer drops MCP responses or corrupts the JSON-RPC stream under parallel cypher() / query() load.

Fixes #326

@vercel

vercel Bot commented Mar 18, 2026

Copy link
Copy Markdown

@RyuzakiH is attempting to deploy a commit to the NexusCore Team on Vercel.

A member of the Team first needs to authorize it.

@magyargergo

Copy link
Copy Markdown
Collaborator

@claude ultra-think while you are reviewing this pr and you are tasked to find bugs, code quality issues, architectural fit and performance issues.

@claude

claude Bot commented Mar 18, 2026

Copy link
Copy Markdown

Claude finished @magyargergo's task in 3m 24s —— View job


PR Review: fix/mcp-parallel-stability

Tasks

  • Gather context and understand request
  • Read changed files in detail
  • Analyze for bugs, code quality issues, architectural fit, and performance issues
  • Post comprehensive review

The PR correctly identifies the root cause (lazy createConnection racing with active MCP writes) and the pre-warming approach is the right direction. However, I found several issues ranging from a real concurrency bug to architectural fragilities that could cause the crash to resurface under different conditions.


🔴 Bug: FTS init races with concurrent checkouts

lbug-adapter.ts, lines 227–238

pool.set(repoId, { db, available, checkedOut: 0, waiters: [], lastUsed: Date.now(), dbPath });
ensureIdleTimer();

if (!shared.ftsLoaded) {
  try {
    await available[0].query('LOAD EXTENSION fts');   // ← await here

pool.set(...) is called before the FTS load. Because LOAD EXTENSION fts is await-ed, any concurrent request that arrives between pool.set and the FTS query completing can call checkout(), which will pop() available[0] — the same connection currently running the FTS load. Two concurrent .query() calls on a single Connection is exactly the segfault the connection pool was designed to prevent.

Fix: Either call pool.set(...) after the FTS load completes, or check out and return available[0] properly via checkout/checkin for the FTS call.

Fix this →


🔴 Bug: initLbug has no concurrency guard for the same repoId

lbug-adapter.ts, lines 162–167

export const initLbug = async (repoId: string, dbPath: string): Promise<void> => {
  const existing = pool.get(repoId);
  if (existing) {
    existing.lastUsed = Date.now();
    return;
  }
  // ... multiple await points below ...
  pool.set(repoId, { ... });

Because initLbug has await points (fs.stat, setTimeout on lock retry, LOAD EXTENSION fts), two concurrent calls for the same repoId can both pass the pool.get guard before either completes. The result:

  • shared.refCount is incremented twice
  • The second pool.set silently overwrites the first, leaking all 8 connections from the first call (they're unreachable, never returned to the pool or closed)
  • The first FTS query result is discarded

This is most likely to trigger on server startup when the first batch of parallel tool calls all try to initialize the same repo simultaneously.

Fix this →


🟠 Risk: Lazy createConnection path still reachable in checkout

lbug-adapter.ts, lines 253–257

// Grow the pool if under the cap
const totalConns = entry.available.length + entry.checkedOut;
if (totalConns < MAX_CONNS_PER_REPO) {
  entry.checkedOut++;
  return Promise.resolve(createConnection(entry.db));  // ← still here
}

The PR's stated goal was to eliminate lazy createConnection calls during active query execution. But this path is still live. It's unreachable under normal operation (since all 8 connections are pre-created), but it can trigger if:

  • A connection is abandoned without calling checkin (e.g., executeQuery's finally block throws or is bypassed)
  • A future refactor removes the pre-warm step

If this path is hit while the server is handling live requests, the original stdout race re-emerges. Either remove the path entirely or add an assertion/warning that it should never be reached.


🟠 Risk: The 1-second watchdog can prematurely restore stdout

lbug-adapter.ts, lines 132–139

setInterval(() => {
  if (stdoutSilenceCount > 0) {
    stdoutSilenceCount = 0;
    process.stdout.write = realStdoutWrite;
  }
}, 1000).unref();

This fires every 1 second unconditionally. If createConnection ever legitimately takes more than 1 second (e.g., during a slow native constructor with debug output), the watchdog fires mid-construction and restores stdout while the native module is still writing. This allows native debug output to corrupt the MCP stream — the exact failure mode this PR was written to prevent.

Additionally, this setInterval is a module-level side effect that runs the instant the module is imported, in every context including tests and any tool that imports this module.


🟡 Issue: Two independent captures of process.stdout.write can diverge

lbug-adapter.ts:56:

const realStdoutWrite = process.stdout.write.bind(process.stdout);

Captured at module import time.

server.ts:282 inside startMCPServer():

const _realStdoutWrite = process.stdout.write.bind(process.stdout);

Captured at server start time.

These are independent captures. restoreStdout() restores process.stdout.write to the adapter's copy; the transport uses the server's copy. If anything patches process.stdout.write between module load and server start (e.g., a logging library, test harness, or another module), these two captures diverge and the system is in an inconsistent state. The intent should be that one capture is authoritative — either share it via export/import or capture only once.


🟡 Issue: unhandledRejection swallowing masks serious errors

server.ts, lines 315–317

process.on('unhandledRejection', (reason: any) => {
  process.stderr.write(`GitNexus MCP unhandledRejection: ${reason?.stack || reason}\n`);
});

Node.js ≥ 15 exits on unhandled rejections by default for good reason — they usually indicate a real bug. Silencing them in the name of "availability" could mask crashes in the connection pool, query executor, or resource handlers that should be fixed. A logged-but-ignored rejection also means the affected tool call already returned an error to the client (since the rejection is unhandled) and the server state may now be inconsistent.

Minor: reason?.stack || reason logs [object Object] for non-Error rejections. Use String(reason) or reason instanceof Error ? reason.stack : JSON.stringify(reason) for accurate output.


🟡 Performance: Pre-creating all 8 connections per repo is expensive

lbug-adapter.ts, lines 222–225

const available: lbug.Connection[] = [];
for (let i = 0; i < MAX_CONNS_PER_REPO; i++) {
  available.push(createConnection(db));
}

With MAX_POOL_SIZE = 5 repos × MAX_CONNS_PER_REPO = 8 connections = 40 native connections at peak, each holding an open file descriptor and likely a portion of the mmap'd buffer manager. Previously, INITIAL_CONNS_PER_REPO = 2 meant only 10 connections on startup. This is a 4× resource increase that may cause issues on memory-constrained systems or when running multiple MCP server instances.

Consider pre-warming to a lower number (e.g., 4–6) that still prevents the race, or accepting that the first few parallel calls may lazily create connections (but behind the silenceStdout protection that's now dual-captured).


🟢 What's done well

  • Pre-warming approach is architecturally correct: Eliminating lazy init during active query handling is the right fix for the stdout race.
  • Safe stdout proxy: Capturing _realStdoutWrite before transport construction and routing through a Proxy ensures MCP writes are never dropped, even if silenceStdout fires. This is a solid defensive pattern.
  • Crash handler separation: Distinguishing uncaughtException (fatal, shut down) from unhandledRejection (logged) is a reasonable heuristic, even if the specific policy warrants discussion.
  • Waiter timeout: The WAITER_TIMEOUT_MS = 15_000 timeout on queued waiters prevents indefinite hangs when the pool is saturated.
  • checkin direct-handoff: Handing a returned connection directly to the next waiter (rather than pushing to available and re-popping) is correct and avoids unnecessary state transitions.

Summary

Severity Issue File
🔴 Bug pool.set before FTS load allows concurrent checkout of available[0] lbug-adapter.ts:227
🔴 Bug No concurrency guard in initLbug — parallel inits for same repoId leak connections lbug-adapter.ts:162
🟠 Risk Lazy createConnection path still live in checkout lbug-adapter.ts:255
🟠 Risk 1-second watchdog can fire during legitimate slow createConnection lbug-adapter.ts:134
🟡 Issue Two independent process.stdout.write captures can diverge lbug-adapter.ts:56 / server.ts:282
🟡 Issue unhandledRejection swallowed; poor error serialization server.ts:315
🟡 Perf Pre-creating all 8 connections is a 4× resource increase lbug-adapter.ts:222

The two 🔴 bugs — the FTS/checkout race and the concurrent initLbug issue — should be addressed before merging, as they can reproduce the original segfault under parallel load, which is exactly the scenario this PR is meant to fix.

@magyargergo

Copy link
Copy Markdown
Collaborator

Could you please tackle with this? 🙏

- Add initPromises dedup map to prevent double-init race when parallel
  tool calls trigger initLbug for the same repoId simultaneously
- Move pool.set() after FTS load so concurrent checkout can't grab a
  connection mid-async-init (FTS race on available[0])
- Replace lazy createConnection growth path with integrity error — pool
  is pre-warmed, lazy creation would silence stdout during active queries
- Add preWarmActive flag so watchdog timer skips stdout restore during
  the synchronous pre-warm loop
- Unify stdout capture: server.ts imports realStdoutWrite from
  lbug-adapter instead of capturing its own copy
@magyargergo

Copy link
Copy Markdown
Collaborator

@claude can you recommend integration tests for this PR?

@claude

claude Bot commented Mar 18, 2026

Copy link
Copy Markdown

Claude finished @magyargergo's task in 2m 5s —— View job


Tasks

  • Gather context and understand request
  • Read changed files to understand the implementation
  • Analyze what integration tests are needed
  • Post integration test recommendations

Recommended Integration Tests for fix/mcp-parallel-stability

The existing test suite has lbug-pool.test.ts covering basic lifecycle, parameterized queries, and error handling, but none of the concurrency/parallelism scenarios that this PR specifically fixes. All tests below use the existing withTestLbugDB helper and fit naturally into gitnexus/test/integration/lbug-pool.test.ts (or a new lbug-pool-parallel.test.ts).


1. Core Regression: Parallel executeQuery calls — MAX_CONNS_PER_REPO at once

This is the exact bug from #326. Tests that 8 simultaneous queries (= pool size) all complete without crashing or dropping results.

describe('parallel query execution (regression: #326)', () => {
  it('handles MAX_CONNS_PER_REPO parallel executeQuery calls without crash', async () => {
    await initLbug('test-repo', handle.dbPath);
    const PARALLEL = 8; // = MAX_CONNS_PER_REPO
    const results = await Promise.all(
      Array.from({ length: PARALLEL }, () =>
        executeQuery('test-repo', 'MATCH (n:Function) RETURN n.name AS name')
      )
    );
    expect(results).toHaveLength(PARALLEL);
    for (const rows of results) {
      expect(rows.length).toBeGreaterThanOrEqual(2);
    }
  });

  it('handles parallel executeParameterized calls without crash', async () => {
    await initLbug('test-repo', handle.dbPath);
    const results = await Promise.all(
      Array.from({ length: 8 }, (_, i) =>
        executeParameterized(
          'test-repo',
          'MATCH (n:Function) WHERE n.name = $name RETURN n.name AS name',
          { name: i % 2 === 0 ? 'main' : 'helper' }
        )
      )
    );
    expect(results).toHaveLength(8);
    for (const rows of results) {
      expect(Array.isArray(rows)).toBe(true);
    }
  });

  it('handles mixed parallel executeQuery + executeParameterized calls', async () => {
    await initLbug('test-repo', handle.dbPath);
    const results = await Promise.all([
      executeQuery('test-repo', 'MATCH (n:Function) RETURN n.name AS name'),
      executeQuery('test-repo', 'MATCH (n:File) RETURN n.name AS name'),
      executeQuery('test-repo', 'MATCH (n:Function)-[r:CodeRelation]->(m) RETURN n.name AS name'),
      executeParameterized('test-repo', 'MATCH (n:Function) WHERE n.name = $name RETURN n', { name: 'main' }),
      executeQuery('test-repo', 'MATCH (n:Function) RETURN count(n) AS count'),
      executeParameterized('test-repo', 'MATCH (n:Function) WHERE n.isExported = $v RETURN n.name', { v: true }),
    ]);
    expect(results).toHaveLength(6);
    expect(Array.isArray(results[0])).toBe(true);
  });
});

2. Waiter Queue: More parallel queries than pool connections

Ensures the waiters queue path in checkout() works correctly — requests that exceed the 8-connection cap queue up and complete when connections are returned via checkin().

it('queues requests beyond pool capacity and fulfills them on checkin', async () => {
  await initLbug('test-repo', handle.dbPath);
  // Fire 12 queries against an 8-connection pool — 4 must queue as waiters
  const PARALLEL = 12;
  const results = await Promise.all(
    Array.from({ length: PARALLEL }, () =>
      executeQuery('test-repo', 'MATCH (n:Function) RETURN n.name AS name')
    )
  );
  expect(results).toHaveLength(PARALLEL);
  for (const rows of results) {
    expect(rows.length).toBeGreaterThanOrEqual(2);
  }
});

3. Concurrent initLbug for the Same repoId — No Double-Init

Tests the new initPromises deduplication guard. Without it, concurrent inits would race past the pool.get check, increment shared.refCount twice, and the second pool.set would silently overwrite and leak the first set of connections.

it('concurrent initLbug calls for the same repoId deduplicate correctly', async () => {
  // Fire 6 concurrent inits for the same repoId
  await Promise.all([
    initLbug('test-repo', handle.dbPath),
    initLbug('test-repo', handle.dbPath),
    initLbug('test-repo', handle.dbPath),
    initLbug('test-repo', handle.dbPath),
    initLbug('test-repo', handle.dbPath),
    initLbug('test-repo', handle.dbPath),
  ]);
  expect(isLbugReady('test-repo')).toBe(true);

  // Pool should be fully functional with correct connection count
  // (double-init would corrupt available[] or leak native connections)
  const results = await Promise.all(
    Array.from({ length: 8 }, () =>
      executeQuery('test-repo', 'MATCH (n:Function) RETURN n.name AS name')
    )
  );
  for (const rows of results) {
    expect(rows.length).toBeGreaterThanOrEqual(2);
  }
});

4. stdout.write Is Restored After Parallel Pool Init + Queries

Verifies the stdout silencing/restoration invariant holds under parallel load. If any silenceStdout/restoreStdout pairing is broken (e.g., from a thrown exception mid-construction), process.stdout.write stays as a no-op and MCP responses are silently dropped.

it('process.stdout.write is the real write after pool init and parallel queries', async () => {
  const realWrite = process.stdout.write.bind(process.stdout);
  await initLbug('test-repo', handle.dbPath);

  await Promise.all(
    Array.from({ length: 8 }, () =>
      executeQuery('test-repo', 'MATCH (n:Function) RETURN n.name AS name')
    )
  );

  // stdout.write must not be the silenced no-op
  expect(process.stdout.write).not.toEqual(expect.any(Function) && (() => true));
  // And must be callable (not throw)
  expect(() => process.stdout.write('')).not.toThrow();
});

5. Connection Checkin — No Leaks After Many Sequential Queries

Runs many sequential queries to verify checkin() always returns connections to the pool. A leak would eventually exhaust the 8-connection pool and block the 9th caller at the waiter timeout.

it('connections are returned to pool after each query (no leaks over 50 sequential queries)', async () => {
  await initLbug('test-repo', handle.dbPath);
  for (let i = 0; i < 50; i++) {
    await executeQuery('test-repo', 'MATCH (n:Function) RETURN n.name AS name');
  }
  // If connections leaked, this parallel batch would hang until WAITER_TIMEOUT_MS
  const results = await Promise.all(
    Array.from({ length: 8 }, () =>
      executeQuery('test-repo', 'MATCH (n:Function) RETURN n.name AS name')
    )
  );
  expect(results).toHaveLength(8);
}, 60_000);

6. Pool Entry Is Atomic — No Half-Built Pool Visible to Concurrent Queries

Validates that pool.set() happens only after pre-warm and FTS load complete, so callers never receive a partial pool entry. An early query (before init completes) must see "not initialized", not a pool with fewer than MAX_CONNS_PER_REPO connections.

it('executeQuery before initLbug completes sees "not initialized", never a partial pool', async () => {
  // Start init without awaiting
  const initPromise = initLbug('test-repo', handle.dbPath);

  // Immediate query before init resolves — must fail cleanly, not segfault
  const earlyResult = await executeQuery('test-repo', 'MATCH (n:Function) RETURN n')
    .catch((e: Error) => ({ error: e.message }));

  await initPromise;

  if (typeof earlyResult === 'object' && 'error' in earlyResult) {
    // Correct: pool wasn't ready yet
    expect(earlyResult.error).toMatch(/not initialized/);
  } else {
    // Also acceptable: init completed before the synchronous JS turn switched
    expect(Array.isArray(earlyResult)).toBe(true);
  }
});

7. Pool Integrity Error Is Thrown (Not a Silent createConnection Race)

After the PR, if connections are leaked somehow (e.g., checkin isn't called), checkout() now throws a Connection pool integrity error instead of silently calling createConnection() (which would silence stdout mid-query). Test this via a mock or by verifying the error message shape.

it('pool integrity error is thrown when connection count drops below MAX_CONNS_PER_REPO', async () => {
  // This test validates the guard added in checkout() for the lazy-create code path.
  // We can't easily trigger it without patching internals, so verify the error is
  // descriptive when the pool reports fewer connections than expected.
  // (Primarily a smoke test that the code path compiles and the message is useful.)
  await initLbug('test-repo', handle.dbPath);

  // Verify pool is functional at full capacity
  const rows = await executeQuery('test-repo', 'MATCH (n:Function) RETURN n.name AS name');
  expect(rows.length).toBeGreaterThanOrEqual(2);
  // Pool should not throw an integrity error on normal operation
});

File to Add Tests In

Add concurrency tests as a new describe('parallel query execution (regression: #326)') block inside the existing gitnexus/test/integration/lbug-pool.test.ts, using the same withTestLbugDB wrapper and seed data already defined there. Tests 1–6 are the highest-value ones to add before merging.

@magyargergo

Copy link
Copy Markdown
Collaborator

@claude would this PR fix #347, #314, #308? compare pros and cons

@claude

claude Bot commented Mar 18, 2026

Copy link
Copy Markdown

Claude finished @magyargergo's task in 2m 48s —— View job


Tasks


Note: The gh CLI and web tools were not approved during this run, so I could not read the body of issues #347, #314, and #308 directly. The analysis below is based on the PR diff, both changed files, the CHANGELOG, and the git history. If issue content is available, @magyargergo please share it and I'll refine this.


Would this PR fix #347, #314, #308?

What this PR actually fixes

The PR addresses one specific failure mode: the stdout race condition that occurs when createConnection() is called lazily during active MCP response writes. The four-part fix is:

Fix Mechanism
Pool pre-warming All 8 connections created at initLbug time, not lazily
Safe stdout proxy Transport uses realStdoutWrite captured at module load
initLbug concurrency guard initPromises Map deduplicates parallel inits for same repoId
Crash handlers uncaughtException → shutdown; unhandledRejection → log

Issue #347 (very recent — filed after #326 was open)

Likely scenario: A follow-up crash or regression seen after #326 was filed but before the fix landed. Given the timing (between #328 and #349), this is probably another variant of the parallel-tool-call crash — possibly triggered by a different tool combination or a slightly different race.

Would this PR fix it?


Issue #314 (between Phase 4 #310 and Phase 5 #315 type resolution)

Likely scenario: Filed during the type resolution phase rollout. Could be a crash or correctness issue in the resolver/ingestion pipeline, or an early MCP stability report coinciding with the LadybugDB migration (#275 landed around this era).

Would this PR fix it?

  • Unlikely if it's a type resolution/ingestion bug — the PR only touches lbug-adapter.ts (the connection pool) and server.ts (MCP transport). It has zero effect on the analysis pipeline.
  • Possibly if it's an MCP crash — if fix(impact): run DB queries sequentially to prevent segfault #314 reported silent MCP server crashes under load immediately after the LadybugDB migration, the stdout race described in this PR would be the root cause. Pre-warming would fix it.

Issue #308 (filed around the LadybugDB migration era)

Likely scenario: Filed just before the Phase 4 type resolution work began. If this is post-migration (#275), it could be a stability report about the new LadybugDB-backed MCP server.

Would this PR fix it?

  • Same split as fix(impact): run DB queries sequentially to prevent segfault #314 — if it's a crash during parallel MCP calls, yes. If it's a pipeline/analysis correctness issue, no.
  • One specific angle: the LadybugDB migration introduced createConnection() with silenceStdout(). Any crash filed immediately after that migration that described "silent server exit" or "Connection closed" from the client would be fixed by this PR.

Pros and Cons

Pros

  • Directly eliminates the root cause — pre-warming means silenceStdout() never runs during active MCP I/O, not just "reduces the window"
  • Defense in depth — even if pre-warming fails, the safe stdout Proxy ensures transport writes always use the real write reference
  • Atomic pool registrationpool.set() now happens after FTS load, so concurrent executeQuery calls see either a fully ready pool or "not initialized" — never a half-built one (fixes the FTS/checkout race from the prior review)
  • initPromises deduplication — concurrent initLbug calls for the same repo now correctly serialize, preventing connection leaks on cold start

Cons

  • Only fixes the MCP transport layer — issues rooted in the analysis pipeline (type resolution, ingestion, resolver bugs) are completely unaffected
  • 40 pre-created native connectionsMAX_POOL_SIZE=5 repos × MAX_CONNS_PER_REPO=8 = 40 connections at peak vs the old ~10; this may resurface on memory-constrained deployments
  • Watchdog fires unconditionally every 1s — as noted in the prior review, this is a module-level side effect that runs in every import context, and could prematurely restore stdout if createConnection legitimately takes >1s (e.g., slow disk on first open)
  • unhandledRejection swallowed — Node ≥15 exits on unhandled rejections by design; opting out may mask real pool or executor bugs that are surfaced as rejected promises

Summary verdict

Issue Likely fixed? Confidence Reason
#347 ✅ Probably yes Medium Recent crash report, timing matches, same failure domain
#314 ⚠️ Maybe Low Depends on whether it's an MCP crash vs pipeline bug
#308 ⚠️ Maybe Low Same as #314 — post-migration MCP crash: yes; analysis bug: no

To give a definitive answer, sharing the issue bodies for #347, #314, and #308 here would let me confirm whether their symptoms match the stdout race this PR fixes.

@magyargergo

Copy link
Copy Markdown
Collaborator

Could you please have a look at the comments above? 🙏

7 integration tests covering concurrent query safety, waiter queue
overflow, stdout.write restoration, connection leak detection, initLbug
deduplication, atomic pool visibility, and mixed query types.
Vitest's projects feature splits test files into two groups: lbug-db
(fileParallelism: false) and default (parallel). This prevents native
mmap file-lock conflicts on Windows without requiring the CI shell loop
locally.
/abhigyanpatwari#316

Verifies that 3 concurrent queries via Promise.all (the exact pattern
from the impact command's enrichment phase at local-backend.ts:1415)
complete without SIGSEGV on a pre-warmed connection pool.
@github-actions

Copy link
Copy Markdown
Contributor

CI Report

All checks passed

Pipeline Status

Stage Status Details
✅ Typecheck success tsc --noEmit
✅ Unit Tests success 3 platforms
✅ Integration success 3 OS x 4 groups = 12 jobs

Test Results

Suite Tests Passed Failed Skipped Duration
Unit 1568 1512 0 1 8s
Integration 1202 1182 0 19 51s
Total 2770 2694 0 20 59s

✅ All 2694 tests passed

20 test(s) skipped — expand for details

Integration:

  • Python match/case as-pattern type binding > resolves u.save() to User#save via match/case as-pattern binding
  • Python match/case as-pattern type binding > does NOT resolve u.save() to Repo#save (negative disambiguation)
  • Swift constructor-inferred type resolution > detects User and Repo classes, both with save methods
  • Swift constructor-inferred type resolution > resolves user.save() to Models/User.swift via constructor-inferred type
  • Swift constructor-inferred type resolution > resolves repo.save() to Models/Repo.swift via constructor-inferred type
  • Swift constructor-inferred type resolution > emits exactly 2 save() CALLS edges (one per receiver type)
  • Swift self resolution > detects User and Repo classes, each with a save function
  • Swift self resolution > resolves self.save() inside User.process to User.save, not Repo.save
  • Swift parent resolution > detects BaseModel and User classes plus Serializable protocol
  • Swift parent resolution > emits EXTENDS edge: User → BaseModel
  • Swift parent resolution > emits IMPLEMENTS edge: User → Serializable (protocol conformance)
  • Swift cross-file User.init() inference > resolves user.save() via User.init(name:) inference
  • Swift cross-file User.init() inference > resolves user.greet() via User.init(name:) inference
  • Swift return type inference > detects User class and getUser function
  • Swift return type inference > detects save function on User (Swift class methods are Function nodes)
  • Swift return type inference > resolves user.save() to User#save via return type of getUser() -> User
  • Swift return-type inference via function return type > resolves user.save() to User#save via return type of getUser()
  • Swift return-type inference via function return type > user.save() does NOT resolve to Repo#save
  • Swift return-type inference via function return type > resolves repo.save() to Repo#save via return type of getRepo()

Unit:

  • CLASS_CONTAINER_TYPES > contains expected class-like AST node types
  • CLASS_CONTAINER_TYPES > does not contain function types
  • CONTAINER_TYPE_TO_LABEL > maps class-like types to correct labels
  • findEnclosingClassId > TypeScript > finds enclosing class for a method
  • findEnclosingClassId > TypeScript > finds enclosing interface for a method signature
  • findEnclosingClassId > TypeScript > returns null for a top-level function
  • findEnclosingClassId > TypeScript > returns null when node has no parent
  • findEnclosingClassId > Python > finds enclosing class for a method
  • findEnclosingClassId > Java > finds enclosing class for a method
  • findEnclosingClassId > Java > finds enclosing interface for a method
  • findEnclosingClassId > C++ > finds enclosing class_specifier for a method
  • findEnclosingClassId > C++ > finds enclosing struct_specifier for a method
  • findEnclosingClassId > Rust > finds enclosing impl_item for a method
  • findEnclosingClassId > Rust > picks struct name (not trait name) for impl Trait for Struct
  • findEnclosingClassId > Rust > still picks struct name for plain impl Struct (no trait)
  • findEnclosingClassId > Rust > finds enclosing trait_item for a method
  • findEnclosingClassId > C# > finds enclosing class for a method
  • findEnclosingClassId > C# > finds enclosing record for a method
  • findEnclosingClassId > C# > finds enclosing struct for a method
  • findEnclosingClassId > Go > returns receiver struct ID for Go methods
  • findEnclosingClassId > edge cases > handles nested classes — returns innermost enclosing class
  • findEnclosingClassId > edge cases > returns null for a node without parent
  • findEnclosingClassId > edge cases > skips containers without a name node
  • PostToolUse with missing/corrupt meta.json > Plugin: emits stale when meta.json does not exist
  • PostToolUse with missing/corrupt meta.json > Plugin: emits stale when meta.json is corrupt
  • extractMethodSignature > returns zero params and no return type for null node
  • extractMethodSignature > TypeScript > extracts params and return type from a typed method
  • extractMethodSignature > TypeScript > extracts zero params from a method with no parameters
  • extractMethodSignature > TypeScript > extracts params without return type annotation
  • extractMethodSignature > Python > skips self parameter
  • extractMethodSignature > Python > handles method with only self
  • extractMethodSignature > Python > handles Python return type annotation
  • extractMethodSignature > Java > extracts params from a Java method
  • extractMethodSignature > Java > extracts zero params from no-arg Java method
  • extractMethodSignature > Kotlin > extracts params from a Kotlin function declaration
  • extractMethodSignature > Kotlin > extracts zero params from a no-arg Kotlin function
  • extractMethodSignature > C++ > extracts params from a nested C++ declarator
  • extractMethodSignature > C++ > extracts zero params from a no-arg C++ function
  • extractMethodSignature > C# > extracts params from a C# method
  • extractMethodSignature > C# > handles C# method with no params
  • extractMethodSignature > C# > extracts return type from C# method
  • extractMethodSignature > Go > extracts params and single return type
  • extractMethodSignature > Go > extracts multi-return type
  • extractMethodSignature > Go > handles no return type
  • extractMethodSignature > Go > marks variadic function with undefined parameterCount
  • extractMethodSignature > Rust > extracts return type from function
  • extractMethodSignature > C++ return types > extracts primitive return type
  • extractMethodSignature > C++ return types > extracts qualified return type
  • extractMethodSignature > C++ return types > returns undefined returnType for void
  • extractMethodSignature > C++ return types > marks variadic function with undefined parameterCount
  • extractMethodSignature > variadic params > Java: marks varargs with undefined parameterCount
  • extractMethodSignature > variadic params > Python: marks *args with undefined parameterCount
  • extractMethodSignature > variadic params > Python: marks **kwargs with undefined parameterCount
  • extractMethodSignature > variadic params > TypeScript: marks rest params with undefined parameterCount
  • extractMethodSignature > variadic params > Kotlin: marks vararg with undefined parameterCount
  • buildTypeEnv > known limitations (documented skip tests) > Ruby block parameter: users.each { |user| } — closure param inference, different feature

Code Coverage

Combined (Unit + Integration)

Metric Coverage Covered Base Delta Status
Statements 61.53% 6977/11339 47.27% 📈 +14.3 🟢 ████████████░░░░░░░░
Branches 55.6% 4780/8596 42.26% 📈 +13.3 🟢 ███████████░░░░░░░░░
Functions 60.62% 599/988 46.38% 📈 +14.2 🟢 ████████████░░░░░░░░
Lines 63.48% 6235/9821 48.88% 📈 +14.6 🟢 ████████████░░░░░░░░
Coverage breakdown by test suite

Unit Tests

Metric Coverage Covered Base Delta Status
Statements 46.45% 5268/11339 47.27% 📉 -0.8 🔴 █████████░░░░░░░░░░░
Branches 41.15% 3538/8596 42.26% 📉 -1.1 🔴 ████████░░░░░░░░░░░░
Functions 45.85% 453/988 46.38% 📉 -0.5 🔴 █████████░░░░░░░░░░░
Lines 48.03% 4718/9821 48.88% 📉 -0.9 🔴 █████████░░░░░░░░░░░

Integration Tests

Metric Coverage Covered Base Delta Status
Statements 46.84% 5312/11339 47.27% 📉 -0.4 🔴 █████████░░░░░░░░░░░
Branches 44.12% 3793/8596 42.26% 📈 +1.9 🟢 ████████░░░░░░░░░░░░
Functions 44.23% 437/988 46.38% 📉 -2.2 🔴 ████████░░░░░░░░░░░░
Lines 47.95% 4710/9821 48.88% 📉 -0.9 🔴 █████████░░░░░░░░░░░

📋 View full run · Generated by CI

@magyargergo magyargergo merged commit 60ede20 into abhigyanpatwari:main Mar 18, 2026
20 of 21 checks passed
motolese pushed a commit to motolese/datamoto-gitnexus that referenced this pull request Apr 23, 2026
… (abhigyanpatwari#349)

* fix: MCP server crashes under parallel tool calls (abhigyanpatwari#326)

* fix: ensure full connection pool is pre-created to avoid race conditions during query execution

* fix: improve graceful shutdown handling with exit codes

* fix: resolve critical concurrency bugs in connection pool init

- Add initPromises dedup map to prevent double-init race when parallel
  tool calls trigger initLbug for the same repoId simultaneously
- Move pool.set() after FTS load so concurrent checkout can't grab a
  connection mid-async-init (FTS race on available[0])
- Replace lazy createConnection growth path with integrity error — pool
  is pre-warmed, lazy creation would silence stdout during active queries
- Add preWarmActive flag so watchdog timer skips stdout restore during
  the synchronous pre-warm loop
- Unify stdout capture: server.ts imports realStdoutWrite from
  lbug-adapter instead of capturing its own copy

* test: add connection pool parallel stability tests

7 integration tests covering concurrent query safety, waiter queue
overflow, stdout.write restoration, connection leak detection, initLbug
deduplication, atomic pool visibility, and mixed query types.

* fix: run LadybugDB tests sequentially via vitest projects config

Vitest's projects feature splits test files into two groups: lbug-db
(fileParallelism: false) and default (parallel). This prevents native
mmap file-lock conflicts on Windows without requiring the CI shell loop
locally.

* test: add enrichment Promise.all regression test for abhigyanpatwari#292/abhigyanpatwari#316

Verifies that 3 concurrent queries via Promise.all (the exact pattern
from the impact command's enrichment phase at local-backend.ts:1415)
complete without SIGSEGV on a pre-warmed connection pool.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: MCP server crashes under parallel tool calls (cypher/query)

2 participants