Skip to content

auto_index watcher leaves orphaned full-size .db.stage.<random> temp files (incl. nested .stage.<A>.stage.<B>), filling the disk #1839

Closed

Description

@darkkid0

Summary

With auto_index = true, the background watcher periodically re-indexes watched projects. Each write of the persisted graph DB leaves behind a full-size .db.stage.<random> temporary file, and in at least one case a nested .db.stage.<A>.stage.<B> copy was also created. For a large repo these temp files are ~1.1 GB each, so a few re-index cycles silently fill the system disk. The temp files are never reclaimed even after a subsequent re-index succeeds and commits a fresh X.db.

This looks like a variant of the re-index write-amplification family (#1083 WAL unbounded, #937 watcher re-index write amplification, #897/#1016 stale WAL/SHM sidecars), but the specific orphaned .db.stage.* temp files (including nested staging) does not appear to have a dedicated report.

Environment

  • codebase-memory-mcp 0.10.8
  • macOS (Apple Silicon), installed native binary
  • CBM_CACHE_DIR / cache root: ~/.cache/codebase-memory-mcp
  • Config: auto_index = true (single key/value in _config.db)
  • Affected project: <VOLUME>-<PROJECT> — a large private repo whose persisted graph DB is ~1.1 GB (names redacted for privacy)

Evidence (current cache root — repo-identifying names redacted)

$ ls -la *<PROJECT>*
-rw-------  ...  1206976512  8 27 00:32  <VOLUME>-<PROJECT>.db
-rw-------  ...  1206583296  8 27 00:17  <VOLUME>-<PROJECT>.db.stage.mjQU5l
-rw-------  ...  1206583296  8 27 00:17  <VOLUME>-<PROJECT>.db.stage.mjQU5l.stage.VgdLsO
-rw-------  ...       32768  8 27 00:17  <VOLUME>-<PROJECT>.db.stage.mjQU5l.stage.VgdLsO-shm
-rw-------  ...     7932792  8 27 00:17  <VOLUME>-<PROJECT>.db.stage.mjQU5l.stage.VgdLsO-wal

Observations:

  1. Orphaned temp, older than the committed DB. The committed X.db has mtime 00:32, but the .stage.mjQU5l temp has mtime 00:17. So a later re-index committed a fresh X.db, yet the earlier 00:17 .stage copy (and its nested copy) were never deleted — pure leak.
  2. Nested staging. X.db.stage.mjQU5l.stage.VgdLsO (+ -shm/-wal) means the writer staged its own temp file a second time. This indicates the atomic "write temp → rename" path is itself being re-staged during a re-index, multiplying the on-disk footprint (here: 1.1 GB × 3 copies ≈ 3.3 GB for one project).
  3. No live handle. lsof shows no process currently holds these .stage.* files, so they are safe to delete — they are leftovers, not in-flight writes.
  4. Smaller watched repos do not carry leftover .stage.* files, consistent with the leak scaling with graph size × re-index frequency.
  5. logs/cbm-daemon.log shows the watcher firing watcher.changed repeatedly (strategy=git) for the watched projects.

Suspected mechanism

The persisted graph DB is written atomically: <project>.db.stage.<random> is written, then renamed over <project>.db. With auto_index on, the watcher re-indexes on every git/file change. When re-indexes overlap or a later write lands before an earlier temp is cleaned up, the temp (and worse, a nested temp-of-temp) is left behind and never reclaimed. Because each temp is a full copy of the ~1.1 GB graph, disk usage grows without bound across idle re-index cycles.

Impact

Suggested fixes

  1. On commit/rename, unlink any pre-existing <project>.db.stage.* in the cache root (and never let the staging glob match *.stage.* inputs, to prevent nested temp-of-temp re-staging).
  2. Enforce a single in-flight index writer per DB (file lock) so overlapping re-indexes don't each emit a temp (also addresses WAL grows unbounded (115 GB in 4.5 h, fills system drive): checkpoint starvation from accumulated per-conversation servers + 11 concurrent index workers on one repo #1083's concurrency).
  3. Add a watcher/re-index storage budget (cf. feat(index): enforce opt-in storage resource limits #1725 / Bound indexing resource usage with configurable limits #1347) and warn/abort when staged + committed DBs exceed a threshold.
  4. Optionally VACUUM INTO / compact and write directly to a single temp name with a guaranteed unlink on both success and failure paths.

Steps to reproduce

  1. codebase-memory-mcp config set auto_index true
  2. Index a large repo (graph DB ≥ 1 GB) and leave it watched.
  3. Let the watcher trigger several re-indexes (or make file/git changes under the repo).
  4. Inspect the cache root — orphaned *.db.stage.<random> files accumulate; under overlap a nested *.db.stage.<A>.stage.<B> appears.

Note on diagnostics

The README asks for logs/trajectory.ndjson, but no trajectory.ndjson / snapshot.json is present in this cache root (likely rotated/absent in 0.10.8 local layout). The file-system evidence above is from the live cache at report time and reproduces the leak deterministically.

Related: #1083, #937, #897, #1016, #1204, #1828

Activity

  1. added
    bugSomething isn't working
    priority/highNeeds near-term maintainer attention; high-impact bug, regression, safety issue, or release blocker.
    stability/performanceServer crashes, OOM, hangs, high CPU/memory
    on Sep 5, 2026
  2. 500rockets commented on Sep 10, 2026

    @500rockets

    Confirming this is still happening on 0.10.6 (macOS arm64). Same nested leftover pattern as in the report, just larger.

    Live graph DB ≈ 8.8 GB
    After some weeks of auto_watch / auto_index: 1,772 leftover .stage files, 3.3 TB
    Quarantined those leftovers (live *.db files left in place). MCP kept serving all projects.
    Within ~8 hours, 80 new leftovers / 352 GB appeared, all the same nested pairs:
    project.db.stage.XXXX
    project.db.stage.XXXX.stage.YYYY
    project.db.stage.XXXX.stage.YYYY-shm
    project.db.stage.XXXX.stage.YYYY-wal
    lsof did not hold the leftovers. delete_project is not a fix here — that would wipe the live index. There is no gc / prune CLI.

    Workaround that stopped the bleed: codebase-memory-mcp config set auto_watch false (then restart the MCP).

    In v0.10.6 this looks like double staging, not just a missed unlink:

    cbm_pipeline_run copies the live DB to project.db.stage.XXXX
    It then points the pipeline at that copy
    cbm_delta_stage_clone() clones again → project.db.stage.XXXX.stage.YYYY
    discard_generation_stage / cleanup_staging_db exist, but they don’t run if the MCP process is killed mid-index, and nothing sweeps abandoned .stage on startup
    Suggested fix (same as this issue, plus one code-level point): clone from the live project.db only (don’t nest a stage on a stage), always unlink the unused sibling after rename, and sweep leftover .stage on startup.

  3. added a commit that references this issue on Sep 12, 2026
  4. DeusData commented on Sep 12, 2026

    @DeusData
    Owner

    Fixed on main by #2111 (merge commit dd454b2346f2ef2054d1799a8b0bca6c5daae0dc).

    The auto_index watcher now sweeps orphaned and nested .db.stage.* temp files instead of leaving full-size copies behind, and no longer takes the misleading invalid_existing_db route on a first index. Ships in v0.10.9.

    Thanks for the report — the full-size orphaned staging files were the crux, and they're cleaned up now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingpriority/highNeeds near-term maintainer attention; high-impact bug, regression, safety issue, or release blocker.stability/performanceServer crashes, OOM, hangs, high CPU/memoryux/behaviorDisplay bugs, docs, adoption UX

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions