mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-09-17 23:52:36 +00:00
* fix(lbug): await evict-then-reopen so it can't race the checkpoint
closeOne() closed the evicted repo's shared Database with a
fire-and-forget `db.close().catch(() => {})` (no await). Both call
sites that evict-then-reopen — evictLRU() right before doInitLbug
opens the new connection, and the "idle & changed" path in initLbug —
proceeded to open the next repo's connection immediately after,
without waiting for the evicted repo's close (and the checkpoint it
triggers) to finish. On the real engine the new open can then collide
with that still-in-flight checkpoint, surfacing on any read as:
Runtime exception: Cannot open database in read-only mode while
checkpoint is in progress. Please retry later.
This reproduces reliably once more than MAX_POOL_SIZE (5) distinct
repos are queried within a short window (self-hosted deployments with
more than a handful of active repos hit it routinely), and gets worse
under genuinely concurrent requests for different repos, since nothing
serialized pool mutations across callers either.
Fix:
- closeOne / evictLRU are now async and await their internal work
(closeOne's own close() call; evictLRU's call to closeOne), closing
the race within a single initLbug call.
- The exported initLbug is wrapped in a small async mutex
(initLbugInner does the real work) so concurrent initLbug calls for
different repos serialize instead of each racing their own
evict-then-reopen against the others.
- closeLbug's two closeOne() calls are now awaited too — closeOne
becoming async meant closeLbug could resolve before pool.delete()
had actually run, which a repo-pinning test caught (isLbugReady()
briefly still true right after a resolved closeLbug()).
- closeOne now deletes the pool entry (and clears its pin, and
notifies pool-close listeners) BEFORE the awaited db.close(), not
after. Review caught that the previous order left a "zombie" entry
reachable via pool.get(repoId) — closed=true, available emptied, but
still present — for the duration of that await; a same-repo
query/init landing in that window would see isLbugReady() as true
and hit a "Connection pool integrity error" in checkout() instead of
just reopening. Deleting first removes the entry entirely, so a
concurrent caller takes the normal fresh-open path instead.
Verified two ways:
- Against the compiled bundle (`ghcr.io/abhigyanpatwari/gitnexus`,
1.6.10/1.6.11 — pool-adapter.js is byte-identical between them): an
A/B docker build with 7 tiny local repos and genuinely concurrent
(parallel, not sequential) /api/graph requests goes from 7/7 failing
to 7/7 succeeding on a freshly-analyzed pool.
- Unit tests here (mocks @ladybugdb/core the same way as
lbug-pool-pinning.test.ts): one asserts the evicted repo's close()
completes before the initLbug call that triggered the eviction
settles; another asserts closeLbug's own promise doesn't resolve
before the underlying close() does. Both gate their mock's close()
on a real short delay and were confirmed to fail against code that
drops the corresponding await.
Note: a second, deeper issue was also observed in the docker A/B
setup — repeated rounds of concurrent access show a repo that has
gone through one evict+reopen cycle can become permanently unable to
reopen for reads, identically with and without this fix. That did not
reproduce with mocks and isn't understood yet; filed separately as
#3186, which stays open and untouched by this PR — this fix closes a
real, root-caused bug on its own but does not resolve #3186 by itself.
Second review round caught a follow-up: the idle-timeout sweep calls
closeOne(repoId) directly, outside of initLbug's poolLock. Now that
closeOne deletes the pool entry before its awaited close(), an
unsynchronized idle close racing a same-repo initLbug could let that
initLbug treat the repo as absent while the idle close (and its
checkpoint) is still in flight — reopening the same class of race this
PR exists to close, just via the idle path instead of LRU eviction.
Routed the idle sweep's closeOne call through withPoolLock too, so it
serializes against initLbug the same way evictLRU already does.
(Tried to add a mocked regression test for this specific interleaving;
dropped it — the mock's dbCache-reuse path masks the difference
regardless of the fix, so it could not be made to discriminate
reliably. Fixed by direct code review instead, same as the note below
already does for the native-engine-specific checkpoint collision.)
Also removed the initLbugInner per-repoId initPromises dedup map: with
every initLbug call now serialized through poolLock, a second call for
a repoId already being initialized cannot observe a pending promise in
initPromises (the first call always fully completes, including its
finally-block cleanup, before the lock releases) — the branch was dead
code the bot correctly flagged twice.
Third review round caught two more follow-ups on the same theme (both
introduced by making the idle sweep route through poolLock):
- closeLbug()'s no-arg ("close everything") branch still calls closeOne
directly in a loop over a snapshotted pool.keys(), without the lock —
an initLbug racing that loop could register a fresh entry the
snapshot never saw, leaving it resident after a call meant to empty
the pool. Wrapped the snapshot+loop in withPoolLock.
- The idle timer callback can now sit queued behind an in-progress
initLbug before its turn arrives, and that init (or a concurrent
touchRepo()) can refresh lastUsed in the meantime — so the pre-lock
idleness check taken when the timer fired can be stale by the time
it actually runs. Re-check lastUsed/checkedOut again inside the lock,
right before closing, instead of trusting the outer snapshot.
* fix(deps): pin @ladybugdb/core back to 0.18.3
Bisected the "checkpoint is in progress" symptom (root cause #2, not
touched by the pool-adapter.ts fix in the previous commit) down to a
single dependency-version-bump commit with zero application code
changes: e91ea0ca, "chore(deps): bump @ladybugdb/core in /gitnexus",
0.18.3 -> 0.19.0.
Confirmed both ends independently, on the actual official build
(Dockerfile.cli), no engine-swapping involved:
- v1.6.9 (native 0.18.3, as released): 7 tiny repos, 5 rounds of
genuinely concurrent /api/graph requests each — 0/35 failures.
- v1.6.10/v1.6.11 (native 0.19.1, as released): same repro — fails
every round from round 2 onward.
- v1.6.11 completely unmodified (not even this repo's own fix) with
ONLY @ladybugdb/core downgraded to 0.18.3 (real `npm install
@ladybugdb/core@0.18.3 --save-exact`, full rebuild, no application
code touched): 0/56 failures across 8 rounds.
That last point isolates this fully: none of GitNexus's own JS changes
between 1.6.9 and 1.6.11 (including the dbIdentity/rebuild-detection
logic added in #2614, or anything in sidecar-recovery.ts) are
load-bearing for this symptom — the regression lives entirely in the
native engine, introduced somewhere between 0.18.3 and 0.19.0.
Full unit suite green with this pin (14818 passed, same 2
environment-specific flakes present on main regardless of this change
— macOS realpath symlink resolution in analyzer-identity.test.ts and a
subprocess retry-count assertion in review-agent-workflow.test.ts,
neither touches lbug/ladybugdb).
This is a pragmatic pin, not a long-term fix: 0.19.0+ presumably ships
fixes of its own that 0.18.3 lacks, and the actual regression should
still be root-caused and fixed upstream (tracked at
LadybugDB/ladybug#919, which a maintainer is already engaging with).
Recommend re-evaluating this pin once that's resolved.
* fix(lbug): serialize closeLbug's single-repo branch with the pool lock
Review on PR #3189 caught the same class of gap as three earlier
rounds on the previous PR: closeOne now deletes the pool entry before
its awaited db.close() finishes, so an initLbug(repoId, ...) racing
this branch could acquire the lock right after the delete, see no
cached entry, and start opening a fresh connection while this close's
checkpoint is still in flight — reopening the exact race withPoolLock
exists to close. The no-arg ("close everything") branch already went
through the lock; this makes the single-repoId branch consistent with
it.
npx tsc --noEmit clean, full lbug/pool unit suite (19 files, 257
tests) green.
* fix(lbug): address azizur100389's review findings on PR #3189
- pool-adapter.ts: the idle timer's in-lock recheck already re-verified
lastUsed/checkedOut against a fresh snapshot, but not pinnedRepos —
pinRepo() can run while the timer callback is queued behind an
in-progress initLbug, and the timer would then still close a repo the
caller just pinned, dropping that lease entirely (LOW finding).
- Storage-version mismatches (opening an index written by a different
@ladybugdb/core build, e.g. after downgrading the pinned dependency)
surfaced as GitNexus's generic "unavailable, retry later" and were
retried LOCK_RETRY_ATTEMPTS times for nothing, since the file's
on-disk version never changes between retries (HIGH/blocking
finding). Added isStorageVersionMismatchError() to lbug-config.ts and
wired it into both places that actually open a LadybugDB connection:
pool-adapter.ts's doInitLbug (used by MCP tools/wiki/group-sync) and
lbug-adapter.ts's doInitLbug (the separate single-connection path
/api/graph and /api/query use via withLbugDb). Both now fail fast
with an actionable "run `gitnexus analyze --force`" message instead.
In lbug-adapter.ts the check wraps both openLbugConnection and
ensureReadOnlyConnectionUsable: the native engine's storage-version
check isn't necessarily enforced until the first real query runs
(ensureReadOnlyConnectionUsable's own probe), so openLbugConnection
alone can succeed on a mismatched file.
Verified end-to-end against the real native engine: registered a repo
under the pinned 0.18.3 engine, swapped its .gitnexus/lbug file for one
written by an unmodified v1.6.11 (0.19.1) image, restarted the server
to bypass in-memory connection caching, and queried /api/graph — got
the actionable message instead of the generic retry-later error.
`npx tsc --noEmit` clean; full lbug/pool unit suite (36 tests, 14
files) green.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix(test): export isStorageVersionMismatchError from wholesale lbug-config mocks
doInitLbug's catch block (both pool-adapter.ts and lbug-adapter.ts) now
calls isStorageVersionMismatchError() unconditionally on every open
failure, but 5 test files wholesale-mock lbug-config.js without that
export — Vitest rejects access to an undeclared mocked export, so any
test driving an error through that catch block (e.g. the WAL-recovery
and evict-reopen-race suites) breaks (bot finding on PR #3189).
Added isStorageVersionMismatchError (stubbed to always return false —
none of these suites exercise the storage-version path) and
STORAGE_VERSION_MISMATCH_SUGGESTION to each mock, matching the existing
isWalCorruptionError/WAL_RECOVERY_SUGGESTION pattern already there.
analyze-pagesize-error.test.ts was not affected: it mocks lbug-config.js
via importOriginal, so it already re-exports the real function.
Verified: the 6 affected files (48 tests) pass; npx tsc --noEmit clean.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix(test): export isStorageVersionMismatchError from remaining lbug-config mocks
The previous commit fixed the 5 test files that wholesale-mock
lbug-config.js via vi.mock(), but missed 4 more that mock it via
vi.doMock() instead (a different Vitest API my earlier grep for
vi.mock(...) didn't match): lbug-adapter-wal-schema.test.ts (8
call sites), lbug-checkpoint-lifecycle.test.ts (12 call sites), and
basicblock-callee-ids-schema.test.ts / convex-metadata-persistence-
contract.test.ts (1 shared mock factory each). All of these exercise
lbug-adapter.ts's doInitLbug, which now also calls
isStorageVersionMismatchError() unconditionally on every open failure
— caught by actually running the full suite rather than trusting the
`-t lbug` name filter, which doesn't match these files' test names.
Verified: all 10 affected files (97 tests) pass; npx tsc --noEmit
clean. Full suite rerun in progress to confirm no other gaps remain.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix(lbug): fail-fast storage-version mismatch and unlock pool lock-retry
Incremental analyze was warning through a version mismatch, and lock-retry
sleep held the pool mutex so one analyze-locked repo blocked every other
init. Fail immediately with the rebuild hint on both adapters, and sleep
outside withPoolLock so other repos can open during backoff.
Co-authored-by: Cursor <cursoragent@cursor.com>
* chore(autofix): apply prettier + eslint fixes via /autofix command
* Address PR review feedback (#3189)
- Serialize initLbugWithDb on withPoolLock so it cannot attach to a Database
closeOne is still checkpointing.
- Delete pin leases again after the awaited close so a pin acquired during
teardown cannot survive onto the next init.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
136 lines
3.9 KiB
JSON
136 lines
3.9 KiB
JSON
{
|
|
"name": "gitnexus",
|
|
"version": "1.6.11",
|
|
"description": "Graph-powered code intelligence for AI agents. Index any codebase, query via MCP or CLI.",
|
|
"author": "Abhigyan Patwari",
|
|
"license": "PolyForm-Noncommercial-1.0.0",
|
|
"homepage": "https://github.com/abhigyanpatwari/GitNexus#readme",
|
|
"repository": {
|
|
"type": "git",
|
|
"url": "git+https://github.com/abhigyanpatwari/GitNexus.git",
|
|
"directory": "gitnexus"
|
|
},
|
|
"bugs": {
|
|
"url": "https://github.com/abhigyanpatwari/GitNexus/issues"
|
|
},
|
|
"keywords": [
|
|
"mcp",
|
|
"model-context-protocol",
|
|
"code-intelligence",
|
|
"knowledge-graph",
|
|
"cursor",
|
|
"claude",
|
|
"codex",
|
|
"ai-agent",
|
|
"gitnexus",
|
|
"static-analysis",
|
|
"codebase-indexing"
|
|
],
|
|
"type": "module",
|
|
"bin": {
|
|
"gitnexus": "dist/cli/index.js"
|
|
},
|
|
"files": [
|
|
"dist",
|
|
"hooks",
|
|
"scripts",
|
|
"skills",
|
|
"vendor",
|
|
"web"
|
|
],
|
|
"scripts": {
|
|
"build": "node scripts/build.js",
|
|
"build:web": "node scripts/build.js --web",
|
|
"serve": "tsx src/cli/index.ts serve",
|
|
"dev": "tsx watch src/cli/index.ts",
|
|
"test": "vitest run",
|
|
"test:unit": "vitest run test/unit",
|
|
"pretest:integration": "node scripts/build.js",
|
|
"test:integration": "vitest run test/integration",
|
|
"test:watch": "vitest",
|
|
"test:coverage": "vitest run --coverage",
|
|
"test:cross-platform": "tsx scripts/run-cross-platform.ts",
|
|
"postinstall": "node scripts/build-tree-sitter-grammars.cjs",
|
|
"assert-publish-coverage": "node scripts/assert-publish-grammar-coverage.cjs",
|
|
"prepare": "node scripts/build.js",
|
|
"prepack": "node scripts/assert-publish-grammar-coverage.cjs && node scripts/build.js --web && node scripts/assert-web-assets.mjs web",
|
|
"version": "node scripts/sync-plugin-manifests.mjs"
|
|
},
|
|
"dependencies": {
|
|
"@ladybugdb/core": "0.18.3",
|
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
"@scarf/scarf": "^1.4.0",
|
|
"busboy": "^1.6.0",
|
|
"chokidar": "^5.0.0",
|
|
"cli-progress": "^3.12.0",
|
|
"commander": "^15.0.0",
|
|
"cors": "^2.8.5",
|
|
"express": "^5.2.1",
|
|
"express-rate-limit": "^8.4.1",
|
|
"fast-xml-parser": "^5.11.1",
|
|
"glob": "^13.0.6",
|
|
"graphology": "^0.26.0",
|
|
"graphology-indices": "^0.17.0",
|
|
"graphology-utils": "^2.3.0",
|
|
"graphql": "^17.0.2",
|
|
"ignore": "^7.0.5",
|
|
"js-yaml": "^5.0.0",
|
|
"jsonc-parser": "^3.3.1",
|
|
"mnemonist": "^0.40.3",
|
|
"node-addon-api": "^8.0.0",
|
|
"node-gyp-build": "^4.8.0",
|
|
"onnxruntime-common": "^1.26.0",
|
|
"pandemonium": "^2.4.0",
|
|
"pino": "^10.3.1",
|
|
"pino-pretty": "^13.1.3",
|
|
"proxy-addr": "^2.0.7",
|
|
"tree-sitter": "0.21.1",
|
|
"tree-sitter-c-sharp": "0.23.1",
|
|
"tree-sitter-cpp": "0.23.2",
|
|
"tree-sitter-go": "^0.23.0",
|
|
"tree-sitter-java": "^0.23.5",
|
|
"tree-sitter-javascript": "^0.23.0",
|
|
"tree-sitter-php": "^0.23.0",
|
|
"tree-sitter-python": "0.23.4",
|
|
"tree-sitter-ruby": "^0.23.1",
|
|
"tree-sitter-rust": "0.23.1",
|
|
"tree-sitter-typescript": "^0.23.2",
|
|
"uuid": "^14.0.0"
|
|
},
|
|
"optionalDependencies": {
|
|
"@huggingface/transformers": "^4.1.0",
|
|
"onnxruntime-node": "^1.24.0"
|
|
},
|
|
"trustedDependencies": [
|
|
"@ladybugdb/core",
|
|
"gitnexus",
|
|
"tree-sitter"
|
|
],
|
|
"devDependencies": {
|
|
"@babel/generator": "^8.0.0",
|
|
"@babel/parser": "^8.0.0",
|
|
"@babel/traverse": "^8.0.0",
|
|
"@babel/types": "^8.0.0",
|
|
"@types/busboy": "^1.5.4",
|
|
"@types/cli-progress": "^3.11.6",
|
|
"@types/cors": "^2.8.17",
|
|
"@types/express": "^5.0.6",
|
|
"@types/node": "^26.0.0",
|
|
"@types/proxy-addr": "^2.0.3",
|
|
"@vitest/coverage-v8": "^4.0.18",
|
|
"gitnexus-shared": "file:../gitnexus-shared",
|
|
"tsx": "^4.0.0",
|
|
"typescript": "^5.4.5",
|
|
"vitest": "^4.0.18"
|
|
},
|
|
"overrides": {
|
|
"adm-zip": ">=0.6.0",
|
|
"sharp": ">=0.35.0",
|
|
"@huggingface/transformers": {
|
|
"onnxruntime-node": "$onnxruntime-node"
|
|
}
|
|
},
|
|
"engines": {
|
|
"node": "^22.18.0 || >=24.11.0"
|
|
}
|
|
}
|