feat: fetch a per-platform engine and add a JetBrains IDE client - #17
Merged
Conversation
…dex steer, walkthrough Telemetry (30d, ~2,540 active machines) showed two large leaks: ~445 machines produced a zero-file index and 84% never returned, and of ~2,394 that activate cleanly only ~20% ever open a visible surface. This addresses both. - funnel.ts: diagnose why an index came back empty (no folder / no supported languages / indexPaths misconfig / all excluded / server gap) and offer the one recovery that fixes that cause, replacing the dead-end "Indexed 0 files" toast. Diagnosis scopes to indexPaths when set so the misconfig case is actually reachable. Notifications are fire-and-forget so an agent-driven index never hangs awaiting a dialog it can't answer. - First successful index shows a one-time (globalState-gated) steer to the surfaces telemetry shows convert best (Symbols tree, Call Graph). - codegraphSymbols empty state: actionable "Index Workspace / Open Walkthrough" welcome gated on a new codegraph.indexed context key (was a blank panel). - Getting-started walkthrough (index -> explore -> call graph -> AI assistant) plus a codegraph.openWalkthrough command. - New funnel.* telemetry (zeroFileIndex, zeroFileCta, firstIndexCta) with bounded enums; the command-palette reindex now also emits index.completed (it previously emitted none). - Shared filesIndexed()/reportIndexTelemetry() helpers remove the response coercion + telemetry mapping duplicated across 5 sites. - 12 unit tests for the diagnosis and glob logic (inline vscode mock; the legacy vsforge suites remain quarantined). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Telemetry showed humans engage visible surfaces (tree views: 483 machines,
call graph: 116) far more than agent tools (238), yet those surfaces were
buried in the Explorer and there was nothing inline in the editor. This adds
first-class discoverable surfaces.
Activity bar:
- Dedicated CodeGraph activity-bar container with an SVG icon; the Symbols and
Memories tree views move out of the Explorer into it (registration is by view
id, unchanged).
Server:
- New batched LSP request codegraph/getDocumentCodeLens(uri): per function/
method, returns caller count, related-test count, and cyclomatic complexity
in one pass, so the editor issues one request per document instead of N.
- Caller/test counting filters to EdgeType::Calls (mirrors helpers::get_callers)
so structural Contains edges don't inflate counts; test-vs-production split
uses a new shared node_props::is_test_like(), which the PR-review path now
also calls so the two can't diverge.
Editor:
- CodeLens above every function ("N callers · M tests · complexity X"), click
reveals the symbol and opens its call graph; matching hover with the same
stats. Backed by one version-cached request per document, evicted on close
and invalidated on reindex. Registered for all on-disk files (server returns
empty for unindexed files, so no language list to drift).
- codegraph.codeLens.enabled / codegraph.hover.enabled toggles (default on),
in the settings snapshot; engagement.codeLensClicked telemetry.
Tests: 2 net-new server tests (counts incl. a Contains-edge regression guard,
invalid-uri error); extension tsc + 12 vitest green. Verified via workflow
code-review (6 findings, all fixed).
Note: the bundled platform binaries need a cross-platform rebuild before
CodeLens works end-to-end in a shipped extension.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…atch The Phase 1 CodeLens endpoint was registered only in handle_custom_request (the `codegraph/*` slash-method dispatcher), which is dead code: the LSP service is built with `LspService::new` and never registers those custom methods, so slash requests return -32601. The live custom-request path is `workspace/executeCommand` matching `codegraph.*` dot-commands in backend.rs::execute_command. As shipped, codegraph/getDocumentCodeLens would have 404'd in the real extension - the unit test passed only because it calls the handler directly, and the code review didn't exercise the runtime dispatch. Building the vsix and probing the running server surfaced it. - backend.rs: add the live `codegraph.getDocumentCodeLens` arm (mirrors getWorkspaceSymbols), and drop the dead slash registration from custom_requests.rs. - codeLensProvider.ts: call via workspace/executeCommand instead of the unregistered slash RequestType. - navigation.rs: skip test functions via is_test_like (structural marker + name/path heuristic) so languages without a structural test marker (Python test_*) don't get a noise lens, matching caller classification. Verified end-to-end against a real indexed workspace: do_work -> 1 caller, 1 test, complexity 2; test functions suppressed. 11 navigation tests pass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Symbols pane was empty on indexed workspaces: SymbolTreeProvider sent `codegraph/getWorkspaceSymbols` via the slash RequestType, which routes to the dead handle_custom_request (unregistered on the LSP service) and returns method-not-found, so the provider caught the error and rendered nothing. Same root cause as the CodeLens dispatch fix. Switch to workspace/executeCommand with the live `codegraph.getWorkspaceSymbols` dot-command (already handled in backend.rs::execute_command). Verified: returns symbols on an indexed workspace. Memories provider already used executeCommand and was unaffected. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…eshooting section The CODEGRAPH_SKIP_MEMORY_CHECK / 0-MB-detection-failure guidance (added with the #13 fix) was unlabeled prose after the Options table, so a user hitting 'embeddings disabled on my Mac' wouldn't find it by scanning headings. Give it a Troubleshooting heading and tighten the wording. Docs only. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
All analysis stays in codegraph-server. The plugin spawns it, speaks LSP over stdio via LSP4IJ, and renders the results - so this is a client shell, not a second implementation. LSP4IJ rather than the platform's own LSP API: the latter is limited to the paid IDEs, which would exclude IDEA CE, PyCharm CE and Android Studio. LSP4IJ is Apache-2.0 and exposes the underlying LSP4J server, so dropping to raw LSP4J stays available if that dependency ever becomes a problem. Surfaces: Code Vision (callers/tests/complexity above declarations), a Symbols tool window, dependency and call graph panels on JCEF with a text fallback, a status bar widget, settings, and the indexing prompt/reindex flow. Engine binaries are not bundled. The Marketplace ships one artifact for every platform, so bundling all four would mean a ~120 MB download for every user to get the ~30 MB they can use. The engine is resolved from an existing install instead; a downloader was written and removed because the per-platform release assets it would fetch do not exist yet. Two verification harnesses, because neither covers the other: - scripts/engine_probe.py replays the plugin's exact wire traffic with no IDE, and diffs the command enum and settings defaults against the engine and the VS Code client so those hand-written files cannot drift silently. - SelfCheckActivity (inert without -Dcodegraph.selfcheck=true) answers what only a running IDE can: JCEF availability, tool window instantiation. Notable fixes found while building this: - getWorkspaceSymbols must omit `query` entirely for the unfiltered view; an empty string takes the engine's modules-only branch and yields an empty tree on a healthy index. Verified against an indexed workspace: 0 vs 7 symbols. - indexOnStartup defaults to false, matching VS Code and the engine. Defaulting it to true raced the index prompt and indexed the workspace twice. - The pre-index grace period now waits for the engine to finish `initialize` rather than for the non-blocking start() call, which provided no grace at all. - Startup activity returns early in headless environments, where there is no user to prompt - this also stops searchable-options generation from hanging. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013iTPP3eSpGSgWN6c5w4Fm1
Second tab on the existing CodeGraph tool window rather than its own sidebar slot: memories are the same graph seen from a different angle, and two CodeGraph icons would be two things to learn. Lists via codegraph.memoryList and switches to codegraph.memorySearch once a query is typed, since those are different commands with different response shapes. Both take currentOnly, so invalidated entries are filtered by the engine rather than re-filtered client-side. Invalidated memories render greyed rather than hidden when shown - silently drawing them as current would be worse than showing they exist. Verified against a seeded store: memoryList returns the entries, the tool window reports two tabs, and no exceptions reach the log. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013iTPP3eSpGSgWN6c5w4Fm1
The VS Code client declares 28 languageModelTools. Those are Copilot-specific, and porting them would mean a second hand-written tool list drifting against the engine. Pointing the AI tooling at the engine's own MCP mode instead keeps the tool surface correct for free - verified end to end, the registered command answers an MCP initialize and lists 42 tools. Writes <project>/.mcp.json in the mcpServers shape that Junie, Claude Code, Cursor and the AI Assistant MCP settings all read, and offers the same config on the clipboard, since every AI client stores MCP configuration somewhere different and pasting always works. Merges rather than overwrites: a project may already point at other MCP servers, and silently dropping them to add ourselves would be a hostile way to install a feature. Tests cover that case specifically - it is the failure that is invisible until some unrelated AI tool stops working. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013iTPP3eSpGSgWN6c5w4Fm1
… fix Telemetry sends the same event and property names as the VS Code client so both editors land in one funnel, adding only ide/ideProduct/ideBuild so a dashboard can split by editor without a second schema. The gate is a pure function with its own tests. A mistake there means measuring someone who declined, which nothing downstream can detect or undo, so every refusal path is asserted individually rather than trusting one happy path. The IDE's own statistics consent is a hard gate the plugin setting can narrow but never widen, and a build with no compiled-in key cannot send at all - so builds from source and forks are silent with no setting to remember. Unknown values are dropped rather than sent as "unknown": a placeholder string looks like a real value in a dashboard and inflates whatever bucket it lands in. Also fixes a pre-existing startup race, found by a self-check run that failed where earlier runs had passed. start() is asynchronous, so a command issued straight after it could get a null server back and fail fast. That is "not up yet", not "cannot run" - and it fails the first command of a session, which is the one a user is most likely to notice, such as a reindex they just asked for. The plugin configuration verifier caught Kotlin apiVersion set to 2.1 while since-build 243 only guarantees 2.0; the mismatch would surface as a NoSuchMethodError on a 2024.3 user's machine rather than at build time. Verifier IDE selection narrowed from recommended() to the development platform: each recommended release is a ~3 GB download, which is a lot of someone else's disk to consume by default. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013iTPP3eSpGSgWN6c5w4Fm1
… verifier
The plugin verifier now runs against both ends of the supported range and
reports Compatible for IC-243 and IC-252. Getting there required two real fixes.
Telemetry no longer consults StatisticsUploadAssistant.isSendAllowed(). That is
@ApiStatus.Internal - not an API plugins may call, and one that can change
without notice. There is no public equivalent, because the platform does not
offer third-party plugins a statistics-consent signal to honour at all.
So the setting is now opt-in rather than default-on. The VS Code client can
default to on because VS Code exposes env.isTelemetryEnabled, a platform consent
it honours; with no such signal here, defaulting to on would mean collecting
from people who never agreed to anything. The marketplace disclosure and the
gate's own documentation say so plainly.
ToggleActionButton is deprecated and scheduled for removal, which matters more
than usual because until-build is unbounded - a removal would break the Memories
toolbar on IDEs this plugin claims to support. Replaced with ToggleAction.
Verifier IDE selection is current() + latest{} rather than pinned versions: a
pinned "newest" stops being newest without anyone noticing, which is exactly the
break it exists to catch.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013iTPP3eSpGSgWN6c5w4Fm1
…rors Three fixes found by building a second editor client against this engine. Embedding settings are no longer gated on `extensionPath`. They sat inside `if let Some(extension_path)`, which made a VS Code-specific option the gate for two unrelated settings: any client that omitted it silently lost its embedding model choice and fell back to signature-only embeddings, degrading duplicate detection, clustering and similarity search with nothing in the log to say why. The path itself is optional and only ever meant "a directory the client owns", so it is now logged rather than load-bearing. Verified: initialize without extensionPath now honours granite-97m and fullBodyEmbedding, where before it logged CRITICAL and used the defaults. `codegraph.getDocumentCodeLens` is now advertised in executeCommandProvider. It was dispatched but unadvertised; VS Code never noticed because it uses the custom-request form, but a client that gates on ServerCapabilities - LSP4IJ's supportsCommand does - would treat the whole inline-CodeLens surface as unsupported. Checked for the collision the neighbouring comment warns about: no VS Code command is registered under that id. Advertised count 35 -> 36. memoryStore no longer maps its failure through `.map_err(|_| internal_error())`. That discarded the cause and logged nothing, so a store failure was unactionable from a user report - and it led me to a wrong diagnosis that took five probes to characterise and was still wrong. It now logs and returns the reason. 361 server tests pass; the JetBrains contract probe passes against the rebuilt engine. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013iTPP3eSpGSgWN6c5w4Fm1
Pre-existing `cargo fmt --check` violations in navigation.rs and parser_registry.rs, neither touched by the preceding fix. Separated from that commit so the functional change stays readable; this one is pure reflow and changes no behaviour. 361 tests still pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013iTPP3eSpGSgWN6c5w4Fm1
Measured before the fix: 312 last-phase.<pid>.json files in ~/.codegraph, the oldest two months old. After: 4, all belonging to live processes. clear() only removes the current process's marker, and main.rs calls it after the LSP serve loop returns - which does not happen when a client force-kills the engine, as both clients do. Every killed process therefore leaked its marker permanently. The disk cost is trivial. The real cost is that stale markers make the clients' 15-second freshness window the only thing standing between a two-month-old marker and a wrong crash diagnosis today. The sweep requires two conditions, so it can never destroy a live diagnosis: the marker is older than an hour, AND its process is gone. Age alone would delete the marker of a long-running engine that later crashes hard; liveness alone would race a client that has not yet read a fresh crash. Verified in practice - one marker survived the first pass because its pid was still alive, and was swept on the next once it had exited. last-recovery markers are deliberately excluded: they are reported once with no freshness window, so an old one still matters to a client that has not read it. sweep_orphans_in() takes the directory so the policy is testable without setting HOME, which is process-global and would race other tests in this binary. Six tests cover both refusal paths, the exclusion, and non-marker files. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013iTPP3eSpGSgWN6c5w4Fm1
Measured before: shutdown answered in 0.00s, process still alive 120s after
exit, terminated only when stdin closed. After: exits 2.05s after exit, rc=0.
I had originally written this off as an upstream limitation not worth working
around. That was wrong on the risk analysis: both clients already SIGKILL the
engine at exactly this point, so returning from main a moment after `shutdown`
is *gentler* than the status quo, not riskier. It also covers the case no
client handles - anything holding the pipe open after `exit`, where the engine
would linger holding an entire graph in memory.
Two causes, one behind the other.
tower-lsp 0.20 dispatches `exit` through service.call(), which flips the state
to Exited but does not break the read loop; the loop only notices via
poll_ready when the *next* message arrives. So `shutdown` now signals a waiter
that main races against serve().
That alone was not enough: with the select resolving, the process still hung.
tokio::io::stdin() reads on a blocking-pool thread that cannot be cancelled,
and dropping the runtime waits for blocking tasks - the very read we are trying
not to wait for. The request path now exits explicitly after clearing its crash
breadcrumb.
Also fixes the probe that produced the original diagnosis. It sent
`"params": {}` on shutdown, which tower-lsp rejects with -32602 "Unexpected
params" - a reply that looks like success to anything checking only for a
response, while the server's shutdown handler never runs. The probe now omits
params and asserts both the shutdown result and prompt termination, instead of
printing a note about a known deviation.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013iTPP3eSpGSgWN6c5w4Fm1
…ers is absent
`workspaceFolders` is optional in LSP - a client may legally send only
`rootUri`, or the deprecated `rootPath`, and several do. The engine read only
the first, so those clients got a server whose memory subsystem never
initialised: every memory command failed for the entire session while indexing
and search kept working normally. A half-broken server is worse than an
obviously broken one, because nothing points at the cause.
Measured against a cold ~/.codegraph, first-ever memoryStore:
rootUri only, before: fails, and keeps failing - "Memory manager not
initialized" on every subsequent call too
rootUri only, after: succeeds in 13.4s (cold embedding model load)
workspaceFolders: unchanged
This is what the earlier "architectural_decision is broken" report actually
was. That diagnosis was wrong twice over - not kind-specific, and not a cold
embedding model either. The real cause only became visible once memoryStore
stopped discarding its error, which is the second time that discarded error
sent an investigation down the wrong path.
An empty `workspaceFolders` list is treated as absent rather than as "no
workspace", so a client sending [] alongside a usable rootUri still works.
The selection is extracted into workspace_paths_from() so all six cases are
tested without standing up a backend.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013iTPP3eSpGSgWN6c5w4Fm1
Adds scripts/publish-release-assets.sh and restores the JetBrains engine downloader it unblocks. The script uploads what the existing cross-platform build already produces in vscode/bin/, under tag v<version> read from Cargo.toml, each asset beside a .sha256. It builds nothing and changes nothing about how binaries are made - it slots in after the manual per-host build. Staging is the default; --publish is required to upload, matching package-npm.sh. It refuses to publish a partial set. A client that resolves its own platform and finds nothing has no way to distinguish "not built yet" from "never supported", so half a release is worse than none. The downloader fetches only the platform it needs - roughly 30 MB against the 498 MB unpacked npm package that is the alternative for users without Node - and verifies every file against its published checksum before installing. An engine runs with the user's permissions; TLS says nothing about a mirror, a proxy or a truncated transfer. Two things the first attempt at this got wrong, both now covered by tests: Windows needs onnxruntime.dll alongside the executable. Fetching only the exe gives a download that succeeds and then fails at startup - package-npm.sh warns about precisely this. The sidecar is part of the install, and a checksum failure on it leaves nothing behind. The URL scheme now matches the convention already in use: the npm postinstall fetches its model from releases/download/<tag>/, and the repository name casing is load-bearing. The download is offered, never automatic. Pulling a native binary unasked on project open is not a decision the plugin should make. Verified end to end against a real release tree served over loopback: the script's checksum format is exactly what the downloader parses, and a 116 MB binary verifies against it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013iTPP3eSpGSgWN6c5w4Fm1
…, 0.20.0
Every channel used to carry all four platform binaries so that each user could
run exactly one of them. The binaries are now published once as release assets
and each channel fetches only what it needs.
npm package 88 MB compressed / 498 MB unpacked -> 16 kB / 50.7 kB
VSIX 118 MB (plus four targeted variants) -> 554 kB, one artifact
JetBrains already fetched; unchanged
mcp-package/bin/fetch-engine.js is the single implementation of the download
contract - URL layout, checksum format, the Windows sidecar rule - for both
JavaScript channels. The VS Code extension does not reimplement it: esbuild
follows the relative path and inlines the same file into out/extension.js,
verified by checking the bundle rather than assuming. The JetBrains plugin
implements the same contract in Kotlin against the same assets.
Where each channel fetches:
npm postinstall, into <pkg>/bin/ - the path is unchanged because
consumers resolve it directly, codegraph-pr.yml among them
VSIX first activation, into ~/.codegraph/bin, since a VSIX has no install
hook. server.ts now looks there too, so an engine installed through
any channel is found by all of them.
Both offer rather than assume. Pulling a native binary that runs with the
user's permissions, unasked, is not a decision these clients should make.
Escape hatches for air-gapped installs: CODEGRAPH_SKIP_BINARY_FETCH, a
pre-placed binary, codegraph.serverPath, or npx codegraph-mcp-fetch-engine to
retry. A failed fetch never fails `npm install` - rolling back a package whose
CLI, hooks and docs all work would be the wrong trade.
Version 0.20.0 rather than a patch: 0.19.1 is published, this adds a
distribution channel and changes how every install obtains its engine.
Note on coverage: vscode/src/server.ts and extension.ts changes are
compile-checked only. Those suites are quarantined because the vsforge test
harness they import is permanently lost - a pre-existing condition, not
introduced here. The download logic itself is covered by 12 checks in
mcp-package/test and 6 in the JetBrains suite.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013iTPP3eSpGSgWN6c5w4Fm1
…ns plugin defects
…graph panel defects
…test classification
publish-release-assets.sh checked that each binary existed. vscode/bin/ is a staging directory that is not cleaned between releases, so a binary from an earlier version sits there looking exactly as valid as a fresh one - and it would have been published under the new tag with perfectly correct checksums for the wrong build. Caught in practice: the 0.19.1 Windows engine was still staged while preparing 0.20.0. The first attempt scraped version strings out of the binaries and reported all three good ones as stale, because the engine does not store its version as a standalone string on every target. A check that blocks legitimate releases is worse than no check. So provenance is recorded where it is knowable instead of guessed afterwards: stamp-binary.sh records what produced each binary, on the host that could actually run --version, and publishing refuses anything not stamped for the version being released. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013iTPP3eSpGSgWN6c5w4Fm1
…ubleshooting doc, fix postinstall asset guard and CodeLens noise
656,283 mcp.crash events from ~134 machines in 30 days - 63% of all telemetry volume - traced to one argument collision. The wrapper always prepends --mcp. Every doc and example also shows --mcp, so a user who put it in their MCP client config got it twice, and clap rejects that with "the argument '--mcp' cannot be used multiple times" and exits 2. The exit happens before McpServer::run(), which is where mcp.start is emitted, so the engine never reported itself; the client respawned into the identical failure and looped. Deterministic, cross-platform, and invisible: the user just sees a tool that does not work. Reproduced against the 0.20.0 engine, and the previously-looping invocation (`codegraph-mcp --mcp`) now starts the server normally. Flags the wrapper owns are filtered out of the client's arguments rather than forwarded, so agreeing with the wrapper about the mode is harmless. Three further changes, each aimed at a way this stayed hidden: - exit 2 now prints what was rejected, including the assembled argument list, and says it is a configuration problem rather than a crash. The arguments are not visible to the user anywhere else. - A crash-loop breaker stops reporting after three identical failures in a minute, matching the VS Code extension and the JetBrains plugin. Those count in memory; this cannot, because an MCP client respawns a fresh process each time, so the count lives in ~/.codegraph/mcp-failures.json keyed by the arguments. One machine sent 504,256 events for want of this. - mcp.crash now carries the version. Only mcp.start did, and mcp.start is by definition absent for a failure before startup - so the case most needing attribution to a release was the one that could not be attributed. Also makes CODEGRAPH_SERVER_PATH work. findBinary() only ever looked in __dirname, while the postinstall told users to set that variable when a download fails or the machine is air-gapped. The advice pointed at something that did nothing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013iTPP3eSpGSgWN6c5w4Fm1
…t no longer ships Excluding `bin/**` from the VSIX removed the four bundled engines, which was the intent - but jina-code-static-256 lived in that directory too. The extension still set CODEGRAPH_STATIC_MODEL to <extensionPath>/bin/jina-code-static-256, so anyone selecting the static embedding model without naming their own path got an override pointing at nothing. Both clients now set the variable only when the user names a directory. Left unset, the engine resolves ~/.codegraph/static_models/jina-code-static-256 itself (embedding/mod.rs::default_static_model_dir) - which is exactly where the npm postinstall downloads it, and is shared across every client. Overriding a correct default with a path that does not exist is strictly worse than not overriding. The JetBrains client had the same shape and its own copy of that fallback path. The copy is deleted rather than fixed: a second statement of the same rule is free to drift from the engine's, and this is the third time in this branch that a duplicated rule has been the bug. Found by the no-mistakes document phase, which correctly declined to fix it - it is a runtime behaviour change, outside that phase's scope - and documented the behaviour instead. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013iTPP3eSpGSgWN6c5w4Fm1
…block EDT dispose
🔍 CodeGraph PR Review105 files changed (+11192/−546, 363 functions) · Risk: 🔴 high Blast radius548 direct callers affected (455 breaking) across
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Intent
The developer was driving CodeGraph 0.20.0 to release: a new JetBrains plugin plus a distribution change where all three clients (npm, VS Code, JetBrains) fetch a single per-platform engine from a GitHub release instead of bundling four binaries. Before rebuilding and publishing, they explicitly asked to fix two outstanding bugs found earlier: task #14, where adding bin/** to .vscodeignore dropped the static embedding model so CODEGRAPH_STATIC_MODEL pointed at a path that no longer ships, and task #15, an MCP crash loop where unknown or duplicated arguments made the engine exit 2 via clap before emitting telemetry, causing MCP clients to respawn forever (656k PostHog events from ~134 machines). They then instructed running the work through the no-mistakes gate and pushing to GitHub, noting the gh login had been switched to anvanster so publishing was unblocked, followed by a fresh rebuild of all four platform binaries to include the crash fixes. Standing constraints from their global instructions applied throughout: push only through the no-mistakes gate rather than directly to origin, never hand-edit auto-generated files like CHANGELOG.md, and no commits or pushes without an explicit request.
What Changed
fetch-enginedownload/verify path in the npm wrapper,engineDownload.tsin the VS Code extension, andEngineDownloader/EngineInstaller/CodeGraphServerResolverin the new JetBrains plugin, withpublish-release-assets.shandstamp-binary.shpublishing and provenance-stamping the assets and each client pinning the engine version per channel. Workspace version bumped to 0.20.0.jetbrains/) as a thin LSP client - Symbols and Memories tool windows, graph panel, code vision, status bar widget, MCP registration for AI tooling, telemetry gate/reporter, crash breadcrumbs and a restart circuit breaker - and extended the VS Code extension with an activity bar view, CodeLens, hover, onboarding walkthrough and zero-file/first-index funnel recovery.--mcpflag and the engine exited 2 through clap before reporting: the wrapper now de-duplicates its own flags, explains exit 2, and trips a persistent crash-loop breaker;CODEGRAPH_STATIC_MODELis no longer pointed at the unbundledbin/directory. Engine-side, the server now terminates on the LSPexitnotification, initializes the workspace fromrootUri/rootPathwhenworkspaceFoldersis absent, sweeps stale crash breadcrumbs, and stops discarding store errors.Risk Assessment
Testing
I exercised both bugs the release was blocked on at the surface an end user actually touches. For the MCP crash loop I staged the base-commit wrapper and the fixed wrapper over the real 0.20.0 engine and ran the exact invocation that was looping (
codegraph-mcp --mcp): the old wrapper reproduces clap's "cannot be used multiple times" and exit 2, the fixed one completes the MCP handshake and lists 42 tools, and a genuinely bad flag now prints the assembled argument list and stops reporting after the third identical failure in a minute. For the static-model bug I drovecodegraph_memory_store/codegraph_memory_searchwith--embedding-model staticboth ways: pointed at the VSIXbin/directory that.vscodeignoreremoved, the vector engine fails to initialize and both tools return "Memory manager not initialized"; unset, the engine resolves the shared~/.codegraphcopy, reports "VectorEngine ready (jina-code-static-256, 256d, static)", and the stored memory comes back from search. The targeted automated suites (wrapper-args,fetch-engine, and the newserver.spawnEnvvitest guard) all pass, and I mutation-tested the new guard by reintroducing the pre-fix default to confirm it fails, then reverted it. No screenshots: this round's changes are process-argument and spawn-environment behaviour with no rendered surface, so the reviewer-visible artifact is the CLI/JSON-RPC transcript. One gap worth knowing: the JetBrains mirror of the static-model rule reads correctly but has no runnable guard here, since its suite needs the IntelliJ SDK and network - that is the same gap already raised and closed for the VS Code side. Telemetry was disabled and a scratch HOME used throughout, so no events were sent and no state was written outside temp; the worktree is clean.Evidence: MCP crash loop: before/after transcript with the real 0.20.0 engine
client config: { "command": "codegraph-mcp", "args": ["--mcp"] } ----- BEFORE (wrapper at base commit 06bcc88) ----- error: the argument '--mcp' cannot be used multiple times Usage: codegraph-server-darwin-arm64 [OPTIONS] $? = 2 <- clap rejects the duplicate before any telemetry; the MCP client respawns into the identical failure, forever ----- AFTER (wrapper at 91cc406) ----- initialize -> {"name":"codegraph","version":"0.20.0"} tools/list -> 42 tools, e.g. codegraph_get_dependency_graph, codegraph_get_call_graph, codegraph_analyze_impact, codegraph_get_ai_context $? = 0 <- the wrapper drops the flags it owns, the engine starts and serves ----- AFTER: an argument the engine really does reject is now explained, and the loop is broken ----- attempt 3: error: unexpected argument '--not-a-real-flag' found codegraph-mcp: the engine rejected its arguments and exited 2. arguments: --mcp --not-a-real-flag This is a configuration problem, not a crash. Check the "args" in your MCP client config - the wrapper already supplies --mcp. codegraph-mcp: failed 3 times in under a minute with the same arguments. Not reporting further failures for this configuration.Evidence: Static embedding model: memory tools broken vs working
engine: codegraph-server 0.20.0 | VS Code setting codegraph.embeddingModel = "static" ----- BEFORE: CODEGRAPH_STATIC_MODEL=<extensionPath>/bin/jina-code-static-256 (that directory is gone - bin/** is excluded from the VSIX) ----- codegraph_memory_store -> Error: Failed to store memory: Other("Memory manager not initialized") codegraph_memory_search -> Error: Memory search failed: Other("Memory manager not initialized") engine log: VectorEngine initialization failed: Model("read config.json: No such file or directory (os error 2)") ----- AFTER (91cc406): CODEGRAPH_STATIC_MODEL left unset unless the user names a directory ----- codegraph_memory_store -> {"id":"571bc1c9-b488-4001-be72-2295fd0ec3ba","status":"stored", ...} codegraph_memory_search -> {"results":[{"content":"parse_config reads the JSON configuration file","kind":"convention","score":0.375554 ... engine log: VectorEngine ready (jina-code-static-256 (256d, static), 256d, static)Evidence: Mutation check: the new guard fails when the regression is reintroduced
Mutation check: reintroduce the pre-fix default (<extensionPath>/bin/jina-code-static-256) in vscode/src/server.ts ❯ src/server.spawnEnv.test.ts (5 tests | 2 failed) × does not point CODEGRAPH_STATIC_MODEL at a bundled path the VSIX no longer ships × leaves an inherited CODEGRAPH_STATIC_MODEL alone rather than clearing it AssertionError: expected '/ext/bin/jina-code-static-256' to be undefined (reverted afterwards;git status --porcelainclean, guard back to 5 passed)Pipeline
Updates from git push no-mistakes
✅ **intent** - passed
✅ No issues found.
✅ **Rebase** - passed
✅ No issues found.
🔧 **Review** - 5 issues found → auto-fixed ✅
scripts/publish-release-assets.sh:130-set -euo pipefailis active, so agrepthat finds nothing inside a command substitution aborts the script before its own diagnostic runs. Line 70 (pin="$(grep -m1 'ENGINE_VERSION = "' ... | sed ...)") and line 130 (recorded="$(grep -F " $bin" "$MANIFEST" 2>/dev/null | ... )") are both plain assignments in statement position, so a failed grep propagates through pipefail and terminates the run. The${pin:-<not found>}andNOT STAMPEDmessages, and the whole guidance block below them, are unreachable in exactly the cases they were written for. Since every channel now depends on this release existing, the operator gets a bare exit 1 instead of the instructions telling them what to stamp or rebuild. Fix:pin="$(... || true)"(or|| pin='') on both, matching the fallbacks already coded.mcp-package/test/wrapper-args.test.js:28-LOOP_STATEis computed from the realos.homedir()and the test unlinks it three times (lines 116, 129, 140) plus lets the wrapper write to it. Runningnpm testtherefore destroys any genuine crash-loop state in~/.codegraph/mcp-failures.json, and the breaker assertion silently depends on the developer's real home being writable —recordFailurereturns 1 on a write failure, so on a read-only or sandboxed HOME the "breaker engages within three identical failures" check fails for an unrelated reason. Every other resource in this test is already isolated inmkdtempSync. Fix: pointHOME(andUSERPROFILEfor Windows) at a temp dir in thespawnSyncenv and computeLOOP_STATEfrom that same dir.scripts/package-npm.sh:46- The pre-pack gate runs onlynode test/fetch-engine.test.js.package.jsonnow defines"test": "node test/fetch-engine.test.js && node test/wrapper-args.test.js", andwrapper-args.test.jsis the regression guard for the duplicated---mcpcollision that produced 656k crash events — the exact defect this release fixes. Packaging can currently succeed with that guard failing. Fix: runnpm testhere, or add the second file to the same subshell.mcp-package/bin/postinstall.js:68- Both comments state thatcodegraph-mcpresolves the engine "from its own bin directory and nowhere else", and the user-facing failure advice only names the bin path.findBinary()(bin/codegraph-mcp.js:171) now honoursCODEGRAPH_SERVER_PATH— that was made to work in this same branch. The air-gapped/offline failure messages omit the one mechanism just fixed for that case, so a user hitting the retry path is steered away from the supported option. Fix: mentionCODEGRAPH_SERVER_PATH=<engine>alongside the bin path in both messages and drop the stale "nowhere else" claim.jetbrains/src/main/kotlin/ai/codegraph/jetbrains/telemetry/TelemetryReporter.kt:164-dispose()callssender.awaitTermination(2, SECONDS)on a project-level service, which IntelliJ disposes on the EDT during project close. WithTIMEOUT_MS = 5_000on both connect and read, an in-flight POST to an unreachable PostHog endpoint blocks project close for the full 2 seconds. The comment immediately above already establishes that the thread is a daemon and cannot keep the IDE alive, which is the reason the wait is not needed. Fix:sender.shutdownNow()without the await.🔧 Fix: harden release scripts, isolate wrapper test, unblock EDT dispose
✅ Re-checked - no issues remain.
🔧 **Test** - 1 issue found → auto-fixed ✅
vscode/src/extension.ts:380- The VS Code half of the static-model fix (vscode/src/extension.ts:380 - only set CODEGRAPH_STATIC_MODEL when the user names a directory) has no executable regression guard. vscode/vitest.config.ts documents that the private @vsforge test harness the extension suite depends on is permanently lost, and vscode/package.json still declaresfile:../vsforge/packages/@vsforge/*dependencies that do not exist in the repo, sonpm installand thereforevitestcannot run here at all. The equivalent JetBrains change needs the IntelliJ SDK and network. I verified the user-visible outcome end-to-end at the engine boundary instead (both env conditions, real engine), and the change itself by reading the diff, but nothing will catch a future regression in the client-side wiring. Worth a decision on whether to rebuild a minimal vscode mock for this path.node mcp-client-sim.js <pkg> 3against the base-commit wrapper + real codegraph-server 0.20.0 - reproduced the exit-2--mcp cannot be used multiple timesrespawn loopnode mcp-client-sim.js <pkg> 1against the target-commit wrapper - MCPinitializereturns serverInfo codegraph 0.20.0, exit 0node bin/codegraph-mcp.js --mcp --bogusx4 with the real engine - exit-2 explanation, argument echo, and crash-loop breaker at the 3rd failure; inspected~/.codegraph/mcp-failures.jsonnode static-model-probe.jswithCODEGRAPH_STATIC_MODEL=<extensionPath>/bin/jina-code-static-256- engine logsVectorEngine initialization failed: read config.json: No such file or directory,codegraph_memory_searchreturnsMemory manager not initializednode static-model-probe.jswithCODEGRAPH_STATIC_MODELunset - engine resolves~/.codegraph/static_models/jina-code-static-256, logsVectorEngine ready, memory search respondsnode test/wrapper-args.test.jsin mcp-package (9 checks)node test/fetch-engine.test.jsin mcp-package (28 checks)Checked engine version pins:Cargo.toml0.20.0 vsmcp-package/bin/fetch-engine.jsandjetbrains/.../CodeGraphServerResolver.kt- all matchgit status --porcelain- worktree left clean, no transient artifacts🔧 Fix: add regression guard for static model spawn env
✅ Re-checked - no issues remain.
node mcp-package/test/wrapper-args.test.js- 9 assertions on flag de-duplication, exit-2 diagnostics and the crash-loop breakernode mcp-package/test/fetch-engine.test.js- engine asset/platform resolution for the unbundled distributionnpx vitest run src/server.spawnEnv.test.ts(invscode/) - the newengineSpawnEnvguard, 5 testsMutation check: reintroduced the pre-fix<extensionPath>/bin/jina-code-static-256default invscode/src/server.ts, confirmed 2 of the 5 guard tests fail, then reverted (worktree verified clean viagit status --porcelain)Manual end-to-end MCP repro: staged the base-commit (06bcc88) wrapper and the fixed (91cc406) wrapper side by side over the realcodegraph-server 0.20.0binary and ran the previously-looping invocationcodegraph-mcp --mcpwith a JSON-RPCinitialize+tools/liston stdinManual crash-loop breaker check: three consecutivecodegraph-mcp --not-a-real-flagspawns, observing the exit-2 explanation each time and the breaker message on the thirdManual static-model repro: ran the engine in MCP mode with--embedding-model staticand exercisedcodegraph_memory_store/codegraph_memory_search, once withCODEGRAPH_STATIC_MODELpointed at the no-longer-shipped VSIXbin/directory and once with it unsetdocs/troubleshooting.md:55- Placement judgment call left for the author: the MCP crash-loop fix introduces user-visible state and output that no user document names - the persistent failure counter at ~/.codegraph/mcp-failures.json, the "engine rejected its arguments and exited 2" message, and the new mcp.crashloop telemetry event. The wrapper-owned-flags half is now covered in mcp-package/README.md (the owner of npm wrapper usage), which should stop users hitting it at all. The remaining state file and diagnostics would fit a "MCP server keeps restarting" section in docs/troubleshooting.md, but that document is currently scoped entirely to VS Code indexing, so widening it is a scope decision rather than a stale fact.vscode/package.json:1-npm run lintin vscode/ fails outright: eslint 8.57.1 finds no configuration file, and no .eslintrc*/eslint.config.* has ever existed in the repo (confirmed at both 06bcc88 and 91cc406). This change adds or rewrites ~13 TypeScript files (engineDownload.ts, funnel.ts, codeLensProvider.ts, server.ts, extension.ts, telemetry/*), none of which have been linted. Authoring a config is a judgment call, not a mechanical fix, so it is left for the author: a flat config with @typescript-eslint (already a devDependency) would be the minimum. tsc --noEmit passes, so this is a lint-coverage gap, not a type error.crates/codegraph-go/src/visitor.rs:1-cargo fmt --all -- --checkreports drift in ~60 files across the parser crates (codegraph-go, -c, -swift, -perl, -memory, -harness and others), roughly 250 hunks. None of them are touched by this change, and every Rust file that is touched is already rustfmt-clean. Runningcargo fmt --allwould be mechanical and safe but would bury the 0.20.0 release diff in unrelated churn, so it is left as a follow-up for a standalone style commit.crates/codegraph-server/src/domain/unused_code.rs:17- clippy reports 22 pre-existing warnings in codegraph-server (the whole find_unused_code module is dead code;BODY_PREFIX_MAX_CHARSis an unused import in 13 parser crates; twomap_or/redundant-closure nits in mcp/server.rs). git blame puts all of them before the base commit, and none sit on a line this change touched, so they are reported rather than fixed here.✅ **Push** - passed
✅ No issues found.