Skip to content

Full index build allocates ~11.5 GB on a ~14k-file TypeScript monorepo regardless of the configured memory budget (0.10.8) #1997

Description

@shafty023

Version

codebase-memory-mcp 0.10.8 (aarch64 static build)

Platform

Linux (arm64)

Install channel

GitHub release archive / install.sh / install.ps1

Binary variant

standard

What happened, and what did you expect?

A full index_repository of a single TypeScript monorepo (13,969 files) peaks at
about 11.0 to 11.9 GB RSS on a 16 GB host, and the configured memory budget does not
bound it. The default ram_fraction budget on this box is 3,916 MB and an explicit
CBM_MEM_BUDGET_MB=2500 was also honored in the logs but exceeded by roughly 3 to 4x
in both cases. The same binary indexes five smaller repositories on the same box with
memory flat (the largest of those is 34,829 nodes / 100,940 edges in 8.1 s), so this is
specific to scale, not to the install.

I expected the build to either stay near its stated budget or fail with an explanatory
error naming the pass that overshot. Instead it produced three distinct problems, which
I am reporting together because they share the one allocation.

The allocation is not a concurrency artifact. A single-threaded run
(CBM_INDEX_SINGLE_THREAD=1, CBM_MEM_BUDGET_MB=2500) still reached 11.4 GB, and the
worker log attributes it to one pass:

parallel.registry.done entries=787665 defines=804677 imports=99865
pass.timing pass=registry_build elapsed_ms=2480
mem.phase phase=registry_build rss_mb=11419 peak_mb=11419

787,665 registry entries cost about 11.4 GB with a single worker, in 2.5 seconds. That
matters for triage: backpressure or worker-count tuning cannot fix a single pass that
wants 11.4 GB on its own, so I do not think this is the same defect as a parallel
over-admission overshoot.

A separate multi-worker run got through extraction and died entering resolve at a
comparable peak:

pass.timing pass=lsp_cross_prepare elapsed_ms=5366
mem.phase phase=lsp_cross_prepare rss_mb=11025 peak_mb=11025
parallel.resolve.start files=13969 workers=4

with the daemon having started under:

daemon.start version=0.10.8 memory_budget_bytes=4107238400 physical_job_limit=4
mem.init budget_mb=3916 total_ram_mb=15665 source=ram_fraction

Consequence 1: multi-worker mode hangs rather than failing

When memory pressure stalls forward progress, the supervisor does not surface an
out-of-memory condition. It times the worker out and reports a hang:

index.supervisor.worker_failed outcome=hang exit_code=-1 signal=15
index.supervisor.reap outcome=hang exit_code=-1 signal=15

This reads as "one file wedged the worker", which sent me looking for a bad file for
some time. It is not one file. Across attempts, extraction stalls at a different
position each run: 12,360, then 12,890, then 13,220 of 13,969 files, with no file in
common between the stall points. The position tracks how much memory was already
committed when that run started, not the content at that offset.

If the supervisor can see that RSS is far past budget when it reaps a hang, saying so
in the worker_failed line would remove the entire wrong-turn diagnosis.

Consequence 2: auto_watch=true runs this build inside the serving daemon

auto_watch defaults to true, so the watcher inside the long-lived MCP session daemon
noticed new upstream commits on the watched monorepo and began rebuilding the index
in process. The build wants about 11.5 GB, the box had roughly 10 to 11 GB free
with services resident, and the kernel killed the daemon. The supervising service
restarted, a fresh session daemon started, the watcher fired again, and the cycle
repeated. Four kills in fourteen minutes:

Out of memory: Killed process 22295 (codebase-memory) total-vm:17120976kB,
anon-rss:11338244kB ... oom_score_adj:200
Out of memory: Killed process 26839 (codebase-memory) total-vm:17121040kB,
anon-rss:11848124kB ... oom_score_adj:100

Two things make this worse than a failed index. The daemons self-set oom_score_adj
to 200 and 100, which is what kept the kernel selecting them. And because the victim is
the server, every client reaching it got HTTP 502 for roughly ten seconds per restart,
so the visible symptom was an intermittently unavailable MCP server rather than
anything that looked like an indexing problem.

A serving daemon probably should not run an unbounded full build in its own address
space at all. At minimum, an in-daemon watcher rebuild seems like it should be capped,
or run out of process, or be off by default at this repository size.

