* fix(mcp): a session serving in-process hands the project back to the shared daemon (#2277)
When a proxy lost its daemon (or never reached one) it served the rest of
the session through an in-process engine. That engine takes the project's
writer lock in fallback mode and nothing ever released it or tried the
daemon again, so every later daemon start for the project failed on the
held lock and every other session ran read-only without auto-sync until
that one session ended. Sessions that fell back read-only stayed read-only
for life too.
The degraded proxy now retries the shared daemon with backoff (5 s,
doubling to a 5 min cap; CODEGRAPH_DAEMON_RETRY_MS / _MAX_MS, 0 disables)
through the existing probe/spawn/poll path. A writing engine is stopped
first, after the calls it is serving have answered, so its lock is free
before a daemon is spawned and two writers never overlap; calls arriving
meanwhile are buffered and go to the daemon, or are served in-process
again if none comes up. A read-only engine keeps serving until a daemon
answers and is closed once its calls finish. The client's initialize is
replayed to the new daemon with its reply suppressed, as on first attach.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* docs(changelog): a degraded session hands the project back (#2277)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* test: make Windows load-sensitive tests wait on the tree, not the clock (#1773)
Each failure from the loaded Windows run was traced to its mechanism and
reproduced deterministically before changing it. No assertion is weakened.
- daemon-lock-refresh: the PowerShell holder slept 200ms against a 375ms
retry budget. Windows refuses a replace-rename while any handle to the
target is open, even a Node read handle, so hold one in-process and close
it inside the rename mock after the first real failure. Also asserts the
first rename really failed with a retryable code.
- mcp-projectpath-lifecycle: the owner daemon armed a 500ms idle timer at
start and exited (code 0) before a starved engine reached it. It now starts
with idle exit off and is armed over IPC once the engines hold a session.
The file waits for catch-up instead of the gate's 3s serve-anyway deadline,
and its index-heavy hooks and eviction case get explicit timeouts.
- mcp-daemon / mcp-writer-lock teardown: racing launchers spawn detached
daemon candidates whose cwd is the fixture; a loser still starting at
teardown pinned it (EBUSY) or took over after the winner was stopped. A
--require preload records candidate pids, and teardown waits for the
losers before stopping the winner.
- mcp-writer-lock: writer.pid appears before the daemon binds and rewrites
daemon.pid, so cleanup could probe a daemon mid-start ('unverified'); wait
for both proxies to attach. Removal uses fs.promises.rm, because fs.rmSync
gives up on the first Windows access-denied error despite maxRetries.
- mcp-daemon #1963: connect only after the proxy attached (lock before bind).
- sync-rebuild-convergence / multi-repo-workspace: explicit timeouts scoped
to the cases that build real indexes; the global timeout is unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(daemon): give the Windows pid-file replace a longer retry budget (#1773)
Windows refuses to replace a file while any handle to it is open, including a
plain Node read handle, so an antivirus scan, an indexer, or another session
reading daemon.pid can each block the daemon's startup refresh for a moment,
and for longer on a loaded machine. The retry waited 375ms in all; it now backs
off 25, 50, 100, 200, then 400ms three times, about 1.6s over eight attempts,
still well inside a launcher's ~6s connect window. Permanent failures and
non-Windows errors still abort at once, and an ownership change still stops
the retry.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* test: close resolution graphs before removing Windows fixtures
Close ArkTS and JVM graph connections in afterEach, including failed assertions. Verified both files on Windows 11 and macOS (32 tests).
* test: compare explore source lines across LF and CRLF checkouts
Normalize only line endings when matching emitted source against checked-out fixtures. Preserve the closure coverage and whole-line assertions. Both files pass on Windows 11 and macOS (15 tests).
* test: exercise old Git fallback without a POSIX shell shim
Inject only the unsupported ls-files invocation and run every other Git command against real repositories. Assert that each rejected call retries, use portable graph paths, and clean up fixtures. Passes on Windows 11 and macOS.
* test: normalize Python fixture before generating CRLF cases
The tests added in #2004 produced CRCRLF from a Windows checkout. Normalize the input to LF before constructing either variant. All 28 native and WASM cases pass on Windows 11 and macOS.
* test: allow time for eviction fixture indexes on Windows
The #2036 eviction test builds and reconciles nine real indexes and consistently exceeds its default five-second timeout on the VM. Allow 15 seconds for this test alone, preserving every assertion. The full file passes on Windows 11 and macOS (21 tests).
* test: finish MCP processes before resetting Windows fixtures
Launch actual servers with their runtime flags and await termination before directory removal. Use asynchronous removal retries for daemon fixtures. Establish a ready daemon and stop its first proxy before simulating PID reuse, then make source stale without opening a competing SQLite writer. Preserve lock and database byte-equality assertions. All touched files validated on Windows 11 and macOS.
* test: allow Windows named-pipe fallback polling to finish
The VM takes up to 25.7 seconds to complete the nominal six-second retry loop. Allow 30 seconds for the three fallback waits and 40 seconds for the two shorter enclosing tests, retaining their lock-preservation and read-only assertions. The full daemon file is validated on Windows 11 and macOS.
* fix(daemon): retry transient Windows PID-file sharing violations
A transient EPERM replacing daemon.pid aborted startup, observed in the quiet-client test and reproduced with a real locked handle at ba3c21e5. Retry only Windows sharing errors with five bounded delays, rechecking ownership each time and cleaning temporary files. Keep publication synchronous before client processing. Cover transient/permanent failures, changed ownership, non-retryable errors, and a real Windows handle; require a confirmed daemon round-trip before measuring a quiet session. Validated on Windows 11 and macOS.
* fix(mcp): close pending sockets and allow read-only fallback (#1963)
* fix(mcp): log when a fallback serves reads without auto-sync (#1963)
A read-only fallback starts no watcher and holds no writer lock, so a
session that quietly stopped syncing looked the same as a healthy one in
the logs. Name the holder on stderr when the fallback goes read-only.
Suggested by @bompus in #1979.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(mcp): stop pending connections and serve read-only fallbacks (#1963)
Pending handshakes could block daemon shutdown or register phantom sessions after disconnect.
Track accepted sockets and reject late hello continuations.
Use explicit read-only engines and SQLite connections for blocked fallbacks, suppressing synchronization, watching, migrations, and repairs.
Preserve writer ownership and the existing project lifecycle, with regression coverage for #1963 and #1356.
Co-authored-by: danusha2345 <danusha2345@users.noreply.github.com>
Co-authored-by: danusha2345 <ewidusoc498@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: danusha2345 <danusha2345@users.noreply.github.com>
Co-authored-by: danusha2345 <ewidusoc498@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* fix(mcp): enable query workers in direct mode (#1465)
Direct and proxy-fallback engines never enabled the query pool, serializing concurrent reads despite CODEGRAPH_QUERY_POOL_SIZE.
Enable bounded workers in both paths and support sessions without a default index, including indexes discovered later.
Preserve main-thread catch-up, staleness notices, and session accounting, with regression coverage for concurrency, parity, configuration, and teardown.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(mcp): enable query workers in direct mode (#1465)
Direct and proxy-fallback engines never enabled the query pool, serializing concurrent reads despite CODEGRAPH_QUERY_POOL_SIZE.
Enable bounded workers in both paths and support sessions without a default index, including indexes discovered later.
Preserve main-thread catch-up, staleness notices, and session accounting, with regression coverage for concurrency, parity, configuration, and teardown.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(mcp): enable query workers in direct mode (#1465)
Direct and proxy-fallback engines never enabled the query pool, serializing concurrent reads despite CODEGRAPH_QUERY_POOL_SIZE.
Enable bounded workers in both paths and support sessions without a default index, including indexes discovered later.
Preserve main-thread catch-up, staleness notices, and session accounting, with regression coverage for concurrency, parity, configuration, and teardown.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Large-codebase indexing died at the end of "Resolving refs" two ways:
watchdog kills of healthy work (24k-file Java on Windows, #1212 — third
iteration of the #1091/#1122 class) and hard OOMs (Linux kernel scale,
where v1.3.0 could not complete at any watchdog setting). Root causes:
~31 of 37 dynamic-edge synthesis passes ran start-to-finish with no
yield points, several materialized whole-graph snapshots (kotlin
expect/actual opened with getAllNodes() — 2M nodes in one array; the
C fn-pointer pass retained every C file's contents twice plus every
function node), and the post-index WAL checkpoint ran minutes of
synchronous IO on the main thread, killing even a successful index at
the finish line.
The pipeline tail now follows the same discipline as the rest: never
hold O(graph) in the heap, yield everywhere.
- All synthesis passes stream node-kind scans (cursors, not arrays) and
yield on time-budgeted checkpoints; language gates skip passes whose
filters a project's file languages provably can't satisfy.
- kotlin expect/actual filters SQL-side; c-fnptr caches are LRU-bounded,
units stream one file at a time, and the all-functions array +
write-only id map are gone; spring reads each .java once, not twice.
- runMaintenance moved to a worker thread (own SQLite connection);
per-file store commits chunk with yields behind a serialized flush
chain (preserving #1015 file-order determinism); resolver warm-up
streams the DISTINCT name set; resolution batch-tail and merged-edge
inserts run in bounded sub-transactions.
- Daemon: fixed a socket-handoff race that could leave a fresh MCP
session permanently silent (client-hello tail unshifted into a
flowing stream with zero listeners — the long-standing #662 test
flake was this real bug); first tool call no longer queues behind
the query pool's cold start (pool.ready gate).
Validation: Linux kernel (70,129 files, 2.05M nodes, 6.4M edges) fully
indexes in 27m8s on a 2-core/6GB container at default heap + default
watchdog; llvm-project (180k files) completes under 1GB RSS including
kill-and-sync recovery; synthesized-edge and full-graph parity are
byte-identical vs baseline on elasticsearch/redis/vim; the ex-flaky
daemon test passed 25/25 under load. Env-gated diagnostics kept:
CODEGRAPH_SYNTH_TIMINGS pass/phase timings, CODEGRAPH_MCP_DEBUG hop
tracing. Design record: docs/design/main-thread-stall-followup.md.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
The shared daemon's inactivity backstop (#692) reaped the daemon after
maxIdleMs (default 30 min) of no inbound query bytes whenever a client was
still connected — without ever checking whether that client was actually
alive. lastActivityAt is fed only by inbound socket data and MCP has no
keepalive, so a genuinely-live session that just hadn't queried CodeGraph in
30 min tripped it. The daemon then exited, and the proxy's onDaemonLost
degrades that session (and every other session sharing the daemon) to an
in-process engine for the rest of its life. On one dev machine over a day the
backstop fired 20 times on live sessions (clients=1) and the liveness sweep
caught 0 real dead peers — net harm.
The backstop exists only to catch a phantom client (one counted but gone,
whose socket-close was never delivered). It now consults the peer pids the
daemon already tracks: after the inactivity window it sweeps provably-dead
peers, then reaps the daemon only if NO remaining client can be proven alive
(every one is an unknown-pid connection the sweep can't verify — the sole
phantom class it can't catch). One provably-alive client keeps the daemon up.
Extracted the decision into Daemon.backstopShouldExit(isAlive) so it's unit-
testable with an injected liveness probe, mirroring reapDeadClients. All #692
guarantees preserved; the only behavior change is that a provably-alive quiet
session is no longer reaped.
- daemon-client-liveness.test.ts: 7 new deterministic cases for
backstopShouldExit (live kept, phantom reaped, mixed protects the live one,
dead-peer swept-then-held, within-window, zero-client).
- mcp-daemon.test.ts: the integration test that asserted the backstop reaps a
live connected client (it encoded the bug) now asserts the opposite — a
live-but-quiet session survives several backstop windows with its lockfile
intact and no backstop shutdown logged.
Validated end-to-end on the built bundle: a quiet session's daemon stayed up
across 4 backstop windows (maxIdle=3s), same pid throughout, zero backstop
fires. Found while fixing #1185.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The "Attached to shared daemon" line is benign INFO, but it was written to
stderr — and MCP hosts render all server stderr at error level (and append an
`undefined` data field), so on every session start a healthy attach showed up
as `[error] … undefined`. It is now gated behind CODEGRAPH_MCP_LOG_ATTACH=1:
silent by default, opt-in for debugging daemon attach. Both attach sites
(runProxy + connectWithHello) route through one helper. The daemon integration
tests opt the harness into the log so their attach assertions still observe a
successful attach.
Re-applies the approach from #640 by @mturac.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
When an MCP host (opencode and others) SIGTERM's the shared daemon as a new
session starts, the existing session's proxy used to exit on the dropped socket
— silently losing CodeGraph for that session, and hanging any request in flight
at the drop. The SIGTERM originates in the host's process-tree teardown, not in
CodeGraph (nothing here signals another process), so the fix is proxy
resilience, not chasing the signal.
The local-handshake proxy now treats a daemon disconnect as recoverable rather
than terminal: it falls back to its in-process engine for the rest of the
session (the same path used when no daemon is reachable at startup, and what
CODEGRAPH_NO_DAEMON does) and re-serves any requests that were in flight to the
dead daemon, so the host never hangs. The proxy still exits when the HOST goes
away (stdin close / PPID watchdog) — only daemon loss is now non-fatal.
Also replaces the over-the-wire liveness-sweep test added in #712 — which was
flaky under heavy parallel load (a raced raw-socket connect) — with a
deterministic Daemon.reapDeadClients unit test. The client-hello round-trip is
still exercised by every daemon test (the real proxy now sends it).
Validated with a reproduction (proxy stays alive, in-flight request answered,
post-drop request recovers) and a regression test in mcp-daemon.test.ts.
Confirmed on macOS (full suite green) and a Windows 11 VM.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Layer-2 defense-in-depth follow-up to the Windows PPID watchdog fix (#711).
That fix makes an orphaned proxy exit so its socket closes and the daemon
reaps via the refcount + idle timer. This adds two daemon-side safety nets for
the residual case where a socket close is never delivered (a Windows named-pipe
hazard) and a phantom client would otherwise pin the daemon forever:
- Liveness sweep: a proxy now sends an optional client-hello carrying its pid
(+ host pid) right after verifying the daemon hello; the daemon periodically
drops any client whose peer process is dead, re-arming the idle timer.
Fail-safe and version-pinned — a connection that never sends the hello just
falls back to the socket-close lifecycle, and the daemon reads it before the
transport so a non-hello first line is handed through untouched.
- Inactivity backstop: the daemon exits after a generous no-traffic window
(CODEGRAPH_DAEMON_MAX_IDLE_MS, default 30 min) even with clients attached, so
a phantom client that sends nothing can't keep it alive.
Pure helpers (parseClientHelloLine, peerIsDead) are unit-tested; the full
handshake + sweep and the backstop are covered end-to-end in mcp-daemon.test.ts.
Validated on a real Windows 11 VM: the sweep reaps a dead-pid client over a
named pipe and the backstop fires with a client still connected.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Trace endpoint relevance (overloaded names resolve to the real implementation instead of an empty protocol/delegate stub), Swift closure-collection synthesizer, multi-phase god-file explore rendering, and serve --mcp cold-start handshake sped ~811ms→~90ms (proxy answers initialize/tools-list locally). Full suite green (1090 pass).
One shared, detached daemon per project root: every `codegraph serve --mcp` is a thin stdio<->socket proxy (Unix socket / Windows named pipe) to it, so N agents in one repo share a single file watcher, SQLite connection, and tree-sitter warm-up instead of N copies. The daemon outlives any single session and reaps via client-refcount + idle timeout; `CODEGRAPH_NO_DAEMON=1` opts out.
Hardened during review: detached-process lifecycle (preserves the #277 watchdog via the proxy; the daemon no longer orphans on host SIGKILL), atomic lockfile + pid-verified stale-clear (no double-daemon on concurrent startup), realpath root canonicalization. Validated on macOS, Linux (Docker - 3x fewer inotify watches for 3 agents), and Windows (named pipes); A/B confirms byte-identical tool output vs direct mode. Closes#411.
Co-Authored-By: Colby McHenry <me@colbymchenry.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>