Skip to content

fix(resolution): what a resolver keeps goes when what it was read from moves - #3214

Merged
colbymchenry merged 2 commits into
mainfrom
claude/spd2-memo-clear
Nov 1, 2026
Merged

colbymchenry merged 2 commits into
mainfrom
claude/spd2-memo-clear

Conversation

@colbymchenry

Copy link
Copy Markdown
Owner

Summary

A resolver lives as long as its CodeGraph, so codegraph serve and a watcher sync on one object for days. Thirteen things it remembered survived clearCaches, so a sync resolved an edit by what the file said before it. The wrong answer goes into the index, where a restart doesn't undo it until that file is edited again.

Nothing changes for a one-shot codegraph init or sync: full indexes are byte-identical to main.

Root cause

clearCaches drops what clearNameMatcherMemos and its peers name. Eight memos in name-matcher.ts weren't named, and three framework memos have no clear function at all. The Cargo crate map and the compile database's include directories were read once and kept for good, the second in a process-level map whose clear functions nothing called.

What was kept Key Edit Sync on one object Fresh index
GO_BUILD_CONSTRAINTS file swap two twins' //go:build lines sep_a.go sep_b.go
GO_BUILD_TWINS target.id add a twin for another target sep_a.go sep_b.go
SFC_SCRIPT_RANGES file <script setup> to <script> no edge run -> helper
OBJC_MEMBER_TABLES file @property Foo * to Baz * Foo::run Baz::run
OBJC_CLASS_FILES class + folder a namesake class beside the caller Widget::refresh Gadget::refresh
RECEIVER_ONLY_NAMES file + name f() to f(history) no edge History::back
CPP_SPECIALIZATION_ARGUMENTS n.id two specializations trade lines read<int> read<long>
CPP_SPECIALIZATION_OWNER_ARGUMENTS n.id a member moves to another specialization no edge formatter::format
cics transidIndexes context a transaction id moves program ONE TWO
expo-modules bindingMemo file + name requireNativeModule('ExpoTwo') ExpoOne.ping ExpoTwo.ping
angular workspaceRoots folder an nx.json appears no edge navigates /lyrics
rust cargoWorkspaceMapCache context a workspace member added import fails resolves
cppIncludeDirCache project root compile_commands.json with -Ithird other/foo.h third/foo.h

