Skip to content

Deleted symbols persist across a full re-index and are returned as current by find_symbol (0.8.63) #67

Description

@hrabbach

Version: memtrace 0.8.63
Platform: Windows 11 Pro (10.0.26200), MemDB mode local
Runtime: single memtrace start (owner + watcher + MCP on :3030), watcher attached to the repo
Affected file language: JavaScript (ESM)

Summary

A top-level exported function was deleted from a file in a normal commit. The file still exists. Hours later, after the watcher had completed a full index and an embeddings pass, the graph still returns three Function nodes for that symbol, at three different line ranges, with found: true and no staleness signal. Two of the three ranges point past end-of-file.

The nodes appear to be snapshots of the symbol from three different points in the file's edit history. find_symbol is a present-time query (as_of defaults to "now"), so a bi-temporal invalid_at should exclude them. It does not appear to be set on deletion.

This is not indexing lag. It was re-measured after a complete index + embed cycle and was byte-identical — same three node IDs, same line ranges.

Impact

find_symbol confidently returns a function that does not exist, with found: true, is_exported: true, and a _pre_edit_check envelope reporting risk_level: low, direct_callers: 0. An agent instructed to prefer Memtrace over grep for code discovery will reason off a deleted symbol, and nothing in the response indicates the result is historical.

direct_callers: 0 is the most dangerous part — it reads as "safe, nothing depends on this," when the correct answer is "this does not exist."

Reproduction

  1. Have a watched repo with memtrace start running.
  2. Pick a top-level exported function in a JS file that has been edited several times over its history, so the symbol has occupied different line numbers across commits.
  3. Delete only that function, leaving the file in place. Commit.
  4. Let the watcher index — confirm via get_repository_stats that last_indexed is newer than the commit and that an embed_complete episode has landed.
  5. find_symbol(name=<symbol>, repo_id=<repo>).

Expected: 0 matches, or matches flagged as historical.
Actual: one node per historical position of the symbol, all returned as current.

Observed case

Symbol getCurrentEngine in a JS module (private repo, paths generalized).

Fact Value
Occurrences in pre-deletion blob 1, at line 104
Deletion commit 1 file changed, 8 deletions(-)
Occurrences in HEAD blob 0
Current file length 98 lines
last_indexed after deletion newer than the deletion commit
Last episode embed_complete, anchored to a later commit
indexing_incomplete false

find_symbol returned 3 matches, all kind: Function, is_exported: true:

node id start–end provenance.episode_uuid
memdb:281475057616547 91–93 75b9c0e1-…
memdb:281475045834150 103–105 null
memdb:281475004340836 104–106 3894c05f-…

Two observations:

  • Ranges 103–105 and 104–106 exceed the file's 98 lines.
  • The node with episode_uuid: null reads as a HEAD-indexed node rather than a historical episode snapshot. If so, the re-index should have replaced or removed it. It did not. This is the one least explicable as expected bi-temporal history.

Why the existing cleanup primitives don't cover it

  • cleanup_stale_records(check_missing: true) matches on file_path missing from disk. The file still exists, so these records are never candidates. Confirmed by dry run — it matched 3 unrelated paths and would delete 127 nodes, none of them these.
  • cleanup_episodes documents that HEAD-indexed nodes are never deleted, putting the null-provenance node out of its reach by design.
  • Plain re-index does not clear them (demonstrated above).

So there appears to be no documented primitive that removes a stale symbol node inside a surviving file, short of delete_repository + full re-index.

Secondary finding (separate, lower severity)

In the same repo, cleanup_stale_records(check_missing: true, dry_run: true) reported 127 orphan nodes across 3 file paths that no longer exist on disk, out of 17,948 records scanned (~5.2s). One of those files had been deleted in a commit roughly a day earlier. So orphans from deleted files also survive watcher-driven indexing until cleanup_stale_records is run manually — that case at least has a working remedy.

What I did not verify

Stated explicitly so nothing here is taken as more than it is:

  • I did not confirm that the ~+4,780 total-node growth observed across one index cycle (16,415 → 21,195; Methods +259, Functions +59) is caused by duplicate accumulation. The commits in that window were mostly documentation, so the growth looked disproportionate, but I did not isolate it and it may have an ordinary explanation.
  • I could not time or test memtrace index . --fresh as a remedy. Run from the repo root while memtrace start was active, it emitted its first line and then made no progress until killed — consistent with contending for the MemDB the daemon owns, though I did not confirm that cause. If a fresh rebuild is the intended remedy, it may be worth documenting that the runtime must be stopped first.
  • I did not test whether this reproduces on non-Windows platforms, in other languages, or on a symbol that has only ever occupied one line range.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions