abhigyanpatwari/GitNexus · warning

LadybugDB unavailable for

Error message

LadybugDB unavailable for ${repoId}. Another process may be rebuilding the index. Retry later. (${err?.message || 'unknown error'})

What it means

initLbug opens (or reuses) the LadybugDB database handle for a repo and retries the native open up to LOCK_RETRY_ATTEMPTS times when it fails with a lock error. If every retry still reports a lock conflict (e.g. a concurrent `gitnexus analyze` holds the file), it gives up and throws this 'LadybugDB unavailable' error. It signals transient lock contention, not corruption; the caller is expected to retry later.

Solutions

  1. Wait for the concurrent `gitnexus analyze` to finish, then retry the query or restart the session — this error is designed to be transient.
  2. Check for a stale/abandoned analyze process (e.g. `ps aux | grep gitnexus`) and terminate it if it crashed while holding the lock.
  3. Re-run `gitnexus analyze` once the repo is quiet to rebuild a clean index and release locks.
  4. If contention is chronic, serialize access: run queries through one server and avoid triggering analyze while a backend session is active.

Example fix

// before
await initLbug(repoId, dbPath); // throws under concurrent analyze
// after
try {
  await initLbug(repoId, dbPath);
} catch (err) {
  if (String(err).includes('LadybugDB unavailable')) {
    await new Promise((r) => setTimeout(r, 2000)); // let the concurrent analyze release the lock
    await initLbug(repoId, dbPath);
  } else throw err;
}
Defensive patterns

Strategy: retry

Validate before calling

import fs from 'node:fs';
// before querying, verify the index exists and no analyze appears to be running
if (!fs.existsSync(dbPath)) throw new Error('Run: gitnexus analyze');
// optionally: detect a live gitnexus analyze process before proceeding

Type guard

const isLadybugUnavailable = (err: unknown): err is Error =>
  err instanceof Error && err.message.startsWith('LadybugDB unavailable for ');

Try / catch

try {
  await initLbug(repoId, dbPath);
} catch (err) {
  if (isLadybugUnavailable(err)) {
    // transient: wait for the concurrent analyze, then retry with backoff
    await new Promise((r) => setTimeout(r, 5000));
    await initLbug(repoId, dbPath);
  } else throw err;
}

Prevention

When it happens

Trigger: Calling initLbug (directly or through the MCP/local query backend) while another process holds the database file lock — typically a concurrent `gitnexus analyze` writing the index — such that all retry attempts with linear backoff (LOCK_RETRY_DELAY_MS * attempt) expire while the writer still holds the lock.

Common situations: Running an MCP server session while `gitnexus analyze --index-only` rebuilds the index in another terminal; CI jobs that index and query in parallel; a crashed analyze process that left a stale lock; multiple MCP server instances contending for the same repo at startup.

Related errors


AI-assisted analysis of abhigyanpatwari/GitNexus@ac9a4e9abd (2026-09-15). Data as JSON: /api/errors/aa0f3fce7d7eed0e. Report an issue: GitHub.

Appendix: source

Thrown at gitnexus/src/core/lbug/pool-adapter.ts:760

 * *sleeps* run outside the mutex so one analyze-locked repo does not block
 * every other pool init for LOCK_RETRY_DELAY_MS * attempt.
 *
 * Returns `true` when this call (re)opened a fresh handle onto the current
 * on-disk file, `false` when it reused/served the existing handle (unchanged,
 * or changed-but-a-query-is-in-flight). Callers that gate their own freshness
 * bookkeeping on "did the pool actually roll over" (LocalBackend) use the
 * return value; callers that only need the pool ready can ignore it.
 */
export const initLbug = async (repoId: string, dbPath: string): Promise<boolean> => {
  let lastError: Error | undefined;
  for (let attempt = 1; attempt <= LOCK_RETRY_ATTEMPTS; attempt++) {
    const result = await withPoolLock(() => initLbugInner(repoId, dbPath));
    if (result.status === 'done') return result.reopened;
    lastError = result.error;
    if (attempt === LOCK_RETRY_ATTEMPTS) break;
    await sleep(LOCK_RETRY_DELAY_MS * attempt);
  }
  throw ladybugUnavailableError(repoId, lastError);
};

const initLbugInner = async (repoId: string, dbPath: string): Promise<InitLbugAttempt> => {
  const existing = pool.get(repoId);
  if (existing) {
    existing.lastUsed = Date.now();
    // Detect an index that `analyze` rebuilt or mutated under this live read
    // pool. Without this, the pool keeps serving the old (POSIX:
    // unlinked-but-open) inode until LRU/idle eviction — a stale-read window
    // of up to IDLE_TIMEOUT_MS after analyze finishes.
    const current = await statDbIdentity(dbPath);
    if (!dbIdentityChanged(existing.dbIdentity, current)) {
      return { status: 'done', reopened: false }; // unchanged → reuse
    }
    // A query is in flight on this entry; closing its connection (and the
    // shared Database at refCount 0) mid-use is a native use-after-free. Serve
    // the current handle for this dispatch — the next initLbug that finds the
    // entry idle (checkedOut === 0) reopens, since the identity stays divergent

View on GitHub (pinned to ac9a4e9abd)