The fix

  • Memos cheap to rebuild go in clearNameMatcherMemos.
  • Framework memos renew with a node list, as the route tables beside them do, since that list's identity changes with the caches. A framework module loads lazily, so clearCaches shouldn't reach into one.
  • Cargo manifests and compile databases join the module-config snapshot (sync: a tsconfig paths (or go.mod module) change keeps resolving imports with the old mapping #2977), because they're too much to read again per sync. That machinery covered neither, so this extends it:
    • An entry is now <digest>@<mtime>:<size>. A sync stats each file and reads one only when its stamp moved, for every config file, tsconfig and go.mod included. A file whose digest is the same re-opens nothing, so a version bump or a build that rewrites the same database costs one read.
    • A changed manifest re-opens Rust, a changed database C and C++.
    • A crate under crates/* names no file the snapshot holds, so a sync that added or removed files reads the workspace again. The include directories found by listing top folders, used with no compile database, are read again at the same point.
    • An index from an older engine has no entries for these files, and that counts as no change, so upgrading re-opens nothing.
  • importMappingCache and the two clear functions with no callers are gone. clearCppIncludeDirCache was called by resolution.test.ts alone.
  • A guard: name-matcher-memo-clears.test.ts fails on a module-level memo in name-matcher.ts that clearNameMatcherMemos doesn't drop, with a six-name allow-list, each with its reason.

Not covered: a path dependency outside the workspace (c = { path = "../c" }) isn't in the snapshot, so a rename in its manifest waits for an edit to whoever imports it. The manifests of a project's own crates are watched.

Validation

Windows 11.

Red before green. 16 tests red on main, one per item plus the Cargo and compile database shapes, in sync-memo-lifetime.test.ts and sync-module-config-change.test.ts. Each indexes, edits and syncs on one object and compares edges and unresolved references with a fresh index. 13 mutations of the fix all turn a test red.

The six found by reading are all real, each with a fixture red on main.

A/B against main, every dump compared byte for byte. Each repo twice: a full index, and an index, a comment added to one file and a sync on the same object. No edge moved, as expected: a fresh process has nothing stale, and a comment gives a memo nothing to go stale on.

Repo Language Full index Sync on one object
vscode-be TypeScript identical identical
excalidraw TypeScript identical identical
vue-vben-admin Vue identical identical
redis C identical identical
rocksdb C++ identical identical
caddy Go identical identical
tokio Rust identical identical
SDWebImage Objective-C identical identical
ObjFW Objective-C identical identical

Cost. 8 interleaved rounds against main, medians, wall s and CPU s:

Repo Step Main This branch
rocksdb index 31.62, 110.76 31.73, 110.39
rocksdb sync, nothing changed 0.98, 0.69 0.95, 0.69
rocksdb sync after an edit 7.59, 8.19 8.16, 8.78
vscode-be index 12.82, 44.88 12.90, 45.40
vscode-be sync, nothing changed 0.94, 0.84 0.97, 0.80
vscode-be sync after an edit 4.29, 4.51 4.38, 4.58

Rounds on this box spread by 10% or more, so 1% can't be shown by timing alone. rocksdb's sync after an edit reads +7% above, so I profiled it: 4 interleaved runs, profile self time 31.2 s on main and 31.1 s here, walls 7.59 to 8.39 and 7.75 to 8.12, and no function in the top 16 moved. A sync adds 6 stats for the new candidates and no longer reads a config file that hasn't moved.

Suite on this head, with a kernel built from it: 880 files passed, 6 skipped; 11,042 passed, 2 expected fail, 87 skipped, 0 failed; 4 workers, Vitest 4.1.11.

Also here

A second commit fixes a fault of mine from #3192, which a full suite here turned up: 1 unhandled rejection, resolver worker exited with code 1, in worker-teardown.test.ts. Each worker's ready promise rejects when a start fails, and only ready() handled that. Terminating a worker that hasn't said ready exits it with code 1, so a pool torn down mid-boot left a rejection nobody handled, when the timing gave it that. The new test in that file is red without the line. It isn't in the CHANGELOG, since #3192 hasn't shipped.

🤖 Generated with Claude Code

colbymchenry and others added 2 commits November 1, 2026 09:46
…m moves

A resolver lives as long as its CodeGraph, so a server syncs on one for days.
Eight memos in name-matcher and three in frameworks outlived clearCaches, and
the Cargo crate map and compile database's include directories were read once
for the process, so an edit was resolved by what the file said before it.

The eight go in clearNameMatcherMemos, and the framework memos renew with the
node lists, whose identity changes with the caches. Cargo manifests and compile
databases join the module-config snapshot: a sync stats them, reads one only
when its stamp moved, and re-opens Rust or C and C++ imports when what
resolution reads from it did. A crate under a glob member is found when files
come or go. importMappingCache and the two clear functions no one called go.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…es no rejection

Each worker's ready promise rejects when a start fails, and only ready() handled
that. Terminating a worker that hasn't said ready exits it with code 1, so a pool
torn down mid-boot left an unhandled rejection when the timing gave it that.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@colbymchenry
colbymchenry merged commit f72c9d2 into main Nov 1, 2026
@colbymchenry
colbymchenry deleted the claude/spd2-memo-clear branch November 1, 2026 16:22
bompus added a commit to bompus/codegraph that referenced this pull request Nov 2, 2026
…m moves (colbymchenry#3214)

Ported from upstream f72c9d2.

Framework memos (Angular workspace roots, CICS transaction ids, Expo
native bindings) renew with the node list, so a sync reads the edited
tree. Cargo manifests and compile databases join the module-config
snapshot: a sync stats them, reads one only when its stamp moved, and
re-opens Rust or C and C++ imports when what resolution reads from it
did; the Cargo crate map is no longer cached per context in the Rust
framework. The kernel's own per-run memos go with the resolver, which a
sync closes, so no name-matcher memo set is ported. CHANGELOG bullets for
this batch's behaviour changes are included.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant