A SIGKILL'd process (the #850 liveness watchdog, OOM, a crash) leaves its WAL
on disk; the next session appends to the same file; and nothing ever truncated
it — PASSIVE checkpoints fold frames but keep the file at its high-water mark,
and the one shrinking path (a clean last-connection close) is exactly what a
killed-daemon world never takes. Observed at 25.6 GB on a 5.46 GB DB, growing
until the disk filled.
- journal_size_limit on every connection: resetting checkpoints now clip the
WAL back to the cap instead of leaving it at its high-water mark.
- healOversizedWal() fired from every DatabaseConnection.open: off-thread
PASSIVE fold + TRUNCATE when the leftover WAL exceeds the cap (64 MB,
CODEGRAPH_WAL_HEAL_MB to override). Single-flight per connection with
bounded retries — concurrent passes defeat each other (each checkpoint sees
the other as a busy reader).
- Daemon/direct MCP watchdogs now pass progressPaths (DB + WAL), extending the
#1231 slow-disk deferral to the long-lived server so a healthy daemon mid
slow statement isn't SIGKILL'd — fewer kills, fewer leaked WALs.
- codegraph status shows WAL size (human + JSON) and warns when it dwarfs the
DB; daemon.log lines and the watchdog kill notice now carry ISO timestamps
so kills can be placed in time.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
The recommended MCP config launches the local binary, so a server left
running drifts behind releases silently — users discover the version gap
only when something breaks. Per the reporter's preferred option 1, the
server now checks the latest GitHub release in the background on startup
(never blocking; reuses the upgrade command's release-redirect resolution
so the two can't drift) and surfaces a one-line notice on three surfaces:
one stderr line (the MCP host's server log), the initialize instructions
(with do-not-run-it-yourself guidance for the agent), and codegraph_status.
Discipline: results cache in ~/.codegraph/update-check.json shared across
every proxy/daemon on the machine — 24h TTL on success, 1h backoff after
failure, an outage never hides an already-known update, and a stale cache
re-kicks a background refresh so long-lived daemons keep noticing. The
initialize path is a memoized synchronous cache read (the respond-fast
contract holds), and both handshake answerers (session + proxy) share one
helper so they can't diverge. Never stdout.
Hardening: the latest tag arrives from a network redirect via an on-disk
cache and ends up inside agent-visible instructions, so only a canonical
vX.Y.Z rebuilt from PARSED semver fields is ever interpolated — a tag
carrying trailing text (parseSemver is not end-anchored) renders without
it, and a non-version tag renders nothing and counts as a failed attempt.
Off is off: CODEGRAPH_NO_UPDATE_CHECK=1 (dedicated) or DO_NOT_TRACK=1
(broad convention — already set by data-plane deployments) suppresses the
network call and the notice entirely. Documented in TELEMETRY.md.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
An MCP host that kills the launcher chain within the server's first ~100ms
while keeping the stdio pipes open (config probe, cancelled request, startup
timeout; Rust hosts that kill a child without dropping its stdio handles) left
the server orphaned: it booted already reparented to init, so the PPID
watchdog's "ppid changed" baseline was captured as 1 and could never fire, and
stdin never EOF'd. The process lingered — idle, ~30MB — until the host itself
exited, accumulating one per abandoned launch (the pile-up reported in #1185).
Reproduced on released 1.2.0/macOS: SIGKILL the launcher at +50ms → permanent
orphan; at +150ms the old late baseline had already run and reaped it.
Three-part fix:
- Capture process.ppid at the earliest line of the CLI entry (early-ppid.ts)
and use it as every watchdog baseline, shrinking the blind window to the few
ms before our first JS runs.
- Thread the real host pid down the bundled path: the npm shim and the
standalone sh launcher set CODEGRAPH_HOST_PPID (an outer launcher's value
wins), so the watchdog polls the host directly. Previously only the
--liftoff-only relaunch set it, leaving the entire npm/standalone install
base with hostPpid=null.
- Never-initialized backstop (startup-handshake.ts): a serve --mcp that
receives no MCP traffic for CODEGRAPH_STARTUP_HANDSHAKE_TIMEOUT_MS (default
15min, 0 disables) shuts down — the catch-all for a kill landing in the
residual pre-JS window. Disarmed on the first byte, so a quiet-but-live
session is never touched.
Also scrub CODEGRAPH_HOST_PPID from the detached daemon's env — it has no host,
and a stale pid must not leak into anything it spawns.
Validated end-to-end on the built bundle: the +50ms early-kill orphan is now
reaped while the host still holds the pipes open, and all six normal
lifecycle paths (clean close, SIGTERM/SIGKILL child, host exit/SIGKILL,
fd-holding adversarial host) stay clean. New coverage in
startup-handshake.test.ts, mcp-startup-orphan.test.ts, and npm-shim.test.ts.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A project kept on an ExFAT/FAT external volume (or some network mounts /
WSL2 DrvFs) broke the background auto-sync daemon at two points, both
because the filesystem lacks POSIX features the daemon relied on:
1. Lock acquisition hard-links a temp file onto .codegraph/daemon.pid for
race-free exclusivity (#411) — these filesystems have no hard links.
2. The Unix-domain socket listen() fails regardless of path length, so the
old length-only tmpdir fallback never triggered.
Both surface as a capability error, but each OS reports a DIFFERENT errno
for the same gap (macOS ENOTSUP, Linux EPERM, Windows EISDIR), so the fix
is policy-based rather than an enumerated code-set:
- Lock: fall back to an O_EXCL create on any non-EEXIST link error. The
temp write already proved the directory is writable, so the fallback
either succeeds (still atomic + exclusive, "first writer wins") or
surfaces its own genuine error.
- Socket: an ordered candidate list [in-project, tmpdir] walked by BOTH
the daemon (binds) and the proxy (connects) — they converge on the
fallback with zero coordination. Relocate past any non-EADDRINUSE bind
error; EADDRINUSE still rethrows, preserving the #974 contract.
Normal repos are unaffected: the in-project candidate binds first, and the
hard-link lock path is unchanged.
Validated end-to-end on real removable-drive filesystems: macOS ExFAT
(hdiutil image), Linux FAT32 (Docker loop mount), Windows exFAT (diskpart
VHD) — each acquires the lock, relocates (or binds a named pipe on
Windows), and serves a real client.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Three fixes for a repo that commits a large JS/TS theme/SDK (Metronic under
static/, ~1,600 tracked files):
1. A SECOND "Resolving refs" quadratic that #915 didn't cover. #915 capped
import-name collisions; this caps method-name collisions (init/update/render
re-declared on every widget), which flow through matchMethodCall Strategy 3
and findBestMatch instead. New AMBIGUOUS_NAME_CEILING (default 500, env
CODEGRAPH_AMBIGUOUS_NAME_CEILING): above it the fuzzy strategies decline
rather than score K candidates — no proximity score can pick the one true
target among thousands anyway. Resolving drops from O(K^2) to linear in refs
(e.g. 900-file synthetic: 28.7s -> 3.4s), edge counts unchanged, and the cap
never fires on normal repos (max real method-collision ~40).
2. A new `exclude` array in codegraph.json keeps git-TRACKED paths out of the
index, which .gitignore can't do (enumeration is `git ls-files`). Mirrors the
existing includeIgnored plumbing across the git, sync, and non-git-walk
paths.
3. `index`/`init` now install the #850 liveness + #277 ppid watchdogs (which
were serve-only), so a wedged or orphaned indexer self-terminates instead of
pinning a core. The --liftoff-only relaunch's spawnSync can't forward
signals, so killing the parent shim used to orphan the worker.
Tests: ubiquitous-name ceiling, exclude (incl. tracked-file exclusion on git +
non-git), orphan self-termination (POSIX), and ppid-parser units. Shared the
ppid parsers out of mcp/index.ts into mcp/ppid-watchdog.ts.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Belt-and-suspenders follow-up to #855. Any non-yielding sync loop on the main
thread wedges the event loop, and nothing running on that loop (timers, signal
handlers, PPID watchdog) can recover it — only another thread can.
A tiny worker thread (in the detached daemon + direct modes) watches a
shared-memory heartbeat the main thread bumps each event-loop turn; if it stops
advancing across enough consecutive checks (~CODEGRAPH_WATCHDOG_TIMEOUT_MS,
default 60s) the worker SIGKILLs the process so a fresh daemon starts on the next
connection. Counts consecutive stale checks (not wall-clock) so it's immune to
clock jumps / sleep; tuned never to fire on real work; opt out with
CODEGRAPH_NO_WATCHDOG=1.
Adds anonymous usage statistics (commands/tools used, languages indexed,
connecting agents) with a strict, auditable allowlist. Never code, paths,
file/symbol names, queries, or IPs.
- src/telemetry/: zero-dep client — consent resolution (DO_NOT_TRACK >
CODEGRAPH_TELEMETRY > stored choice > default-on), random machine UUID,
in-memory counters → capped JSONL buffer → completed-day rollups; sync
exit-append (survives process.exit) + opportunistic bounded sends; the
first-run notice gates the first SEND, never local buffering, so the
installer's consent toggle always precedes it. Off is off: no recording,
no socket, buffered data deleted.
- codegraph telemetry status|on|off; per-command counting via preAction hook.
- MCP: tool counting after the reply is on the wire (session + proxy
in-process fallback), agent attribution from initialize clientInfo,
unref'd daemon flush interval. Zero hot-path cost, zero stdout.
- Installer: visible default-on consent toggle (asked once, never re-asked),
install/index/uninstall lifecycle events.
- telemetry-worker/: public Cloudflare Worker behind telemetry.getcodegraph.com
— allowlist validation, IP stripping, per-machine rate limit, forwards to
PostHog as anonymous events. Ships nowhere with the npm package.
- TELEMETRY.md (field-by-field contract) + README section + design doc.
- 20 unit tests; suite-wide CODEGRAPH_TELEMETRY=0 guard so tests never
pollute real telemetry. Full suite: 1448 passing.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
A stdio MCP server's lifeline is stdin: when the host/client goes away,
stdin should end and the server should exit. The server paths listened
for stdin 'end'/'close' but NOT 'error'.
That gap bites with a socket-backed stdin — the shape VS Code / Claude
Code use (a socketpair, not a pipe). On client death the socket can
surface as an 'error' (ECONNRESET/hangup) instead of a clean 'close'.
Unhandled, it escalated to the process-wide uncaughtException handler,
which logs and keeps running — so the server orphaned instead of
exiting. On Linux a POLLHUP socket fd left registered in epoll then
wakes the event loop continuously, pinning a core at 100% CPU; once the
main thread spins, the setInterval PPID watchdog can't even fire, so the
orphan runs forever (the report's 28+ minutes).
Add treatStdinFailureAsShutdown(): listen for 'error' as well as
'end'/'close', and DESTROY the stdin stream on any terminal event so the
fd leaves epoll and can't churn, then run the path's shutdown. Wired into
the live paths — startDirect, the local-handshake proxy, and
StdioTransport — plus the legacy pipe proxy. Fires once (re-entry guard).
Note: this is hardening for a class of failure that matches every piece
of the report's evidence (socket stdin, userspace main-thread spin, high
involuntary context switches, watchdog never firing), but the exact 100%
CPU spin could not be reproduced in Docker (Linux) across /dev/null EOF,
socket peer-death (RST/FIN), the reporter's 0.9.7 bundle, and the npx
chain — all exited cleanly — so the trigger is environment-specific.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
On Windows the PPID watchdog could never fire: orphans aren't reparented, so
`process.ppid` stays constant after the parent dies (defeating the ppid-change
check), and the standalone bundle pre-bakes `--liftoff-only`, skipping the
relaunch that sets `CODEGRAPH_HOST_PPID` (defeating the host-liveness check).
With neither signal available, an orphaned proxy / direct server ran forever,
the shared daemon never saw the client disconnect, and its idle timer never
armed — node processes accumulated until CPU saturated.
Add a win32-only signal: poll the original parent's liveness directly, since
ppid is stable there. Gated to Windows so POSIX double-fork cases keep relying
on the ppid-change signal (a dead original parent is not proof of orphaning on
POSIX). The decision is extracted into a pure, unit-tested helper shared by all
three watchdog sites (proxy socket, proxy local-handshake, direct mode).
Validated on a real Windows 11 VM: in the exact bundle scenario (direct mode,
no HOST_PPID) an orphaned server now exits within one watchdog poll via the new
path; the POSIX reparent path is unchanged and its integration test still passes.
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>
* fix(sync): detect changes via filesystem, not git status
Incremental sync detected changes with `git status --porcelain`, which only sees uncommitted working-tree changes — so committed changes from git pull/checkout/merge/rebase (clean tree afterward) were never reconciled, and non-git projects leaned on a slow full rescan. Change detection is now filesystem-based and git-independent: a (size, mtime) stat pre-filter skips unchanged files, then a content hash confirms the rest; removals are checked against the filesystem (git ls-files still lists deleted-but-unstaged files). Also adds a non-blocking catch-up sync on MCP connect so changes made while the server was down (e.g. a terminal git pull) are reconciled on connect.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(changelog): add 0.9.5 entry for filesystem-based sync fix
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add a PPID watchdog to the MCP server so a `codegraph serve --mcp` child terminates when its host (Claude Code, opencode, …) is force-killed — OOM killer, `kill -9`, container teardown — and the stdin close handlers don't fire. The child would otherwise linger indefinitely, holding inotify watches, file descriptors, and the SQLite WAL.
Also propagates the host PID across the `--liftoff-only` re-exec (CODEGRAPH_HOST_PPID) so the watchdog reaps the orphan on the from-source path too, not just the bundled launcher. Poll interval is CODEGRAPH_PPID_POLL_MS (default 5000ms, 0 disables).
Resolves#277.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
MCP tools failed with "CodeGraph not initialized" when a client launched
the server outside the project and sent no rootUri/workspaceFolders — the
server fell back to its own cwd, missed the project's .codegraph/, and
returned a misleading "run codegraph init" error on every call. The only
workaround was passing projectPath by hand to each tool.
When no explicit path is given, the server now asks the client for its
workspace root via the standard MCP roots/list request (gated on the
client advertising the roots capability) before falling back to cwd. This
required teaching the stdio transport to send server->client requests and
match their responses by id (previously responses were dropped as invalid).
When a project still can't be resolved, the error now names the directory
it searched and tells the user to pass projectPath or add --path to the
MCP config, instead of pointing at a re-init they don't need.
Reported-by: @zhangyu1197
Closes#196
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Recursive fs.watch on a WSL2 /mnt NTFS/9p mount walks the directory tree
with every readdir/stat crossing the Windows boundary, stalling the event
loop long enough to blow past opencode's 30s MCP handshake timeout so the
tools never appear. This is the file-watcher half of the #172 fix, which
moved the DB/WASM open off the handshake but left the watcher on the
critical path.
- Add watchDisabledReason() policy: CODEGRAPH_NO_WATCH (off) >
CODEGRAPH_FORCE_WATCH (force on) > WSL2 + /mnt auto-detect (off).
FileWatcher.start() and the MCP server both honor it; the server now
logs why watching is off and how to refresh.
- Add `codegraph serve --mcp --no-watch`.
- When watching is off, init/install offer git sync hooks (post-commit,
post-merge, post-checkout) that run `codegraph sync` in the background,
or fall back to manual sync; either way the user is told the index
stays frozen until re-synced. uninit removes the hooks.
- Tests: watch-policy + git-hooks (idempotency, user-content preservation,
core.hooksPath).
Root-cause analysis and workaround by @mengfanbo123.
Closes#199
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The MCP `initialize` handler was awaiting `tryInitializeDefault` —
which opens the SQLite DB and runs `await initGrammars()` (tree-sitter
WASM bootstrap) — before sending the JSON-RPC response. On slow
filesystems (Docker Desktop VirtioFS on macOS, WSL2) this could exceed
Claude Code's ~30s handshake timeout, leaving the codegraph child
process alive and unresponsive with no tools visible in the client.
Send the response first; defer the open to a tracked background
promise. The lazy retry path used by `tools/list` and `tools/call`
now awaits that promise instead of racing it with `openSync`, so we
never double-open the SQLite file.
Adds a subprocess-based regression test that asserts the JSON-RPC
response arrives on stdout before `startWatching()` logs to stderr.
This ordering check catches the regression on any filesystem, not
just slow ones where the timing matters in practice.
Reported by @sashanclrp; isolated by @sgrimm's wire capture.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a universal tool-selection playbook surfaced by MCP clients
(Claude Code, Cursor, opencode, LangChain, OpenAI Agent SDK) in the
agent's system prompt automatically. Without this, agents have to
infer tool composition from individual tool descriptions and tend to
walk callers manually instead of reaching for codegraph_impact, etc.
Scoped tight: only the 9 tools that exist on main today
(search/context/callers/callees/impact/node/explore/files/status), no
"(when present)" references to unmerged tools, no per-language
guidance. ~40 lines of useful guidance.
Salvaged from #121, which bundled the instructions with #117's MCP
tool-registry refactor and referenced many tools that don't exist on
main.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Addresses the need for automatic graph synchronization on file changes. Implements FileWatcher using native OS file events (FSEvents/inotify/ReadDirectoryChangesW) with 2-second debouncing to prevent thrashing on rapid saves. Filters changes against include/exclude patterns and ignores .codegraph directory modifications. Integrates with CodeGraph API (watch/unwatch/isWatching methods) and MCP server for automatic activation. Updates documentation to reflect shift from semantic to full-text search and removal of manual hook installation requirements.
Eliminates anonymous error reporting functionality that was collecting stack traces and error context via Sentry. Removes all telemetry-related code, configuration options, and documentation references.
Fixes#47 — "database is locked" after crash and MCP "not initialized"
when project IS initialized.
- FileLock: treat locks older than 10 minutes as stale regardless of PID
status, covering cases where PID was reused or kill signal check fails
- MCP server: log errors from tryInitializeDefault() to stderr instead of
silently swallowing, so transient open failures are diagnosable
- MCP server: retryInitIfNeeded() properly cleans up failed instances
before retrying, preventing resource leaks
- CLI: add 'codegraph unlock' command for manual lock file removal
Adds support for Dart and Liquid languages with tree-sitter parsing.
Improves accuracy of code symbol extraction for existing languages.
Indexes project files to enhance code navigation features.
Migrates build system to facilitate code contributions.
Removes git hook functionality.
Integrates Sentry for error tracking and reporting.
Enhances project initialization and configuration loading.
- Add evaluation test suite with TypeScript and Python fixtures
- Fix MCP server to defer CodeGraph init until rootUri received
- Fix call edge extraction by calling resolveReferences() after indexAll/sync
- Fix glob matching for root-level files (e.g., **/*.py now matches auth.py)
- Fix duplicate node extraction for methods inside classes
- Update context tests to use buildContext for semantic search + graph traversal
- Export unused formatter functions to fix build
Evaluation results:
- TypeScript: 96% precision, 79% recall, 85% F1
- Python: 99% precision, 80% recall, 85% F1
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>