Consequence 3: the CLI hint points away from the evidence

Every one of these failures surfaces to the caller as only:

{"status":"error","hint":"Pipeline failed. Check repo_path exists and contains source files. ..."}

repo_path existed and contained source files in all of them. The real evidence is in
undocumented files under <cache_dir>/logs/ (cbm-daemon.log and .worker-log-*),
which I found by guessing. Nothing in the CLI output mentions either path.

The knobs that actually resolve this are also undocumented. I found them by running
grep -aoE 'CBM_[A-Z0-9_]+' over the 281 MB static binary: CBM_INDEX_SINGLE_THREAD,
CBM_MEM_BUDGET_MB, CBM_INDEX_WORKER_TIMEOUT_S, CBM_DISABLE_LSP_CROSS,
CBM_INDEX_MAX_RESTARTS, CBM_MAX_FILE_BYTES, CBM_RETAIN_PER_FILE_MB,
CBM_RETAIN_TOTAL_MB, CBM_SEMANTIC_ENABLED and others. The config command exposes
only auto_index, auto_index_limit, auto_watch, ui-lang, ui_enabled and
ui_port, none of which touch memory. Documenting the first three would have saved the
whole investigation.

What works

Stated plainly, because it narrows the problem:

  • CBM_INDEX_SINGLE_THREAD=1 with about 15 GB free (every other service stopped)
    completes the identical full index in 1m33s, status=indexed, 164,323 nodes and
    834,119 edges, both matching the expected counts, parse_partial_count 278, six
    indexes and none quarantined. One caveat so the timing is not overread: that run
    reused cached extraction from the preceding failed attempts, so 1m33s is not a
    cold-start number. The memory behavior is the point, not the speed.
  • mode: fast succeeds on the same repository.
  • The five smaller repositories on the same box index normally, memory flat throughout.

So the build is entirely capable of completing. It just needs about 11.5 GB of headroom
that neither budget setting reserves or enforces, and nothing in the product tells an
operator that in advance.

Reproduction

I cannot share this tree, and I want to be straight about that rather than imply a
public repro I have not run. What I can characterize precisely:

  1. Code being indexed: a private TypeScript monorepo, 13,969 files, producing
    787,665 registry entries, 164,323 nodes and 834,119 edges. Ordinary application and
    library TypeScript with dense cross-package imports; no generated blobs, no vendored
    trees, nothing that .gitignore should have excluded.

  2. Host: 16 GB RAM (total_ram_mb=15665), 4 vCPU, aarch64, Linux.

  3. Command:

    codebase-memory-mcp cli index_repository '{"repo_path":"<repo>","mode":"full"}'
    
  4. Result: with services resident, either a supervisor hang (multi-worker) or a
    kernel OOM kill, at 11.0 to 11.9 GB RSS in both cases, against budget_mb=3916 by
    default or 2,500 when set explicitly. With CBM_INDEX_SINGLE_THREAD=1 and about
    15 GB genuinely free, success in 1m33s.

Expected: the build respects CBM_MEM_BUDGET_MB, or fails with an error naming
registry_build and the observed RSS.

My guess at a public equivalent, untested: any TypeScript monorepo in the 12k to 15k
file range with heavy cross-package imports, indexed on a host with under about 16 GB
free, should show the same mem.phase phase=registry_build peak. If it would help, tell
me which public repository you would like measured and I will run it on this box and
post the phase table.

Diagnostics trajectory

Not captured during these runs, since I did not know CBM_DIAGNOSTICS=1 existed until
after the fact. I can reproduce with it set and attach the ndjson on request; the
repository and the box are both still available, and I can also test a patch or a build
with extra mem.phase instrumentation.

Project scale

164,323 nodes / 834,119 edges / 13,969 files (787,665 registry entries)

Confirmations

Same deployment as #1991 (the per-request latency floor), if that is useful context for
the environment. Thanks for the work on this project.

Metadata

Metadata

Assignees

No one assigned

    Labels

    parsing/qualityGraph extraction bugs, false positives, missing edgesstability/performanceServer crashes, OOM, hangs, high CPU/memoryux/behaviorDisplay bugs, docs, adoption UXwindowsWindows-specific issues

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions