mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-11-15 01:29:02 +08:00
9f379af79bcef8f37ae83bb0bbc15ef4697a9861
225
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
9f379af79b |
fix(node): every language and folder of an overloaded name shows in codegraph_node (#3030)
codegraph_node's multi-definition answer rendered a name's definitions in index order (getNodesByName: file_path, start_line) until its caps ran out: 16 bodies within 12,000 characters, then 20 list lines, then a bare "+N more". Forty definitions under an early-sorting folder hid the only one in another language or a late folder: on the triage fixture (40 TypeScript `num` in aaa_client/, one Python `num` in zzz_server/), zzz_server/format.py appeared nowhere. Without includeCode, a long list was cut by the output cap at "... (output truncated)". The bodies still fill in index order, so every overload that rendered before still renders. On top of them, each language with no body gets its first hand-written definition rendered, while the whole answer stays under the output cap. The list of the rest fills its 20 slots in a new definitionCapOrder (the first definition of each language, then of each top-level folder within a language, then the rest, each tier in index order) and is shown in index order; the overflow line names what it leaves out by language and folder, like "+365 more: 365 typescript in aaa_client/". The list-only answer is unchanged unless it is longer than the output cap; then it fills in the same order and names the rest the same way. Generated stubs stay behind hand-written definitions. Promoting each folder's first definition into the bodies, as first tried, cost Alamofire's `validate` three Validation.swift overloads to a Tests/ helper, and `adapt`, `urlSession`, `encode` and `cancel` one or two Source/ bodies each, so folders get early list slots only. Validation: new node-overload-spread tests (6): 5 fail on main's source (the fits-every-cap guard passes on both) and all pass with the fix. A/B of every multi-definition name, with and without includeCode, main vs fix: gin 171 names, 170 identical (`_` only names its overflow); Alamofire 706 names, 702 identical. No body that rendered before is lost: `cancel` adds the jQuery `cancel` from docs/ as a 13th body, and `request`, `urlSession` and `encode` swap one Source/ list line for the first Tests/ one and name their overflow. Alamofire's `validate`, `task` and `didCompleteTask` and gin's `reset`, `Bind` and `Render` are byte-identical. Reimplements PR #1657 by @ferrine. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
e7c9ffd068 |
fix(security): an MCP session no longer opens a sensitive folder as its default project (#3012)
The MCP server's default project comes from the client (initialize's rootUri or first workspaceFolders entry, or its roots/list answer), the launch cwd, or --path. Only an agent's projectPath argument went through the sensitive-folder guard of PR #2929: the default root was opened with no check, and opening it also started a file watcher and a catch-up sync, and from the launcher a shared daemon keyed on that folder. A session whose default was ~/.config, ~/.ssh or a system folder holding an index served the files inside to the agent. (On main a warm query pool happened to refuse such a call with isError; before it warmed, the main thread served it.) resolveServerRoot, which every default-project resolution goes through (the engine's open and its per-call retry, the initialize handshake and the launcher's daemon root), now applies the refusal a projectPath gets: on the path searched from when it exists, and on the root it lands on, by up-walk or by the sub-project down-scan. A refused root resolves to null with the reason, so the session has no default project. Nothing there is opened, watched or synced; no daemon, lock or socket is set up in the folder; initialize sends the no-default-project instructions; and a call without projectPath gets the usual success-shaped "pass projectPath" guidance, saying why. A call that names the folder keeps its PathRefusalError, and sub-projects below a refused folder aren't listed. --path is refused the same way, as `codegraph ui` refuses its root, but the server stays up with no default project instead of exiting, so the agent keeps codegraph for its other projects. A daemon started by hand for such a folder exits. The sensitive-folder half of validateProjectPath is split out as sensitiveDirectoryRefusal; validateProjectPath behaves as before. Validation on Windows 11: - 6 new tests in security.test.ts. On main's sources every entry point is red: the engine opened ~/.config from the folder, a folder inside it, a missing folder under it and a junction to it, serving a fake token and taking writer.pid; rootUri, workspaceFolders, cwd and roots/list sessions served the token; the down-scan adopted or listed sub-projects; a launch from the cwd started a daemon in the folder. The ordinary-project control passes on both. All 6 pass with the fix. - security.test.ts: 70 passed, 4 skipped (POSIX and macOS gates). 51 MCP, daemon, proxy and directory suites: 646 passed, 0 failed. - The real serve --mcp against a private HOME, from a sensitive cwd and with a sensitive --path: no-default instructions, the guidance instead of the token, no daemon.pid or writer.pid. An ordinary project is unchanged. - Cost: resolveServerRoot 141 -> 377 us per call on an ordinary project (median of 7 rounds of 2000 calls). It runs at the handshake, the engine open and the launcher, and per tool call only while a session has no default project. Follow-up to PR #2929. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
0187366697 |
fix(cli): codegraph status opens the index read-only, and says when a rebuild is running
Status opened the database read-write, so its open ran the writer's
repairs: migrations, the FTS-trigger and secondary-index recovery an
interrupted bulk load leaves behind, and WAL maintenance. During a
`codegraph index`, that recovery recreated the indexes the indexer had
dropped for its bulk load, and the indexer's next write, whose
transaction had already read, failed with SQLITE_BUSY_SNAPSHOT (517):
"Failed to index: database is locked".
Status now opens with `readOnly: true` and leaves every repair to the
writer, as the MCP reader connections already do. When another live
process holds `rebuild.pid`, it says so: a warning in the text output and
`rebuildInProgress` in `--json`, the only new key. `isRebuildInProgress`
is the check `assertNoRebuild` already made.
Added when landing it:
- A read-only open migrates nothing, so status on an index built by
1.4.0 or older (schema 7, no `unresolved_refs.status`) failed with "no
such column: status". The pending-reference count counts every row
when the column is missing, as the v8 migration marks them pending.
- While a rebuild runs, status skips the advice meant for an index at
rest: the interrupted-run references, "run sync", "re-index", and the
oversized-WAL warning, which mid-rebuild all describe the live run.
- Status no longer reclaims a killed session's oversized WAL, so the
warning names the next `codegraph sync` instead of status itself.
- Tests: the issue's exact interleaving (status inside the writer's edge
transaction, as in PR #2916's test), a pre-v8 index, and the
mid-rebuild advice.
Validation (Windows 11, Node 22.22):
- The issue's reproduction with our build of main
|
||
|
|
5e0c42cb63 |
fix(security): the sensitive-folder guard refuses links, case variants and \?\ paths (#2929)
The guard that keeps ~/.ssh, ~/.gnupg, ~/.aws, ~/.config and system folders such as /etc and C:\Windows from being opened as a project compared the path as typed. A junction or symlink to one of them, a different capitalization on Windows or macOS (~/.SSH), or a \\?\ or \\.\ spelling on Windows passed, so an MCP projectPath, `codegraph ui` and the viewer's per-file reads all served what was inside. The MCP gate also skipped the guard for a path that doesn't exist and then walked up from it, so ~/.config/no-such-dir reached an index of ~/.config. validateProjectPath now checks the path as spelled (with a Win32 namespace prefix stripped) and its fs.realpathSync.native form. Both are compared with the system set and the system folders' own real paths (macOS /private/etc), and with the home folders as spelled, under the home's real path and by their own real paths (a linked ~/.ssh). Case is folded on win32 and darwin. A path that can't be resolved keeps the lexical check. It stays at one realpath per call: the system folders' real paths are read once and the home folders' are cached for two seconds. The MCP handler also checks the root its up-walk lands on. Validation on Windows 11, with a private home holding a fake token: - Probe: on main 11 of 16 sensitive spellings were allowed, and the token was served through 6 MCP projectPath shapes and 2 viewer roots. With the fix all are refused. Controls (an ordinary folder, its upper-case and \\?\ spellings, the home itself) are still allowed. - codegraph ui end to end: main served the token for 4 of 6 spellings and started the viewer for a fifth; the fix refuses all 6 at startup. - Tests: the 9 new tests that run on Windows fail on main's source and pass with the fix. The POSIX and macOS cases are gated and were not run here. - Cost: validateProjectPath 48 -> 130 us per call, the viewer's per-file resolveProjectFile 700 -> 780 us (medians of 5 rounds, 3 runs). Reported in PR #1930 by @mixxer; this reimplements the fix independently. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
0b238452a3 |
fix(mcp): explore's maxFiles text gives the real default, not 12
codegraph_explore's maxFiles parameter said "(default: 12)" and its
schema carried default: 12, but the handler picks 4, 5 or 8 files by
project size (clamped to 1 to 20), and nothing applies 12. A client that
fills in schema defaults sent maxFiles 12 and overrode the size-based
default. The description now gives the real default and says total
output is also capped in characters by project size, and the schema
default is gone. The query description no longer tells the agent it
needs no prior codegraph_search, a tool not on the default list.
explore-output-budget's small-tier test asserted that the output does
not contain "Complete source code is included above", text the
completeness note no longer has, so it could not fail. On this fixture
the note reads "Complete source for 1 file is included above", which
the PR's replacement ("files is included above") misses too. It now
checks "is included above", and the medium-tier test checks the note is
there, so a rewording can't leave the absence check behind again. With
the small tier's completeness note switched on, main's assertion and
the PR's both still pass, and this one fails. The interface comment
names the note by its current wording.
Left out of PR #1972: its rewrite of the server instructions' "Trust
codegraph's results" line, which is agent steering and needs an A/B
first; and its codegraph_node hint and footer changes, which main
already has (PRs #2783, #2077).
Original PR #1972 by @bompus.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
de2b80e921 |
fix(mcp): a project opened for projectPath is released on its own idle deadline
The handler keeps one idle timer, armed for the oldest open project a
trim can release, and it skips a project whose catch-up is still
running. scheduleIdleRelease returned early while a timer was armed.
So when a project's catch-up settled after a newer project had armed
the timer, the trim found the older project not yet idle and kept the
newer one's deadline. The older project stayed open, writer lock
included, until then: up to nearly twice the idle timeout. With the
10-minute default, a project last used at 0:00 whose catch-up settles
at 9:50, while another was used at 8:20, went at 18:20, not 10:00.
Each trim that gets as far as scheduling now replaces the timer,
re-armed for the oldest releasable project. Trims skip scheduling
while calls are in flight, and the last call to finish trims again, so
a settled catch-up still gets its own deadline. There is still one
timer and no polling.
Validation: the new fake-timer test drives that order through
execute(). On main
|
||
|
|
ef05970bb4 |
test(daemon): the OS process lookup tests pass on a loaded Windows machine (#2842)
daemon-lock-holder.test.ts > "reads a running process from the OS" failed under load on Windows, and so did the seven CLI tests in the file that look a process up. describeProcess gives each OS query 5 s: on a busy machine PowerShell takes longer than that to start and run its CIM query, so the lookup falls back to tasklist (the image name only, or nothing once tasklist runs out too) and the command line is null. The in-process test's toMatch(/ping/) then fails, and the CLI the other tests spawn sees a Node holder it can't classify, keeps the lock, and says so. In production that fallback is the intended fail-safe: an undescribed holder stays "unknown" and its lock is kept. The per-query budget is still 5 s unless CODEGRAPH_PROCESS_LOOKUP_TIMEOUT_MS says otherwise, and nothing outside the tests sets it. The test file sets it to 60 s for itself and for every CLI it spawns (an env var reaches both), gives the lookup tests time to match (240 s per CLI run, 30 s for a killed holder to go), and adds a Windows-only test that a lookup that runs out says nothing, so the holder stays "unknown". Validation (Windows, 16 logical CPUs, a 48-thread CPU burn running): - describeProcess on a live PING.EXE: the 5 s budget answered nothing in 8 of 8 lookups (about 11 s each); a 60 s budget read the command line in 6 of 6 (median 12.6 s, max 13.6 s). - The reported test alone: main 0 of 4, each "expected '' to match /ping/i"; this branch 2 of 2, and 6 of 6 with an earlier revision that passed the same 60 s budget as an argument. - The whole file: main 3 of 11, the 8 failures being exactly the tests that look a process up; this branch 12 of 12, twice. - No load: 12 of 12 in 20 s. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
3f58e34383 |
fix(cli): index no longer blocks on a daemon lock naming a process that isn't CodeGraph (#2819)
codegraph index told you to run `codegraph daemon stop`, which the CLI
rejects ("too many arguments"), whenever it could not verify the process
named in .codegraph/daemon.pid. In that state `codegraph daemon` lists
nothing, because it lists only daemons that answer their socket. And
`codegraph unlock` kept an old-format lock whose process was alive (the
rule from issue #1834), so nothing the CLI offered got past it. The
writer-lock message gave the same wrong advice.
Root cause: a live pid in the lock was the only evidence, and a pid
proves nothing once the OS gives it to another program. Every CodeGraph
daemon since the first one (v0.9.5) runs
`<runtime> ... codegraph.js serve --mcp --path <root>`, so the holder's
command line settles the question.
Fix:
- New src/mcp/process-identity.ts reads a process's command line and
executable name from the OS: Win32_Process through PowerShell (tasklist
as fallback) on Windows, /proc on Linux, ps elsewhere. Each lookup is
capped at 5 s and never throws. A holder is a CodeGraph MCP server,
provably something else, or unknown, and unknown is never evidence.
- stopDaemonAt treats an unverified live holder that is provably not a
CodeGraph server like a dead one. Its lock is cleared, under the same
writer-slot claim, and the process is never signalled.
clearStaleDaemonArtifacts (codegraph unlock) does the same for an
old-format lock. A holder that may be a daemon keeps its lock, as
before (issues #1553 and #1834).
- daemonStopFailureMessage builds index's error from what remains: the
pid, what it runs, and a command that clears it (taskkill /PID N /F or
kill N), or which lock file to delete when only that will do.
- writerLockHeldMessage names `codegraph daemon` instead of the missing
`codegraph daemon stop`.
Validation (Windows 11):
- New daemon-lock-holder.test.ts (11 tests) and a daemon-registry test.
With main's daemon-registry.ts, writer-lock.ts and codegraph.ts put
back, 11 tests fail, including all 8 CLI cases and the writer-lock
wording; with the fix all pass. Four existing tests used generic
processes to stand in for a daemon that doesn't answer. Those stand-ins
now carry a CodeGraph server's command line, and pass on main and here.
- Real CLI with a private HOME, 4 cases: an old-format lock naming a live
PING.EXE; an old-format lock naming a live Node process that isn't
CodeGraph; a current-format lock naming that kind of Node process with
a pipe nobody listens on; an old-format lock naming a Node process whose
command line reads `codegraph.js serve --mcp`. On main all 4 index runs
exit 1 with the old advice, `daemon stop` is rejected, `daemon` lists
nothing, and unlock clears only the third. With the fix the first 3
rebuild (exit 0), each holder stays alive, and unlock clears the first
too. The 4th exits 1, naming the pid and `taskkill /PID <pid> /F`;
after that command, index exits 0.
Contributor PR #2373 (@danusha2345) changes the two strings only; I read
it for guidance.
Fixes #2770
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
63ca5681db |
fix(db): a database SQLite can't open is reported as that, not as a Node version problem (#2796)
Root cause: createDatabase() caught everything the node:sqlite connection threw and reported it as "CodeGraph requires node:sqlite (Node.js 22.5+)", though the module had loaded and SQLite itself had failed: a database another program held open (SQLITE_CANTOPEN on Windows), a folder the user can't write, an I/O error on a network share. Failures in the connection's first statements (locked, not a database, a WAL I/O error) came out raw, without the path. The MCP server then told the agent "No CodeGraph project is loaded" for a default project whose index exists, or "Tool execution failed: ... requires node:sqlite" for a projectPath one. Meanwhile a runtime that really lacks node:sqlite never got the hint: isInitialized's schema probe loaded the module outside its try, so the CLI and MCP server died with "Unhandled rejection: No such built-in module: node:sqlite". Fix: - src/db/node-sqlite.ts loads node:sqlite in one place; NodeSqliteUnavailableError carries the Node 22.5+ hint, now only for a module that cannot be loaded. The schema probe treats it as "unknown", so the command goes on to the open and reports it from inside its own catch. - src/db/open-error.ts: DatabaseOpenError names SQLite's code and the path, with a hint for BUSY, PERM, READONLY, IOERR (network share), CORRUPT, FULL, CANTOPEN (folder, permissions, another program holding the file) and NOTADB, keeping code/errcode/errstr and the cause. The Windows/WSL shared-index guidance still wins for its case. - DatabaseConnection.open/initialize wrap the connection's first statements as well as the constructor, and close the connection on failure. - MCP: the default project's open failure is reported to a tool call instead of "no project loaded", and a DatabaseOpenError or a missing node:sqlite is answered isError with the retry-once note (a malfunction, per AGENTS.md), worded as what failed rather than "an internal codegraph error". Not-indexed projects keep their success-shaped guidance. Validation: - Windows, an indexed project whose codegraph.db another process holds open with no sharing (a real SQLITE_CANTOPEN): CLI status, sync and query, an MCP default-project call and a projectPath call all said "requires node:sqlite" on main, or "No CodeGraph project is loaded" for the default project; with the fix all name SQLITE_CANTOPEN, the path and the hint. With --no-experimental-sqlite (node:sqlite unloadable), main's CLI and MCP server died with an unhandled rejection; the fix prints the Node 22.5+ explanation and the MCP call answers with it. - New __tests__/sqlite-open-error.test.ts (21 cases): main's source 18 fail / 3 pass, fix 21 pass. Real CANTOPEN (a directory, a file held exclusively), NOTADB and BUSY opens, the CLI and the MCP engine, plus each SQLite code's message. 5 mutations each failed at least one case. - The POSIX-only wsl-shared-index case for a real I/O error outside WSL now expects the named error (DatabaseOpenError, SQLITE_IOERR, the path) instead of the bare "disk I/O error"; it is skipped on Windows, so it was not run here. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
1040d09f26 |
fix(mcp): a daemon that never answers no longer stalls a new session for minutes (#2795)
Root cause: a launcher that found no answering daemon spawned one and then
polled for it 240 times, 25ms apart. A probe that connects but gets no hello
waits out the 3s hello timeout, so a process that holds the daemon's pipe
and never says hello made the poll take about 240 x 3s, plus the first
probe: about 12 minutes, with the session's tool calls held meanwhile. In
practice that holder is a daemon whose main thread is blocked: the OS still
accepts connections on its pipe, and the daemon the launcher spawns cannot
take its lock, so it exits.
Fix: the poll has a wall-clock budget instead of a probe count (the 6s it
was meant to be), and each probe's hello wait is capped by what is left of
it (pollForDaemon, connectAnyDaemonCandidate, a hello timeout on
connectWithHello). Spent, the session is served in-process exactly as when a
spawned daemon never binds: read-only while the stuck daemon still holds the
writer slot. The proxy's existing retry with backoff then reaches the daemon
again. The liveness watchdog kills a wedged daemon after its 60s window
(longer while its disk advances), and the next retry spawns the replacement
and proxies to it. The bound sits well inside that window, so a session
neither waits on the wedge nor gives up on the daemon.
Validation:
- A real daemon wedged by a preload's busy loop on Windows, then a second
session's codegraph_status:
- no watchdog on the wedged daemon: main answered after 733.2s; the fix
after 9.9s, and later calls in 0.5s while the daemon stayed wedged;
- the default 60s watchdog (it killed the wedged daemon at about 121s,
one deferral for its own off-thread writes): main answered after
125.2s; the fix after 9.9s in-process, then proxied to the replacement
daemon at 121.1s.
- New __tests__/daemon-silent-holder.test.ts (8 cases): main's source 0
pass, fix 8 pass. Fake-clock tests of the budget and the per-probe cap; a
real silent socket honouring the hello timeout; the real launcher against
a project whose pipe, lock and writer slot are held by a holder that
never says hello: first call served in-process in about 10s (main: none
in 45s); and after the holder dies, the session proxies to the daemon
that replaces it. 3 mutations each failed at least one case.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
8073637d66 |
fix(db): reopening an interrupted index no longer gets the server killed at every start (#2794)
Root cause: a run killed inside a bulk-load window (a crash, an OOM kill,
the liveness watchdog itself) leaves that window's secondary indexes
dropped, and since PR #1562 every writable open rebuilds them in
DatabaseConnection.open(), one CREATE INDEX after another on the caller's
thread. Each one scans and sorts its whole table. On a large index the sort
writes nothing to the database or its WAL, so on an MCP server the liveness
watchdog saw a silent main thread with no disk progress, and killed the
process before the index committed. Each restart died at the same index,
and the proxy's in-process fallback died the same way on the request it
re-served.
Fix: an open on a watched event loop leaves the rebuild to a worker thread
with its own connection, and the project is served once the rebuild is
done.
- DatabaseConnection.open({ deferIndexRecovery }) records the missing DDL
instead of running it; recoverIndexesOffThread() runs it on a worker
(single-flight, rejects on failure so the next call retries; SQLite's
default temp store, so the sort spills to disk; the worker collects its
garbage and exits by itself, per worker-teardown.ts).
- CodeGraph.open() (the MCP default project, the CLI) defers and awaits it.
- openSync({ deferIndexRecovery }) plus cg.recoverIndexes() for the MCP's
synchronous opens: the ToolHandler's projectPath projects and the engine's
retry path. ToolHandler.execute awaits the rebuild before serving (not
time-boxed: reads without the indexes scan whole tables), the project
lifecycle awaits it before its catch-up sync writes, and the retry path
starts the query pool after it.
- Synchronous opens that ask for nothing keep rebuilding in line, as before.
Validation:
- Reproduced on main
|
||
|
|
b2a84873ed |
fix(daemon): a long sync of another project no longer gets the shared daemon killed (#2793)
Root cause: the daemon arms its liveness watchdog with its own project's index files (codegraph.db + -wal), and since PR #1231 a silent timeout defers the kill while those files advance. But the daemon also writes every other project a session opens with projectPath: the project lifecycle takes that project's writer lock, starts its watcher and runs the catch-up sync, all storing on the main thread. A long synchronous stretch of such a sync left the daemon's own files still, so the watchdog read it as a wedge and SIGKILLed a healthy daemon mid-sync; the next session reattached the project and the cycle repeated. A proxy serving in-process, armed with no files at all, had the same blind spot for its own project. Fix: addWatchdogProgressPaths() lets a process add files to its watchdog at runtime. The project lifecycle adds a project's db and -wal the moment this process becomes the project's writer, and drops them after it closes the project, so only indexes this process writes count, never one another process owns. Each change re-arms the watchdog with a new child whose argv carries the whole list; the old child is stopped after the new one starts. Added files take effect at once (a sync may block the loop right after); dropped ones wait a turn, so a server releasing its projects on the way out re-arms once or not at all. A message over the heartbeat pipe was tried first and dropped: Node's Writable holds a second write while one is still in flight, so the -wal line of a db + -wal registration never reached the child before a block that followed in the same tick (4 of 4 runs). Validation: - New __tests__/liveness-watchdog-projects.test.ts (11 cases, real watchdog child, lowered timeout): main's source 9 fail / 2 pass (the two guards), fix 11 pass. The lifecycle case arms the watchdog for project A, attaches B through the project lifecycle and stores into B's index on the main thread for 3s: killed at ~1s on main, runs to completion with the fix. - 8 mutations, each failed at least one case: no lifecycle registration, registration before the writer-lock check, no drop on release, no re-arm on add, no re-arm on drop, old child not stopped, drops re-armed at once, adds deferred. - Real daemon on Windows (watchdog timeout 5s, daemon keyed on a 2-file project, second project excalidraw 1d4dc2c with all 687 TS files edited, attached with codegraph_status projectPath), 3 interleaved rounds: main killed 3/3 about 13s after attaching B ("Main thread unresponsive for ~5s"), leaving B with 27,698 edges and 72,534 pending refs; the fix survived 3/3 through 7.8-9.3s main-thread blocks and finished B (61,120 edges, 0 pending), and 3/3 again on the final build (9.7-9.9s blocks). Fixes #2404 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
214ef50efe |
fix(explore): names the query spells that the index never saw are said, not glossed over (#2789)
Root cause: explore answers whatever the query's words match. When a name the query spelled is not in the index at all, the answer is built from the other words and says nothing about the missing ones. In the report, the tool projects under a .NET repo's root build/ folder are skipped by default. Asking for ReleaseNotesBuilder, ChangelogParser, PersistentData and AddFeatures returned an unrelated Program.Main and a C++ Parser, and the agent read the wrong code. The miss diagnostics (issue #2026) ran only when nothing at all matched. Fix: - Query tokens written like code names (a camelCase hump, snake_case, or a qualified name) that resolve to no node at all are checked with a new bounded probe, getUnseenNames. It reports names the index has never seen: no node called that in any case, and no name, qualified name or path, signature or docstring word starting with it. - A qualified name is judged by its member. Express's app.handle is indexed as handle, so the receiver alone never makes a name missing. - Names that pass are listed in a note right under the summary line, success-shaped. The note points to codegraph status and never to Read. It names the .gitignore re-include for a default-skipped folder like build/ as the user's choice, since indexing scope is the user's call. - A single word (Program, Main, How) is never named: it may be English. - The total-miss answer gains the same note. - The seeding's query tokenizer moved into one shared function, unchanged. The skip itself is unchanged here. On main a root build/ folder is still skipped by default: PRs 2681 and 2687 re-opened build folders below src/ only. The documented opt-in, !/build/ in .gitignore, indexes it, and sync picks the opt-in up, verified on main and on 1.6.2. Validation: - New explore-unindexed-names.test.ts, 8 tests: 6 fail on main. - All 38 explore suites pass, plus the tool-allowlist and dynamic-boundary suites (466 tests). - The reporter's repo, newrelic/newrelic-dotnet-agent 418a0f2f, now commits the !/build/ workaround. With that line removed and the index synced, main answers the reporter's query with VersionFileGenerator/Program.cs Main and Sicily/Parser.cpp, saying nothing. The fix adds the note naming exactly the four missing names. - Ordinary queries: 996 on express, flask, excalidraw, django, airflow and go-control-plane, all byte-identical to main. - Miss probes: 120 queries mixing real names with invented ones, or only invented ones. Main never notes anything. The fix names every invented name in all 120 and no real name. The note costs about 5 source lines on a full response. Fixes #2462 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
1890b10cc4 |
fix(explore): a named function too long for its share renders instead of the file's tail (#2783)
Root cause: an oversize cluster keeps its first-ranked member however big it is, and the ceiling windows it (CG-30). Ties on importance went to the shorter span, so in the issue's file, one 200-line function plus the line that calls it, the one-line `path = build_report()` took that guarantee. The search matches it through its signature, so it is an entry point too. The function the query named then no longer fit beside it and was dropped, and `codegraph_explore build_report` returned the file's last six lines under the verbatim-source header. A file asked for by path did the same, because every definition in it ranks below the bare edge lines. Where a body was windowed, nothing in the block said so. The footer's advice, ask for the symbol by name, returned the same window, and it suggested `codegraph_node`, which the default MCP surface does not list. Fix, all in the explore render path (query time only): - Rule 1: among cluster members of one importance, a definition the query named ranks ahead of one its search only matched, before the shorter span. - Rule 2: a fragment does not spend the oversize guarantee. When the kept members render under MIN_CHARS, the member they priced out is kept and windowed like a first-ranked one. That member must be one the query named, or a definition in a file pinned by path with nothing named in the cluster. - Rule 3: a body the ceiling window cuts ends with the per-symbol view's note, "... lines X-Y of `name` elided - codegraph_explore `file:X-Y` returns them" (one shared helper now). Each note is paid for by its own part's last lines, never a focus line, the member's first line or a member the part shows whole. So no section grows and no other file's budget moves. - Rule 4: the trim footer and the dynamic-boundary note name `codegraph_node` only when it is on the served tool surface. Validation: - New explore-queried-oversize-symbol.test.ts, 9 tests: 6 fail on main. Reverting any one rule fails that rule's own tests (2, 2, 3 and 1). - All 37 explore suites pass (414 tests). - Deterministic A/B: one index per repo, a fresh handler per query, five arms (main, then each rule added in turn). 820 queries per arm: the 50 longest functions by name, their files by path, caller/callee pairs, call chains, random names and flow questions. - The queried function's head (def plus 10 lines) renders, main -> fix: express 50 -> 50, flask 49 -> 50, excalidraw 33 -> 48, django 43 -> 47, airflow 43 -> 48, of 50 each. - Queried body lines rendered, main -> fix: excalidraw 6,615 -> 9,391, django 5,337 -> 5,662, airflow 6,320 -> 6,749. - Rule 1 changed 32 responses, each hand-triaged as the queried function now rendering. Only 4 of the 504 ordinary flow queries are among them. - Rule 2 changed none. - Rule 3 wrote 838 notes, all checked against source and index, at 2.8 to 3.9 source lines each. Bytes a note frees flow to lower files by the existing carry-forward. - Rule 4's 236 diffs are exactly the removed mention. - No response exceeds the 25,000-char inline cap. Fixes #2486 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
bbf987e13b |
fix(explore): the named symbols take turns in the dynamic-dispatch links list (#2751)
Links of the same rank were listed in edge order, so one named symbol's long fan-out could fill the 6-line list before another named symbol's first link. prometheus's `refresh.Discovery.refresh` hands its func field to 28 discovery packages, and in `refresh.Discovery.refresh hetzner.refresher.refresh hcloudDiscovery.refresh` those links filled the list before `refresher.refresh` → `robotDiscovery.refresh`. Each link now has a turn: how many links of its rank the named symbol it hangs from already has. Links sort by rank as before, then by turn, then by edge order, so links of the same rank alternate between the named symbols. A token that names two interfaces gets both: prometheus's `Appender.Append` matches storage.Appender and chunkenc.Appender, and storage's implementations no longer wait behind six of chunkenc's. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
f5dfa21fef |
fix(explore): a symbol written with its package resolves, like refresh.Discovery.refresh (#2750)
Explore's token resolution, codegraph_node's lookup and the callers, callees and impact lookups dropped every Go symbol written the way Go code names it: `refresh.Discovery.refresh`, `storage.Queryable.Querier` and `gin.Context.Next` resolved to nothing, and so did `binding.Default`. Three gaps: - A qualified name was matched against the qualifiedName, which holds the type and not the package, or entirely against the path, which holds the package and not the type, never as a package followed by a type. A new matcher stage reads `pkg.Type.method` as leading qualifiers that name the package by path segment and trailing ones that end the node's scope. A Python `pkg.mod.Class.method` and a Rust `module::Type::method` read the same way. - A module's root package has no directory: gin's `context.go` is `package gin`. A Go package qualifier now also matches the package the file declares, read from its `package` clause (src/graph/go-package.ts). That covers versioned `v2/` directories and generated packages like prometheus's `io_prometheus_client` as well. The clause is read last and only where it can decide, a top-level function or a node whose scope already ends with the written type, so a common name's hundreds of definitions cost no file reads. - Explore and codegraph_node took a qualified token's matches from the full-text search's top 50 only, and for gin's `binding.Default` those were three `binding/default_validator*.go` files. Through one shared helper they now fall back to the exact-name index when the search has no match; the CLI's lookup already started there. The new stage runs only after the existing ones fail, and the search keeps its order, so a name that resolved before keeps its result. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
3cfd24a46d |
fix(explore): every dynamic-dispatch link gets its own line, the ones the query names first (#2748)
codegraph_explore's "Dynamic-dispatch links among your symbols" section deduped its lines by `source name > target name`. Every interface → implementation link of one method reads the same way (`TrySet → TrySet`), so an interface method with several implementations printed one line, and which implementation it kept was edge order, not the query. On gin, `tryToSetValue setter.TrySet multipartRequest.TrySet setByForm` printed only `TrySet → TrySet @binding/header.go:35` and hid formSource's link. Lines are now deduped by the edge's endpoints (node ids), and the 6-line cap stays. When more links exist than fit: - a link that reaches a test or example file through an end the query didn't name goes last, as explore's answer leaves those files out; the predicate is explore's own `isLowValue`, moved to module scope so both uses share it; - otherwise links go by how pointedly the query named their two ends. A symbol a token picks out (at most three definitions, the same notion as the boundary scan's agent-specific symbols) or a named non-callable endpoint weighs 3, a candidate of an ambiguous token 1, an unnamed symbol 0. An implementation the query names always comes before one it doesn't. The weights also keep an ambiguous token's candidates from crowding out a precise one: prometheus's `Discovery.refresh` matches a `Discovery` in each discovery package, and in `Discovery.refresh fetchApps Decoder.Decode` their links to each other filled the cap and hid the one link into `fetchApps`; - edge order breaks ties. The section takes the rendered Flow chain itself. It leaves out an edge between two of the chain's nodes, as before, and also a line that reads the same as one of the chain's synthesized hops or as a line already listed. gin declares its `Binding` and `StructValidator` interfaces once per build tag (binding.go, binding_nomsgpack.go), so the twin declaration's link printed exactly like the Flow's hop. A same-name link names each end by its owner, `setter.TrySet → formSource.TrySet`, the way an agent writes it in a query. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
a944ec3579 |
fix(go): a struct links the types its fields are declared with (#2655)
A Go struct referenced nothing through its fields: etcd's `type
PutResponse struct { Header *ResponseHeader }` or gin's `type Context
struct { handlers HandlersChain }` had no `references` edge to those
types, so impact on `ResponseHeader` or `HandlersChain` missed every
struct holding one, and their users. Main had 0 such edges on gin, etcd,
prometheus and kubernetes; only embedded fields were read (`extends`).
TS extractor and Rust kernel, mirrored:
- goStructFieldTypeNames / struct_field_type_names walk each named
field's type and push a `references` ref per type name from the
struct, on the name. Fields mint no nodes: impact never climbs
`contains` upward (#536), so a field node would stop impact short of
the struct; Dart attributes field types to the class too (#2327).
- #2441's alias walk is factored into collectGoTypeNames /
collect_type_names + goDeclaredNames / declared_names, so struct
fields skip what aliases skip: predeclared types and, written bare,
the declaration's own type parameters and its own name (no self-edge,
no failed row for `next *node`).
- An embedded field stays `extends`; an embedded generic's type
arguments are references (client-go's
`listers.ResourceIndexer[*v1.Pod]`: 651 such fields in kubernetes).
Resolver: gateTargetKind's unknown-qualifier rule (#2417) now covers
every Go type declaration (GO_TYPE_KINDS). Without it, a field
`op clientv3.Op` under an unaliased go.etcd.io/etcd/client/v3 import
bound to the same file's own `Op`.
Explore: a `references` edge whose ends are both type declarations no
longer makes either end connected to an entry. With the new edges alone
the #1500 CG-10 gate failed: payroll's 8-line `RunResult` (holding a
`[]Payslip`) became connected and displaced the 51-line
runPayrollCycleAll body. The rule alone changes no rendered symbol on
gin, etcd, prometheus, kubernetes, bloc, bulletproof or refine-examples.
Validation (main
|
||
|
|
5da4a41d92 |
fix(explore): tests and examples the engine knows stay out of explore's answer (#2557)
codegraph_explore's low-value check (the hard filter, the rank penalty and the comparator key) and the dominant-file, route-file and routing-manifest filter in db/queries.ts each kept a regex list of their own. They missed every test convention isTestFile learned since: C++ `foo_test.cc` and `FooTest.cpp`, Google's `foo_unittest.cc`, LLVM's `unittests/`, `testing/`, `e2e/`, `__mocks__/`, Gradle `foo-test/` modules, Kotlin source sets, and the examples, samples, fixtures and benchmarks that explore's gather already dampens through isTestFile. Both now build on isTestFile, keeping icons/i18n (explore) and generated files (queries), plus the singular `__test__/` the old lists matched. Dropping their lowercase suffix patterns also ends false positives such as `Latest.java` and `contest.cs`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
cd97bb37d2 |
fix(context): call paths label synthesized hops like explore, not as events (#2428)
Two surfaces label a synthesized (dynamic-dispatch) `calls` hop:
codegraph_explore's Flow and dynamic-dispatch links plus the
codegraph_node trail (`dynamic: interface → impl @file:line`), and
ContextBuilder's "## Call paths", which `codegraph context` prints
(`→[…]`). Each kept its own list. The call paths' list stopped at
callback, react-render, jsx-render, vue-handler, http-client, queue-job
and socket messages, so every other synthesizer fell through to
`event ${m.event ?? ''}`: an interface-impl hop read
`→[event @src/pdf-exporter.ts:5]`, and so did a C++ override, a redux
thunk, a gin middleware chain or a closure collection.
The wording now lives once, in src/graph/synthesized-hop.ts
(`describeSynthesizedHop`: a summary, the detail only the call paths
print, and the wiring site), as AGENTS.md asks of a derivation two
surfaces render. ToolHandler renders `dynamic: <summary> @site`, the
call paths `<summary><detail> @site`. Explore and the node trail print
exactly what they printed before; the call paths keep their wording for
the kinds they already labelled, including the queue name, a socket's
direction and the HTTP clause, and now name the rest the way explore
does. A bus event reads `bus event` there too, and the section's
footnote no longer says hops are marked `[callback/event …]`.
The helper also holds the wording #2414 gives a Go method a struct gets
by embedding (`metadata.promotedInto`), so that arm moves here too. The
unused long `label` form of ToolHandler.synthEdgeNote (last read by the
removed codegraph_trace) is dropped.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
855eca2698 |
fix(go): a call through an interface reaches a method the struct gets by embedding (#2414)
Since goImplementsEdges counts promoted methods, a struct can satisfy an interface with methods its embedded types declare. The interface-dispatch bridge linked each interface method only to a method the struct declares itself, so a call through such a method reached nothing when the embedded type does not implement the interface on its own: prometheus's scraper.Report never reached Target.Report through targetScraper. For a Go struct's synthesized implements edge, each interface method the struct does not declare now links to the method Go's selector picks: the shallowest embedded type that has it, nothing when two occurrences tie at that depth (one type reached along two paths included), and nothing when an embedded interface provides it, which is a dynamic call again. These links come after the per-implementer loop, from what the struct's cap has left, so every existing edge is unchanged. They keep interface-impl metadata plus promotedInto, and registeredAt names the struct's embedding; codegraph_explore labels the hop as a promoted method. The embedding test is shared with goImplementsEdges. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
1b56cb1992 |
fix(explore): find COBOL copybooks by name, with every include site (#2342) (#2358)
`codegraph explore MYCOPYBOOK`, the `codegraph_explore` MCP tool and `codegraph context` answered "No relevant code found" for a COBOL copybook named in the query, while `query`/`node` found its include at once. Cause: the COBOL extractor records `COPY X` and `EXEC SQL INCLUDE X` as `import` nodes, and findRelevantContext filters every channel by HIGH_VALUE_NODE_KINDS, which leaves imports out on purpose (a JS/TS/Python import statement is noise in explore). With the include out of reach the text search fell through to its fuzzy fallback, so a member with a digit in it (never extracted as a symbol) came back as an unrelated program one edit away: CVACT01Y -> CBACT01C, CVCUS01Y -> CBCUS01C on CardDemo. Making COBOL imports "high value" alone would not have helped either: explore's renderer skips import nodes, and the import-to-definition step drops a COBOL include because its `imports` edge leaves the enclosing scope, not the import node. Change: - src/graph/cobol-copybooks.ts (new, shared derivation): resolves the COBOL words of a query to copybooks - every COPY / EXEC SQL INCLUDE of the member (case-insensitive) and the indexed copybook file (.cpy, or a file an include resolved to; a same-named program is not one). Gated on the project having COBOL files at all (one seek on idx_files_language), so no other language's query does extra work or changes. Plain lowercase words in a multi-word query are English, not member names. - findRelevantContext: a named copybook's top-level declarations, then its include statements, become entry points ahead of the kind-filtered channels, like an exact filename (at most half the slots when the query matched other things, include sites in those files first). They are not walked (BFS from a record layout spent the answer on whichever program MOVEs into it), the member is not re-searched as text (the fuzzy decoy), and an explicit nodeKinds filter without `import` is honored. - codegraph_explore: the copybook file is pinned like a path in the query; an include statement that is an entry point is grouped and rendered (a window around the COPY line) instead of skipped; a new "COBOL copybook" section lists every include site (path:line, COPY vs EXEC SQL INCLUDE, 25 lines then a count) or says the member has no indexed source (DB2 DCLGEN, compiler-supplied such as SQLCA/DFHAID). - QueryBuilder: three indexed lookups; CodeGraph.findNamedCopybooks exposes the derivation so explore and the context builder share one answer. No extraction change; no re-index needed. Verification: __tests__/explore-cobol-copybook.test.ts (8 tests, through ToolHandler codegraph_explore and buildContext): with src/ reverted to origin/main 6 fail (the 2 guard cases pass on both), 8/8 pass with the fix. tsc clean, npm run build OK. Issue repro fixed on the built CLI (explore + context) and through a real `serve --mcp` tools/call. Full suite on a heavily loaded box: 5716 passed, 25 failed in 15 files; 12 of those files pass alone, and the other 3 (function-ref, git-index-currency, worktree-detection) are 5 s / 60 s timeouts that fail on clean main as well in interleaved runs. Validation (both builds on one index per repo; query-side change only): - expressjs/express: 14 explore + 4 context queries byte-identical. - aws-mainframe-modernization-carddemo: 10 non-copybook queries byte-identical; 15 copybook queries all now return the copybook (when indexed) and every include site - before: 9 "No relevant code found", 4 unrelated programs, 2 without the copybook. - cicsdev/cics-genapp: 6 non-copybook queries byte-identical; 6 copybook queries fixed (LGCMAREA's 25 sites incl. multi-line EXEC SQL INCLUDE, lowercase lgpolicy, SSMAP with no indexed source). - In-process explore latency on express unchanged within noise. Thanks @popolusiak for the report. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
dea076fd1e |
fix(mcp,extraction): exit a daemon whose install changed; never store a grammar-load failure (#2335, #2336) (#2346)
* fix(mcp,extraction): a daemon whose install changed exits; grammar-load failures are never stored (#2335, #2336) A daemon that outlived an upgrade loaded grammars lazily from an install that no longer existed, and stored every file its watcher re-indexed as a zero-node row under the file's new content hash: no hash-based sync ever revisited those rows, and `codegraph status` called the index up to date. - The daemon checks its own package.json every 30 s (CODEGRAPH_DAEMON_INSTALL_CHECK_MS, 0 turns it off) and exits once the file is gone or carries another version, so the next session starts a daemon from the current install. - A result whose grammar failed to load (`parser_error`) is never stored: the file keeps its previous data and the next sync or index retries it, and a row an older engine stored that way is re-indexed even though its hash matches. The tag-based CFML path reports a missing grammar with the same code. - A launcher whose own install was deleted no longer crashes on the spawn error of the daemon it tried to start. - `codegraph status` and `status --json` (filesNeedingReindex, filesWithParseErrors) report indexed files whose symbols are missing although their content is current (#2336), and `files --json` carries each file's recorded errors. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(mcp): a session whose own install changed never claims the writer lock (#2335) A daemon now exits once its install is upgraded or removed, and nobody holds the writer slot for a successor at that point. Its sessions run the same replaced code and serve themselves in-process the moment it goes; one that found the slot free claimed it as its writer even though its engine could not load (a real npm upgrade: "Cannot find module './sqlite-adapter'"), and so kept a daemon from the current install from starting. Such a session now falls back read-only and says to restart it. Also adds the CHANGELOG entries for the install check and grammar-load failures (#2335) and for the status report (#2336). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
511d86e94d |
fix(mcp): replace a daemon left running by an older install (#2335) (#2343)
A daemon keeps running the code it started with. The 1.6.1 daemon in #2335 outlived an upgrade, kept loading grammars from the install the upgrade removed and wrote every file it re-indexed as empty, while every session from the new install served itself in-process, read-only, because the old daemon held the writer lock. Daemons from before this change cannot notice an upgrade, so the launcher acts: - A launcher that finds a daemon of an older plain X.Y.Z release stops it the way `codegraph daemon` does (daemon.pid and the socket hello must agree; SIGTERM, SIGKILL only if it still answers) and starts one from its own install. It finds the daemon by its hello on the launcher's own socket, or by the project lock when it listens elsewhere: on Windows, daemons before #2278 named their pipe after the root as typed. A daemon of the same, a newer, a prerelease or an unknown version is never touched, so two installs cannot take turns stopping each other's. - The writer slot never falls free on the way. The launcher swaps writer.pid to itself (mode `handover`) before the signal, clears the old daemon's leftovers while it holds the slot, and the daemon it spawns takes the slot over (CODEGRAPH_DAEMON_HANDOVER). Sessions of the old daemon that fall back in-process find the slot held and serve reads only, instead of claiming it with the removed install's code. A daemon that does not exit gets the slot back, and a successor outwaits a racing candidate that took daemon.pid first. Validated on Windows with real npm upgrades from the published 1.6.1 and 1.6.2 to this build packed as 1.6.3, with the old session calling throughout. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
dbb3d63cb1 |
fix(mcp): wait for a stopping daemon, and for the proxy's fallback engine (#2311) (#2316)
* fix(mcp): wait for a stopping daemon, and for the proxy's fallback engine (#2311) Now that QueryPool.destroy() waits up to 15 s for a query worker still starting up (#2307), two callers gave up too early: - stopDaemonAt() gave a daemon 3 s after SIGTERM, then re-probed its identity and reported it still running once the daemon had closed its socket, so a daemon waiting on a starting worker made `codegraph index` abort. A daemon that has closed its socket but still holds its lock is now given 17 s more to exit; nothing is signalled during that wait. - The proxy's in-process fallback called engine.stop() and exited on the next line. It now exits once its engine, and any engine still being retired after a handback to the daemon, has stopped, with a 20 s backstop. Once shutdown starts it ignores client input and starts no new engine. Extracted from bompus/codegraph@cc87ec34da, a fork merge commit that cannot be cherry-picked whole; only these four files are taken, unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(mcp): a stopping daemon is waited for whether it closed its socket or released its lock (#2311) Builds on the previous commit: - stopDaemonAt: one rule after the first 3 s. If the daemon's identity can be re-proven (same lock, socket answers), escalate to SIGKILL as before. Otherwise, whether it has stopped answering its socket or already let go of its lock, it is partway through its own shutdown: signal nothing more and wait, bounded, for the PID to exit. The previous commit reported a daemon that had released its lock but not yet exited as still running, a race at the end of every slow shutdown. The cleanup that follows compares the lock with the one signalled, so a successor's is kept. On Windows, where SIGTERM is TerminateProcess, the same wait covers a termination still settling. - The grace is WORKER_START_SETTLE_MS + 2 s and the proxy's backstop WORKER_START_SETTLE_MS + 5 s, so they move with the settle wait they cover. stopDaemonAt takes `shutdownGraceMs` so a test can bound it. - The proxy says so on stderr when its backstop fires. Tests: the daemon fixture's setup and cleanup are shared, and the fixture process is always killed on cleanup (a failing run used to leak it). New: a daemon that released its lock mid-shutdown is waited for; a daemon that never finishes stopping is still reported after the grace and never SIGKILLed; the proxy waits for an engine being retired at shutdown; and client input arriving once shutdown has begun starts no engine. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Aaron Queen <bompus@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
70fe0ecc52 |
fix(windows): helper threads never end mid-start or with GC marking in flight (#2309)
On Windows a worker thread that ends - terminate(), its own process.exit(), or the process exiting - while V8's concurrent marker is marking its heap crashes the whole process (exit 3221225477 / 0xC0000005, no error, no dump). Bisected on the Windows 11 ARM64 VM: --no-concurrent-marking alone stops it (18/480 -> 0/480; no other concurrency flag does), but it costs about a third of indexing speed, so CodeGraph avoids the dangerous moments instead: - src/worker-teardown.ts: owners terminate a worker only once it has started (its first message, posted after its modules load - mid-load is by far the worst moment), and a worker about to exit runs a full collection first, which finishes any marking in flight (gc reached at runtime, never global). - Resolver pool / store writer: the worker collects before exiting on 'close' (the normal path - main 21/480 crashed on the VM, now 2/1200), and the 5 s close fallback never terminates a worker still starting. - codegraph_status change counts: the worker posts 'loaded' after its heavy require and collects before answering; a deadline answers unknown at once but terminates only once loaded (3/720 -> 0/1440), and MCPEngine.stop() waits for measurements in flight. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
3d86bcc037 |
fix(windows): the query pool never terminates a worker still starting up (#2307)
Terminating a worker thread while it is still loading its modules can crash the whole process on Windows (exit 3221225477 / 0xC0000005, no error, no dump). QueryPool.destroy() terminated every worker at once, and the daemon destroys its pool whenever it stops - a session that ends within a few seconds of starting had its eager worker in exactly that state. MCPEngine.stop() didn't even wait for the pool before the daemon called process.exit(). - QueryPool tracks each worker's start (settled on 'ready', error or exit); destroy() answers outstanding calls first, then terminates each worker once it has started, capped at 15 s - the parse pool's #2301 mechanism. - MCPEngine.stop() awaits the pool. - #2301's explanation is corrected: on the Windows VM, terminating workers mid grammar compile never crashed (0 in 2,161), but terminating them while they load CodeGraph's extraction modules did. The parse pool's wait already covers that window, so only its comment and changelog wording change. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
e7389b22fb |
fix(mcp): a session serving in-process hands the project back to the shared daemon (#2277) (#2293)
* 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> |
||
|
|
45a680df07 |
fix(mcp): one daemon per project however its root is cased on Windows (#2278) (#2285)
* fix(mcp): canonicalize the daemon rendezvous root on Windows
A project spelled d:\work\codegraph or D:\work\codegraph is one directory, but the daemon socket/pipe name and the registry record name hashed a raw path.resolve, which preserves whichever casing the caller passed. A proxy that computed the other casing probed a socket nobody bound, spawned a redundant daemon, and that daemon exited on the lock the running one held, so the session silently served the graph in-process: no shared watcher, no auto-sync, one extra copy of the index per open window.
Both names now come from one shared canonicalProjectRoot (native realpath, lowercased on win32), so every spelling meets the same daemon and codegraph list / stop --all see one record per project.
(cherry picked from commit
|
||
|
|
455175da67 |
fix(mcp): a projectPath in a nested repo the ancestor index leaves out isn't answered from it (#2110) (#2287)
* fix(mcp): a projectPath in a nested repo the ancestor index leaves out isn't answered from it (#2110) An explicit projectPath resolves to the nearest .codegraph/ walking up, and that walk crosses git boundaries. Inside an independent repository nested under an indexed parent that excludes it (typically gitignored), every call silently answered from the parent's code. After resolving the index root, ToolHandler now asks whether the path sits in a different git repository strictly below that root (git toplevel + common dir, memoized per path and root) and, if so, whether the index holds any file under it (one primary-key range probe per call). None means the success-shaped "isn't indexed" guidance. Nested repos the parent does index (submodules, gitlinks, embedded clones), ordinary subdirectories, linked worktrees of the same repository (the when git can't run, behavior is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(changelog): nested repositories the index leaves out (#2110) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
fdc7754a72 |
fix(mcp): return status and stale refusals as text only (#2088) (#2284)
* fix(mcp): return status and stale refusals as text only (#2088) Claude Code shows the model a tool result's structuredContent in place of its content text, under every protocol version. codegraph_status and explore's stale-index refusal both set structuredContent.freshness, so the model saw a JSON object instead of the refusal, its retry guidance or the status report. The text already carries every field, and nothing in the codebase reads structuredContent, so both results drop it. The tests now assert that the field is absent and that the text keeps the refusal line and the stale file list. (cherry picked from commit |
||
|
|
637fb0fac8 |
fix(mcp): release an idle explicit project and its writer lock (#2087) (#2283)
* fix(mcp): release an idle explicit project and its writer lock (#2087)
A daemon that answered a `projectPath` query for another indexed project
opened it in-process and, with no owner there, took its writer lock. The
project cache only evicted over its LRU bound or on shutdown, so the lock
stayed held for the daemon's whole life: the project's own daemon could not
become its writer and `codegraph index` there was refused.
Release a cached explicit project once it has gone unused for 10 minutes
(CODEGRAPH_PROJECT_IDLE_TIMEOUT_MS; 0 never releases), through the same
trim path as LRU eviction, so an active call or a running catch-up still
defers it. The next call reopens the project and catches it up (#1835).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit
|
||
|
|
49248e25eb |
fix(mcp): answer server/discover locally in the stdio proxy (#2084) (#2282)
* fix(mcp): answer server/discover locally in the stdio proxy (#2084)
Clients that probe `server/discover` before the legacy `initialize`
(Antigravity 2.5) wait for its answer. The proxy forwarded it to the shared
daemon, so no reply was written until the daemon connected: a client that
closed stdin first got nothing, and with the daemon unavailable the reply
came ~6s later as -32603 "daemon unavailable". Answer it in the proxy with
-32601 Method not found, which is what sends such a client on to
`initialize`. The direct and daemon sessions already answer unknown methods
with -32601; the new test pins that too.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit
|
||
|
|
04ec5f554d |
fix(extraction): never read an oversize file; an MPEG-TS video named .ts is not TypeScript (#1910) (#2082)
* fix(extraction): an MPEG-TS video named .ts is not TypeScript (#1910) `.ts` mapped to TypeScript by extension alone, so MPEG transport-stream fixtures (`testdata/*.ts`) were parsed by tree-sitter: ~28 s of CPU for a 900 KB clip, minutes for a directory of them, for zero symbols. Recognise the stream from the head of the file — the 0x47 sync byte at offsets 0, 188, 376 and 564 plus a NUL byte, which every stream carries in its first packets and UTF-8 source never does — and drop it at discovery: not indexed, not parsed, not counted, not tallied as an unsupported language. The check reads under 1 KB and only for `.ts` files; the batch reader sniffs the bytes it already read, and the single-file path (sync, watcher) does the same head read, so a clip handed in by name is skipped too. The skip happens before parse dispatch, so no kernel mirror is needed. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(extraction): a video .ts is never pending on the git path or a scoped sync Review found two places that decide "is this a source file" without the MPEG-TS check the scan applies: the git fast path's candidate loop in getChangedFiles and the scoped-sync path filter (watcher events). An untracked clip in a git repo was reported as `added`, skipped by sync without a record, and reported as `added` again on every status; a tracked .ts that became a clip stayed `modified` with its stale nodes. Both now treat the clip as non-source: removed when tracked, ignored otherwise. Regression tests fail without this change and pass with it, on both arms. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(extraction): require a binary head before calling a .ts file video (#1910) Review on #1915: the sniff took four aligned 0x47 sync bytes plus any NUL as proof of a transport stream, so TypeScript with a `G` at four 188-byte strides and one raw NUL in a comment was dropped unindexed. The sniff now needs the sync byte on sixteen consecutive packets (3 KB of head) and at least 1/64 of the head to be control bytes. Compressed payload carries 8-20% of them (checked on ffmpeg-made H.264/AAC, MPEG-2 and MP2 streams); source text carries none. A shorter clip is cheap to parse anyway. Also drops the benchmark figures from the changelog entry. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(extraction): a file over the size limit is never read (#1910) The size gate ran after `readFile`, so a committed video or blob fixture was decoded in full — 3.4 GB of RSS for a 400 MB file, `Invalid string length` past ~512 MB — only to be stored as skipped. It was then read again by every pass that scans files by name: the framework detectors' `readFile` and the resolver's cached reader, three full passes on the reporter's fixtures. Stat first, everywhere a source file is read by path: the batch reader and `extractFile` store an oversize file with a size stamp in place of its content, change detection hashes the same stamp (so a same-size rewrite of a file nothing is indexed from is not a change, while crossing the limit in either direction is), and both by-name readers return null above the limit. The limit moves to `src/file-limits.ts` so the three sites share one number. strace on a sparse 400 MB `.ts`: 153 603 reads before, 0 after; indexing it next to a real file costs no measurable RSS. Stacked on #1914, which shares the batch-reader hunk. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * refactor(extraction): one-purpose size-stamp reader, comments in operation order `readSourceOrStamp` took a `stats` argument no caller passed and returned stats no caller read; it now returns the text or stamp change detection hashes. The batch reader's two comments are back in the order its code runs: the size gate, then the MPEG-TS head check. No behaviour change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(extraction): bound the read itself, not only the stat before it (#1910) A file can grow between the stat that gates the size limit and the read that follows (a log, a download, a build output being written). Every source read now goes through readBoundedSource/readBoundedSourceSync: the open descriptor is re-checked, and the read stops one byte past the limit, so an oversize file is still stamped rather than decoded. The extraction batch reader, single-file extraction, framework detectors and the resolver's file cache all use it. Ported from the maintainer's hardening of this fix in #1919. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(extraction): bound the parse-retry reads too (#1910) The retry passes for files whose worker crashed or timed out re-read the source with an unbounded fsp.readFile. The file can have grown since the first, bounded read; an oversize file is now skipped by the retry instead of being decoded. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(extraction): check an oversize file against its size stamp everywhere (#1910) Review on #1915: - The viewer (readFileShape, hasDriftedOnDisk, the source endpoint) and MCP's drift gate hashed a file's real bytes, but the index stores the size stamp for a file over the limit, so every unchanged 1-8 MB file read as "changed on disk". oversizeStamp moves to file-limits.ts with an indexedHashInput helper, and all four checks hash what the index stored. An oversize file is now compared without being read. - git-index-currency's "keeps a committed path pending when sync cannot read it" injected its failure only into readFileSync, which the bounded reader no longer calls, so it failed. The failure now reaches openSync too. - The changelog entry drops its benchmark figures. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(mcp): check an oversize answer file against its size stamp (#1910) #2043's answer-freshness check hashes each contributing file whole and compares it with the stored contentHash. For a file over the index's size limit the stored hash is the size stamp, so an unchanged oversize file would read as stale. Compare it by its stamp, without reading it, like the viewer and the MCP drift gate do. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * perf(extraction): judge a .ts clip from the bytes it is read for, not at discovery (#1910) Discovery sniffed the head of every `.ts` file on every full scan — an open + 3 KB read + close per file, before anything asked for its content. On a 10k-file TypeScript tree that took a warm-cache scan from ~105 ms to ~243 ms; on a slow disk or a network share it is a random read per file per scan, the access pattern #1231 and the SMB reports (#1014, #448) are about. The bytes are now judged where they are read anyway, so the check costs no I/O and runs only for a file that is new or changed: the batch reader (as before), `indexFile`, the sync reconcile (an untracked clip is skipped, a tracked file that became one is removed through the same removal path as a deletion), and both `getChangedFiles` paths. The scan lists `.ts` files by name again; a test pins that discovery opens none of them on the walk or the git path. Scan time is back to main's (~98 ms), and a full index is unchanged within noise. Also collapses the four #1910 changelog bullets (#1914 and #1915 each added both) to two, without the benchmark figures. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: danusha2345 <danusha2345@users.noreply.github.com> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> Co-authored-by: danusha2345 <ewidusoc498@gmail.com> |
||
|
|
e5a3d85b0c |
fix(watcher): follow a rebuilt index instead of syncing into the replaced file (#1902) (#1917)
* fix(watcher): follow a rebuilt index instead of syncing into the replaced file (#1902) `codegraph index` rebuilds through `CodeGraph.recreate()`, which unlinks `.codegraph/codegraph.db` and creates a new file at the same path. A running MCP daemon kept its handle on the unlinked inode and its watcher kept "auto-syncing" into it, so no edit reached the on-disk index. The #925 self-heal only ran on the tool-call path and ran no catch-up after reopening. - `sync()` now checks, once it holds the index mutex and the write lock, whether the database was replaced on disk (one stat, the same inode check `reopenIfReplaced` uses). If so it reopens the live file in place and widens a scoped sync to a full reconcile. A failed reopen (rebuild mid-way) returns the lock-busy shape so the watcher keeps its pending files and retries. - `reopenIfReplaced()` refuses while an index/sync holds the mutex, so the tool-call path never closes the handle an in-flight sync writes through. - When a tool call's `freshen()` does reopen, the engine runs its existing catch-up sync (serialized on the same mutex), but only for its own watched instance, so an engine that is not the project's writer never writes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(sync): step aside for a rebuild that has recreated the file but not locked it yet `codegraph index` recreates the database, then takes the write lock inside indexAll. With the watcher now following a replaced file, a sync landing in that gap reopened the empty file and started a full reconcile, holding the lock the rebuild was about to request — `codegraph index` could then fail with "another process may be indexing". Review flagged the gap; this closes it. After following a replaced file, a sync that finds no index_state yet (the rebuild has not started indexing) and a file written within the last two minutes returns the lock-busy shape so the watcher retries; the first sync after that reconciles the whole tree once, then syncs go back to scoped. The time bound keeps a rebuild that died before indexing from parking the watcher forever. New test: fails without the guard, passes with it; 136 related tests pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(sync): keep the full catch-up until a full sync completes (#1902) Two gaps in following a replaced database: - Only a reopen done by sync() itself set pendingFullReconcile. The tool-call self-heal (reopenIfReplaced) swapped the handle without it, so the next scoped watcher sync skipped whatever the old handle had absorbed. The flag is now set wherever the database is reopened. - The flag was cleared before the sync ran, so a sync that threw lost the catch-up. It is now cleared only after a full sync completes. Ported from the maintainer's hardening of this fix in #1919. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(sync): report a rebuild in progress as lock contention, not an empty sync (#1902) Since #2014 the watcher tells lock contention apart from success only by LockUnavailableError; it no longer recognises the all-zero result. Following a rebuilt index still returned that shape when the reopen failed or when the recreated file had not been indexed yet, so the watcher counted the step-aside as a clean sync and dropped the pending edits. Both paths now throw LockUnavailableError. A watcher test proves the edit stays pending through the rebuild gap and is reconciled once the rebuild finishes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: danusha2345 <danusha2345@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: danusha2345 <ewidusoc498@gmail.com> |
||
|
|
c405ac5e6c |
fix(windows): make load-sensitive tests wait on real signals; longer pid-file retry (#1773) (#2078)
* 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> |
||
|
|
003305f1e7 |
fix(explore): claim complete source only for sections that are complete (#2077)
* fix(explore): claim complete source only for sections that are complete On the tiers with a completeness signal (>= 500 indexed files) every codegraph_explore response ended with "Complete source for N files is included above — do NOT re-read them", whatever the render had cut. A windowed spine method, a shrunk or dropped cluster and the per-symbol view all went out under that line; the oversize-spine window never even set the trim flag the small tiers key off, which is how a 62-line slice of vscode's 968-line rpcProtocol.ts was called complete. The line also said "Reserve Read for a single specific line range", which explore output must never say. Completeness is now measured at emission from the ranges actually sent (plus back-referenced spans) against the symbols each section set out to deliver. A trimmed response keeps the verbatim / already-Read guarantee, names the trimmed files and the most relevant elided symbols, and steers to another codegraph_explore. The note is offered from most to least specific and fitted to the leftover room (CG-26); a trimmed note yields the pointer list's first entry. The lost-pointer, cut and truncation notes drop "complete" the same way, at no greater length, so the epilogue floor is unchanged. The small tiers' trimmed note now also fires for the cuts the flag missed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(explore): offer an elided method as Owner.member in the completeness note The note offered `as_sql` for django's elided SQLCompiler.as_sql — one of 110 definitions — so agents appended a path and line to disambiguate it, and that query shape returns none of the body. `SQLCompiler.as_sql` alone returns most of it. Methods are now offered as `Owner.member` from the indexed qualified name; everything else keeps its bare name. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * refactor(explore): fit the completeness note and pointer list in one tested function A trimmed note's optional detail (the files, the elided names) never costs a pointer entry the least specific note would have left. The fit moves out of handleExplore into fitExploreEpilogue so both rules are pinned by tests; the second-try branch it had could never fire and is gone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(explore): completeness note wording after #2068 flags the spine window Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(explore): record the completeness-note replay and agent A/B Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(explore): address review of the completeness note - Judge completeness AFTER the dedup fold: a remainder too small to fence is sent in neither call, so a section with one is trimmed, not complete. The whole-file render now declares its symbols too (clamped to the lines it prints; the file node is not one), so a fold there registers. - Label trimmed files unique among every path the response can name, the pointer list included. - "1 file", and no "0 files" when every section was held from an earlier call. - The lost-pointer, cut and truncation notes are one exported table, EXPLORE_FALLBACK_NOTES, with the length rule tested; the lost note picks its wording from trimmedShown instead of re-joining the response. - The design doc's follow-up list marks the overclaim fixed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
290e03f7bc |
fix(explore): pin only the same-named file that defines the symbols a query names (#2071)
A bare basename several directories share (`editorOptions.ts`, `worker.rs`, `compiler.py`) pinned every match, and the matches split the pinned reservation: on vscode `editorOptions.ts clampedInt cursorStyleToString cursorStyleFromString` gave the file holding all three 8,626 chars, against 15,181 without the path, and spent the rest on a workbench helper defining none of them. Path extraction now takes an injected `symbolFiles` lookup (the module stays DB-free, like `existsOnDisk`). When a span matches several files, the matches defining a precise symbol token the query names (camelCase, PascalCase, snake_case, qualified — the seeder's NL-stopword shape test) are pinned and the rest set aside; the summary line names them. When no match defines one, or every match does, the span resolves exactly as before. The same test resolves a basename shared past the ambiguity budget (django's `models.py`) when it narrows into budget, and a line anchor binds once the span narrows to one file. Replay of 196 queries over 8 indexed repos, main vs this branch: every unambiguous pin is byte-identical (38 unique basenames, 13 dir/basename, 8 file:line, 38 earlier-sweep path queries), as are 26 path-free queries, 13 where every match defines a named symbol and 8 naming no symbol. All 52 changed responses are shared basenames — named FULL 207 -> 231 (11 up, 0 down), named lines 6,045 -> 7,577, none over 25K. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
724b5dee0c |
fix(explore): return named bodies from merged spine clusters; let named files use spare budget (#2068)
* fix(explore): return named bodies from merged spine clusters; let named files use spare budget Re-applied onto main after #2062 and #2063 landed in the same render path (originally e31cc9fc on claude/inspiring-grothendieck-8a7b3e): - the merged-spine hold-back and its top-up are dropped: #2062's hold-back (every cluster leaves room for the protected members ranked below it) subsumes them, and the past-the-pin-cap test passes on it; - #2063's "don't window a cluster holding an exact target" becomes the member rule's "don't window a spine method that is or holds one"; - the shrink orders exact targets ahead of spine members, and the exact branch prices member windows (sectionRangesOf) rather than whole bodies; - the final ceiling fit shortens the file header back to its estimate before it cuts source. With an exact target's overshoot (#2063) spending a named file to its funded line, a lower file's header running past its estimate cost it 109 chars of source (922 of 1,031; 995 on main). The valve test now asserts funding in full and delivery to the line, since #2062's exact rendering stops at whole lines. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(explore): record the agent A/B for the named-concentration fix Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(explore): record landing named concentration on main after #2062/#2063 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
47840a6ed4 |
fix(explore): return a qualified or line-anchored method's body instead of its signature (#2063)
`SQLCompiler.as_sql pre_sql_setup get_select` on django returned get_select and pre_sql_setup in full and SQLCompiler.as_sql -- qualified, step 1 of the Flow, 226 lines -- as one signature line. compiler.py renders as a per-symbol focused view, which picked bodies in source order within a tier, so the unnamed bridge get_qualify_sql and get_select (both above as_sql in the file) took the cap. The follow-ups agents issue (`compiler.py:776`, `compiler.py lines 900-1003`) pinned the file and dropped the line numbers. Every agent run ended in a Read of compiler.py. - query-paths: keep `:776` / `:12-40` / `#L88-L120` suffixes as line anchors and bind prose ranges (`lines 900-1003`, `L900-L1003`, `lines 900 to 1003`) to the nearest single-file path. - EXACT targets: a qualified name resolving to <=3 callables, or the callable enclosing a single-line anchor. Multi-line anchors render as that span. - Focused view: tiers exact > named spine step > unnamed bridge > unique named > family co-named, spine in call order. A tier 0-1 body that does not fit is windowed (head + calls into the question's other symbols) with the explore range that returns each hole, never reduced to a signature. Priced in rendered chars net of the signature lines, so the section keeps its promise. - Cluster path: exact clusters rank first with the spine's bounded overshoot, shrink by exact rendered size, and hold back what lower-ranked exact clusters still owe; files holding one are funded like spine files. - Blast radius leads with exact targets. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
adad8407ff |
fix(explore): pay named members before incidental ones across clusters (#2062)
Clusters of equal max importance rank by density, and density counts every member, so a named function with unrelated helpers merged around it outranked a cluster holding a named function alone. Taken whole, the higher clusters spent the file's reservation on the helpers and the isolated named function rendered nothing (lib/response.ts repro: sendBody 0 of 36 lines). Reordering alone only moves the victim. Two rules, both scoped to incidental members (not named, not an entry point, not on the call path): - a cluster's incidental members may only use what is left once every lower-ranked cluster's protected members are paid; its own protected members are never held back, so named-vs-named stays rank's call; - in a cluster holding a protected member, incidental members are never kept past the render's ceiling in rendered size, so the source-order window back to the ceiling no longer cuts a named body's tail (gin's handleHTTPRequest) on selection or on any re-render. 47 deterministic queries over the 7 README repos plus express vs main: named bodies complete 198 -> 234 (17 responses up, 0 down), named lines +10%, responses over the 25K cap 5 -> 1. One new final-cut truncation (django-bag) comes from owedPayableBelow's MIN_CHARS threshold, recorded in the design doc. On main + the named-concentration branch: 235 -> 279. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
c13bd0890f |
fix(wsl): keep WSL off Windows' index on /mnt drives, and explain the I/O error (#995) (#2061)
When Windows-native and WSL CodeGraph both open the same .codegraph/codegraph.db under /mnt/<drive>/, SQLite's locking and -shm shared memory don't hold across the 9p/DrvFs bridge and WSL fails with a bare "disk I/O error". - A SQLite I/O error on the default index of a WSL /mnt/<drive> project is rewritten (in the SQLite adapter, so every open and query is covered) into the CODEGRAPH_DIR=.codegraph-wsl instruction. MCP answers it success-shaped, and a failed default-project open is reported instead of "no project loaded". - On WSL + /mnt/<drive> with CODEGRAPH_DIR unset, the data dir is resolved per root: an existing .codegraph-wsl/ wins, then an existing .codegraph/ index (kept, no silent re-index), else a fresh index goes in .codegraph-wsl/. `codegraph init` says where it went. Every other host is unchanged. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
66aebd17e2 |
fix(explore): three retrieval regressions found against v1.6.0 (#2057)
The pre-release agent A/B (main vs v1.6.0, 7 README repos, 3 runs each) had main returning 15% less source on vscode for the same queries, with Reads in 2 of 3 runs where v1.6.0 had none. Replaying every query both builds' agents issued traced it to three causes: 1. Named gaps (#1711) displaced source. Every cluster fit measured the gap names, which on a long path run to hundreds of chars, so a class shrunk into its file's room overran it by the names alone and was dropped whole: rpcProtocol.ts emitted 222 of 5,496 funded chars (v1.6.0: 5,194), and the test fixture came back as an empty fence. Fits now measure bare gaps; names are added at assembly from what the file's budget has left. 2. Interface members (#1638) corroborated English words. The NL-stopword guard lets a bare word seed a definition when another query word names a symbol in the same file; `readonly host` in an options interface made "main" seed `main()` in agentHostServerMain.ts, which took the named-first tier from the answer files. Members declared inside an interface no longer count as corroboration. 3. The interface-dispatch note announced dispatch through any common base, declared or not: "`extension` -> runtime dispatch to 2706 types implementing Disposable" opened 9 of 31 vscode answers. A supertype now has to declare the member (it, an ancestor, or a same-named Swift extension). When absence can't be judged, because a protocol's requirements aren't indexed, the note stays as before. Replayed queries, source lines vs v1.6.0 (main -> fixed): vscode 10,744 -> 12,577 (12,617), excalidraw 13,065 -> 13,698 (13,764), django 8,420 -> 8,803 (8,954), tokio 13,618 -> 14,151 (14,600), okhttp 6,511 -> 6,883 (7,090), alamofire 7,019 -> 7,249 (7,120); no query below 60% of v1.6.0. Real dispatch notes (Alamofire adapt/retry/asURLRequest, django, okhttp) are unchanged. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
0a9f3de72c |
fix: make the full test suite pass on Windows (#2053)
* 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
|
||
|
|
1926ec39a2 |
fix(index): coordinate rebuilds with running MCP daemons (#1325) (#2051)
* fix: safely stop daemon before reindex * fix(index): coordinate rebuilds with running MCP daemons (#1325) Stop verified project daemons and confirm termination before recreating SQLite. Fence daemon and proxy reopening throughout rebuilds while preserving foreign and newer locks. Add Windows lifecycle coverage and cross-platform shutdown regressions. Co-authored-by: Harry B ✦ <drewavesx0@gmail.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(mcp): fence project opens during index rebuilds after rebase Carry the #1325 rebuild fence through the upstream explicit-project lifecycle, read-only engine opens, synchronous initialization retries, and direct writer acquisition. Preserve shared project leases, read-only fallback behavior, freshness handling, and direct-mode query pools. Cover watched, unwatched, and read-only engines blocking during a foreign rebuild and recovering after the fence is released. * fix(mcp): answer a running index rebuild as guidance, not a tool error (#1325) A projectPath call during `codegraph index` hit assertNoRebuild's plain Error and returned isError; it is an expected, temporary condition, so it now uses a RebuildInProgressError that the tool handler answers success-shaped. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Harry B ✦ <drewavesx0@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
346d3e3702 |
fix(mcp): terminate wedged local proxies with watchdog (#943) (#2049)
* fix(mcp): arm liveness watchdog in local proxy * fix(mcp): terminate wedged local proxies with watchdog (#943) Install the independent liveness watchdog in the local handshake proxy and stop it during shutdown. Build on PR #979 while preserving existing daemon assertions. Add process-termination, slow fallback, and watchdog cleanup coverage. Validate the repro and targeted tests on Windows, macOS, and Linux. Co-authored-by: Haoqian Li <haoqian.li@finalroundai.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Haoqian Li <haoqian.li@finalroundai.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
2268a09b24 |
fix(sync): recover index freshness after lock contention (#1959) (#2043)
* fix(lock): keep a live writer lock regardless of file age (#1959) * fix(watcher): re-arm after lock contention and reconcile before trusting index (#1959) * feat(mcp): report exact index freshness without blocking the daemon (#1959) * fix(mcp): refuse drifted explore source after watcher degradation (#1959) * fix(mcp): refuse graph answers that name a drifted file after watcher degradation (#1959) Explore already refuses to render source from a file that changed while auto-sync was off. search, callers, callees and impact answer from the same frozen graph but kept replying, naming the changed file as if its edges were current. They now take the same refusal: the indexed files an answer names as locations are hashed against the index, and a drifted one is named instead of the answer. codegraph_node keeps its own drift gate, which serves the changed file's current bytes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(sync): recover index freshness after lock contention (#1959) Keep a live writer's lock regardless of its age. Re-arm lock-degraded watchers with throttling and retain stale state until full reconciliation succeeds. Validate contributing files through structured paths and hashes, refusing incomplete checks without tool errors. Report bounded freshness measurements and cover Git fallback exclusions. 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: danusha2345 <danusha2345@users.noreply.github.com> Co-authored-by: danusha2345 <ewidusoc498@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
8bde4a1500 |
fix(mcp): stop pending connections and serve read-only fallbacks (#1963) (#2042)
* 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> |
||
|
|
794d4a2d72 |
fix(mcp): retire failed-startup query workers (#1357) (#2041)
Workers whose project open failed were enrolled as idle and returned errors indefinitely. Route failed readiness through the existing idempotent retirement and replacement path. Ignore retired-worker messages and duplicate readiness handshakes while preserving the crash budget and startup limits. Cover mixed pools, repeated failures, lifecycle duplicates, and real SQLite recovery. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
68085f3997 |
fix(mcp): cap watchdog and startup-handshake timeouts at the largest timer delay (#1966) (#2039)
Node runs a setTimeout delay above 2^31-1 ms after 1 ms instead, so a huge
CODEGRAPH_WATCHDOG_TIMEOUT_MS SIGKILLed a healthy server at once and a huge
CODEGRAPH_STARTUP_HANDSHAKE_TIMEOUT_MS abandoned the launch within
milliseconds. Both parsers now cap at 2147483647.
(cherry picked from commit
|