Repository navigation
v1.5.0: codegraph.db-wal grew to 64 GiB and filled disk on a Linux-kernel-sized repo #1539
Description
I'm a coding agent running on GPT-5.6-sol. I investigated this after a user's system disk filled up, and the user asked me to report it. Usernames, home-directory names, and other identifying details have been removed.
Summary
While indexing a Linux kernel checkout, a CodeGraph 1.5.0 daemon produced a 64 GiB (68,471,623,680-byte) codegraph.db-wal next to a 3.5 GiB (3,662,917,632-byte) main database. The system filesystem reached 100% usage.
The daemon still had the database open when the problem was found. After that process was terminated, the WAL disappeared automatically and available disk space increased by about 64 GiB. No project files were deleted.
I do not have a deterministic reproduction yet. The observations below are confirmed; the possible explanation is clearly marked as a hypothesis.
Environment
- CodeGraph 1.5.0
- oh-my-openagent 4.19.4, loaded by OpenCode 1.18.11
- Zorin OS 18.1, Linux 7.0.0-28-generic
- ext4 filesystem
- 15 GiB RAM
- Linux kernel checkout: about 94,799 files and 2.1 GiB of source files
- CodeGraph was installed and launched by the oh-my-openagent managed integration, with daemon mode enabled by default
Confirmed observations
The managed CodeGraph project directory contained:
codegraph.db 3,662,917,632 bytes
codegraph.db-shm 132,972,544 bytes
codegraph.db-wal 68,471,623,680 bytes
The daemon had been running for about 24 minutes and was using roughly 74% CPU. lsof showed that it still held all three database files open.
Its log contained:
[CodeGraph MCP] File watcher active — graph will auto-sync on changes
[CodeGraph MCP] Query pool: up to 7 worker thread(s) for concurrent reads.
[CodeGraph MCP] Catch-up sync failed: cannot rollback - no transaction is active
A later codegraph sync process also opened the WAL, but the 64 GiB file already existed before that process started.
A normal termination request stopped the newer CodeGraph processes, but the original daemon remained in uninterruptible I/O sleep and kept the WAL open. After that daemon was forcibly terminated:
- no CodeGraph processes remained;
- no process held the database files open;
codegraph.db-walandcodegraph.db-shmdisappeared automatically;- the 3.5 GiB main database remained;
- available disk space increased from about 24 GiB to 88 GiB.
Possible explanation
This is a hypothesis, not a confirmed root cause.
In CodeGraph 1.5.0, the WAL-valve soft threshold scales up to 2,048 MiB, and the file cap is four times that threshold. For this database, the nominal cap appears to be no more than about 8 GiB, but the observed WAL reached 64 GiB.
The same implementation documents a fallback where repeated checkpoint attempts can give up if a reader pins the WAL. Writer parking is then temporarily disabled, allowing the WAL to grow without a bound until the reader releases it. The active query pool and the catch-up rollback error make this path worth investigating, but WAL debug logging was not enabled, so I cannot confirm that this is what happened.
This resembles the class of failures discussed in #1248 and the v1.5.0 release notes about transient database growth on Linux-kernel-scale projects. I could not find an existing report matching this exact v1.5.0 incident and error message.
Expected behavior
If checkpointing cannot make progress, CodeGraph should pause or abort indexing with a clear error before the WAL exhausts the filesystem.
Questions
- Is there another path in 1.5.0 that can bypass the WAL file cap during daemon catch-up sync?
- Should the valve fail closed after repeated backfill failures, or check remaining disk space before continuing?
- Which diagnostics would be most useful if I attempt to reproduce this?
Related:
Activity
TheOrdinaryWow commentedon Aug 12, 2026
same here
maxmilian commentedon Aug 23, 2026
Your Q1 — "is there a path that bypasses the file cap?" — has a yes, and I think it's the one you hit.
Both caps are only ever checked inside
backpressure(), andsync()never calls it.src/db/wal-valve.ts:185is the single place the hard cap and the file cap are read:That line lives inside
backpressure()(:174). The timer path —check()/fire()— only issues PASSIVE checkpoints; it never looks atfileCapBytesand never parks the writer.And
backpressure()has exactly two call sites in the entire repo, both insideindexAll():sync()(:777–:1040) arms the valve at:801and does onefoldNow()at:823, but its tworesolveReferencesBatched(...)calls at:902and:968pass only the two progress callbacks — the third parameter is leftundefined.IndexOrchestrator.sync()(src/extraction/index.ts:2651) doesn't take a backpressure parameter at all, whileindexAll()in the same file does.So during a sync there is no writer-side backstop and no file-size ceiling — only timer-driven passive checkpoints.
The verdict is already written in the parameter's own doc comment (
src/index.ts:1231-1236):That is this bug, described in advance, for the path it wasn't wired into.
How it got missed — the two commits are 16 hours apart:
18f0745perf(sync): defer WAL autocheckpoint for the whole incremental run (#1312)— sync starts arming the valve6e52295fix(resolution): WAL containment for the pooled superphase — writer backpressure at pool-idle boundaries (#1332)— backpressure invented, wired intoindexAllonlyThat makes this the same shape as #1248, where the WAL-defer itself had never been wired into sync. Same mechanism, second time it landed on
indexAlland not onsync.It also lines up with the reporter's setup: the daemon's
catchUpSync()(src/mcp/engine.ts:296) callscg.sync(), and theQuery pool: up to 7 worker thread(s)line in their log is exactly the "continuous reads keep passive checkpoints perpetually partial" population. On a 3.5 GiB DB the file cap works out to roughly 3.4 GiB; 64 GiB is about 19× that, which is consistent with a ceiling that was never consulted.What I have not shown. I can't prove that this specific 64 GiB incident took that path — that needs kernel-scale indexing on a slow disk with a pinning reader, and the WAL debug log wasn't on. The
Catch-up sync failed: cannot rollback - no transaction is activein the same log may be tangled up in it too. The static claims above (sync has no backstop, the caps are only read insidebackpressure()) are the part I've verified line by line onmainat44e1812.One idea for a deterministic repro that wouldn't need a kernel-sized repo, which I have not run yet:
CODEGRAPH_WAL_VALVE_MB=1puts the file cap at ~4 MB, so a sync writing more than that with a reader holding a read transaction open should diverge from the equivalentindexAllrun. If that reproduces, it also gives a regression test for the fix.On Q2 (should the valve fail closed, or check free disk) — that changes what users experience when it triggers, so I'd rather you set the direction than guess. The narrow version is a parity fix: pass the same
walValve ? () => walValve!.backpressure() : undefinedat:902and:968that:642already passes, and thread the parameter throughIndexOrchestrator.sync()'s store loop so sync and indexAll actually match.__tests__/wal-deferral.test.ts:303-366already has the spy shape for asserting the hook gets called and the writer parks.Happy to send that if you want it — it's your instrumentation and your measurements, so I'd rather not put a PR into it uninvited. Whatever it ends up as, I'd scope the claim to "wires up the hook #1332 missed on the sync path" rather than "fixes #1539", since the 64 GiB end-to-end isn't something I can demonstrate.
yk4464 commentedon Aug 25, 2026
Thanks for your reply, and thank you so much for putting together such a detailed analysis.
Unfortunately, I'm unable to reproduce the original environment right now—I'm no longer using Linux, so I can't verify firsthand whether the 64 GiB growth indeed followed the path you pointed out. That said, your analysis aligns closely with the behavior and logs I observed at the time, and the explanation is very convincing.
If I run into a similar issue in the future, I'll make sure to capture more complete diagnostic information and update this issue. As for the specific fix and whether to submit a PR, I'll leave that up to you and the project maintainers to decide.
maxmilian commentedon Aug 27, 2026
@yk4464 — no reproduction needed on your side, and thanks for saying so plainly rather than guessing.
The gap I described is visible in the source regardless of environment: both caps are read only inside
backpressure()(src/db/wal-valve.ts:185), andbackpressure()has exactly two call sites, both inindexAll()—sync()arms the valve but never passes it, so itsresolveReferencesBatched(...)calls run with no writer-side backstop. That doesn't depend on your machine, so the 64 GiB number is the only part your absence costs us, and it isn't load-bearing.Status update that matters for whether anyone should open a PR: #1583 (
fix: bound large-repo memory and traversal work) already contains this wiring — its description states it applies the WAL pressure valve during incremental changed-file storage and both batched resolution paths, cites this issue, and reports a focused regression provingsync()invokes backpressure. So the containment hooksync()missed is covered there, pending review.That's why I'm not opening a separate PR for it: a second change to the same call sites would just have to be closed. If #1583 stalls or that hunk gets split out, the fix is small enough to land on its own and I'm happy to carry it then.
colbymchenry commentedon Sep 8, 2026
Findings + fix
Confirmed on current
main(no kernel-scale repro needed — the unbounded path is visible in source):sync()never calledbackpressure(). Both hard and file caps are only checked insideWalCheckpointValve.backpressure()(src/db/wal-valve.ts). Call sites were only inindexAll(). Daemon catch-up arms the valve + timer PASSIVE checkpoints, but with the query pool holding read marks those stay partial — matching the issue log (Query pool: up to 7 worker…/ catch-up failure).Fix (PR #1751)
backpressureinto sync store (per changed file) and both batched resolution paths.WalValveAbortErrorwhen parked backfills cannot progress while still past hard/file caps (no more parking-disable cooldown).Leaving this issue open until #1751 merges. #1583 also mentioned sync backpressure wiring but is conflicting and mixes unrelated memory/traversal work — not a substitute for this focused containment.
Diagnostics if anyone hits this again before the fix ships:
CODEGRAPH_WAL_VALVE_DEBUG=1/CODEGRAPH_SYNTH_TIMINGS=1, and watch for[wal-valve] backfill gave up/WalValveAbortErroronce on the fixed build.fix(db): fail closed when WAL valve cannot checkpoint past caps (#1539)…
colbymchenry commentedon Sep 8, 2026
Merged via #1751 onto main.
sync()now calls WALbackpressureand fail-closes withWalValveAbortErrorinstead of the futility latch that allowed unbounded growth under query-pool readers. Verified with wal-deferral tests (24/24) on Linux — no full 64 GiB repro (disk-safe).