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
- Wait for the concurrent `gitnexus analyze` to finish, then retry the query or restart the session — this error is designed to be transient.
- Check for a stale/abandoned analyze process (e.g. `ps aux | grep gitnexus`) and terminate it if it crashed while holding the lock.
- Re-run `gitnexus analyze` once the repo is quiet to rebuild a clean index and release locks.
- 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
- Avoid running `gitnexus analyze` and MCP query sessions concurrently on the same repo; serialize them.
- Schedule indexing (analyze) outside query-heavy windows.
- Use one long-lived MCP server instead of spawning multiple processes against the same index.
- Use exponential backoff with a cap in retry loops so long analyze runs are tolerated.
- Alert on repeated occurrences — it usually means a crashed analyze left a stale lock.
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
- Analysis did not finalize for
- Analysis did not finalize for
- Analyze worker crashed
- Bridge query prepare failed
- Cannot park after an in-place FTS abort )` : ''}. The…
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 divergentView on GitHub (pinned to ac9a4e9abd)