Repository navigation
Description
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:
- Orphaned temp, older than the committed DB. The committed
X.dbhas mtime00:32, but the.stage.mjQU5ltemp has mtime00:17. So a later re-index committed a freshX.db, yet the earlier00:17.stagecopy (and its nested copy) were never deleted — pure leak. - 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). - No live handle.
lsofshows no process currently holds these.stage.*files, so they are safe to delete — they are leftovers, not in-flight writes. - Smaller watched repos do not carry leftover
.stage.*files, consistent with the leak scaling with graph size × re-index frequency. logs/cbm-daemon.logshows the watcher firingwatcher.changedrepeatedly (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
- Unbounded disk consumption (multiple full-size copies of multi-GB graphs) until the system drive is full, which then also breaks daemon startup (cf. Daemon start fails with daemon.ipc.listen_failed stage=pending_publication when /tmp is full; clients get a misleading "active or starting" 30s timeout and MCP hosts silently drop the server #1828 "/tmp is full").
- Silent: no log warning, no auto-cleanup of orphaned stages.
Suggested fixes
- 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 nestedtemp-of-tempre-staging). - 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).
- 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.
- Optionally
VACUUM INTO/ compact and write directly to a single temp name with a guaranteedunlinkon both success and failure paths.
Steps to reproduce
codebase-memory-mcp config set auto_index true- Index a large repo (graph DB ≥ 1 GB) and leave it watched.
- Let the watcher trigger several re-indexes (or make file/git changes under the repo).
- 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.
Activity
fix(pipeline): take the stage lock before the stage file is visible (c…
500rockets commentedon Sep 10, 2026
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.
Merge pull request #2111 from DeusData/fix/1839-1864-stage-orphans
fix(pipeline): never mint a staging path from a staging path
fix(pipeline): own every staging file through an exclusive sidecar lock
fix(pipeline): sweep the orphaned stages of a dead writer before each…
DeusData commentedon Sep 12, 2026
Fixed on
mainby #2111 (merge commitdd454b2346f2ef2054d1799a8b0bca6c5daae0dc).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 misleadinginvalid_existing_dbroute 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.
fix(pipeline): never mint a staging path from a staging path
fix(pipeline): own every staging file through an exclusive sidecar lock
fix(pipeline): sweep the orphaned stages of a dead writer before each…
fix(pipeline): take the stage lock before the stage file is visible (c…
fix(pipeline): never mint a staging path from a staging path
fix(pipeline): own every staging file through an exclusive sidecar lock
fix(pipeline): sweep the orphaned stages of a dead writer before each…
fix(pipeline): take the stage lock before the stage file is visible (c…