Skip to content

index_repository: OOM kill instead of degradation — nested .gitignore below another .gitignore is dropped, and the memory budget is advisory (mem.backpressure.futile → soft_overshoot) #1973

Open

Description

@OSDDQD

Version

codebase-memory-mcp dev — built from source at ae5de7fe (main, 2026-08-31).
Also reproduced with the 0.10.8 release archive (linux-amd64-portable).

Platform

Linux (x64) — Ubuntu 26.04, 24 cores, 31 GB RAM, gcc 15.2, scripts/build.sh (no UI).

Install channel

Built from source (and GitHub release archive).

Binary variant

standard

What happened, and what did you expect?

index_repository on a repository that contains a nested .gitignore below a directory that has its own .gitignore does not just index too much — it is killed by the OOM killer, and nothing usable is reported to the caller.

Two separate defects compound here:

1. Discovery ignores the deeper .gitignore. try_load_nested_gitignore() in src/discover/discover.c:826 early-returns as soon as the walk frame already carries a local_gi, and local_gi is inherited by child frames. So only the shallowest nested .gitignore on a path is ever loaded. This is the same root cause reported in #530 (§2 "Cascading .gitignore Files Ignored in Subdirectories"); I am filing separately because the consequence below is not covered there and is much worse than "too many files get scanned". The code is unchanged on current main:

static cbm_gitignore_t *try_load_nested_gitignore(const walk_frame_t *frame) {
    if (frame->local_gi || frame->prefix[0] == '\0') {
        return NULL;   // any ancestor .gitignore disables every deeper one
    }

An empty ancestor .gitignore is enough to disable the deeper one.

2. The memory budget is advisory, so over-collection turns into an OOM kill rather than degradation. In src/pipeline/pass_parallel.c:774-781, once backpressure has spun PP_BACKPRESSURE_MAX_SPINS times and the process is still over budget, it logs mem.backpressure.futile action=soft_overshoot once and then keeps admitting work. There is no cap, no shedding, and no abort — RSS then grows by roughly an order of magnitude past the budget until the kernel kills the process.

Expected: exceeding the configured budget should degrade (shed work, cap concurrency, or fail the index with a clear error), not overshoot ~9x and die. CBM_MEM_BUDGET_MB reads like a limit but does not act as one.

3. The failure is invisible to the caller. The CLI surfaces only:

error: daemon-backed CLI execution failed
Failed index_repository (23983 ms)

No per-project log is written (only .worker-log-* survives, and only if you know to look). In one run cli --progress printed nothing at all for 5 minutes before the kill. cbm-daemon.log ends at daemon.start.

Reproduction

Fully synthetic, no proprietary code. This mimics the standard Laravel layout, where storage/.gitignore exists and storage/<subdir>/.gitignore contains * — so every Laravel project with cached artifacts on disk hits this.

T=/tmp/cbm-oom-repro; rm -rf "$T"; mkdir -p "$T/storage/dump" "$T/app"; cd "$T"
git init -q .
printf 'node_modules/\n' > .gitignore
: > storage/.gitignore                      # empty ancestor .gitignore is enough
printf '*\n' > storage/dump/.gitignore      # git honours this; cbm does not
printf '<?php\nfunction appFn() { return 1; }\n' > app/x.php

python3 - <<'PY'
import json
def nest(d):
    if d == 0: return {"leaf": "x"*60, "n": 12345}
    return {"lvl%d_%d" % (d, i): nest(d-1) for i in range(3)}
blob = json.dumps({"__meta": {"route": "/api/v1/thing"}, "data": [nest(5) for _ in range(3)]})
for n in range(5000):                       # 5000 x ~72 KB deeply nested JSON
    open("storage/dump/%05d.json" % n, "w").write(blob)
PY

git check-ignore -q storage/dump/00000.json && echo "git DOES ignore these files"

CBM_CACHE_DIR="$T/cache" codebase-memory-mcp cli index_repository --repo-path "$T"

Result: the process grows past the budget and is OOM-killed (12.5 GB anon-RSS inside a 12 GB cgroup, ~10 s). Run it under systemd-run --user --scope -p MemoryMax=12G so it kills only itself.

Expected: ~2 nodes — app/x.php and the project — since git ignores everything under storage/dump/.

A softer variant of the same repro shows the discovery bug without the kill: use flat 26 KB JSON payloads instead of the nested ones, and indexing succeeds with 1,020,008 nodes / 1,020,006 edges built entirely from files git ignores, out of a repository whose only source file is two lines of PHP.

Real-world impact (a Laravel app, ~5,300 PHP source files): discovery reported files=25638 where the repo has 6,682 git-visible files; 5,404 of the extras were storage/debugbar/*.json dumps. Indexing was OOM-killed at 25 GB anon-RSS / 38 GB virtual after 26 s wall (5m28s CPU across 24 cores), twice, on both 0.10.8 and main. Adding a one-line .cbmignore with storage/ makes the same repository index in 9 s with 32,108 nodes / 174,369 edges — so nothing else about the repo is unusual.

Logs

level=info  msg=pipeline.discover files=25638 elapsed_ms=1215
level=info  msg=pipeline.mode mode=parallel workers=24 files=25638
level=info  msg=parallel.extract.start files=25638 workers=24
level=info  msg=parallel.extract.retention retain_sources=true total_mb=347 per_file_mb=32
level=info  msg=parallel.mem.budget total_mb=2779 per_worker_mb=115
level=warn  msg=mem.pressure.warn rss_mb=2798 budget_mb=2779 pct=100
level=warn  msg=mem.backpressure.futile action=soft_overshoot
level=warn  msg=index.retain_capped path=storage/debugbar/<id>.json bytes=98669
... 5451 x "parallel.extract.file.start", only 24 x "parallel.extract.file.done" ...

kernel: Memory cgroup out of memory: Killed process (codebase-memory)
        total-vm:38258628kB, anon-rss:25088216kB
systemd: run-*.scope: Consumed 5min 28.032s CPU time over 25.717s wall clock time, 24G memory peak.

CBM_MEM_BUDGET_MB=4096 was set explicitly in one run and accepted (mem.init budget_mb=4096 source=CBM_MEM_BUDGET_MB); peak RSS was unaffected.

Diagnostics trajectory

Not captured — the process dies within ~10-26 s of the extract phase, and CBM_DIAGNOSTICS output is owned by the daemon that goes down with it. Happy to run it if you tell me how you want it captured across the kill; the synthetic repro above reproduces the whole thing in under a minute.

Project scale

Synthetic repro: 5,000 ignored files, 1 source file → 1,020,008 nodes / 1,020,006 edges (flat variant) or OOM (nested variant).
Real repo: 6,682 git-visible files, 25,638 discovered; 32,108 nodes / 174,369 edges once storage/ is excluded.

Confirmations

Activity

  1. added
    bugSomething isn't working
    priority/highNeeds near-term maintainer attention; high-impact bug, regression, safety issue, or release blocker.
    on Sep 5, 2026
  2. added a commit that references this issue on Sep 8, 2026
  3. DeusData commented on Sep 8, 2026

    @DeusData
    Owner

    Fixed on main by b5185d1 (PR #2095): only the shallowest nested .gitignore on a path was loaded, so an empty storage/.gitignore disabled a deeper storage/dump/.gitignore and the ignored tree was discovered and indexed (the OOM you hit). Every nested .gitignore is now chained root→leaf with git precedence. Ships in v0.10.9. One behaviour change worth knowing: a nested negation now re-includes what the root .gitignore ignores, matching git. Thank you for the synthetic repro — it made this deterministic.

  4. DeusData commented on Sep 9, 2026

    @DeusData
    Owner

    The OOM half of this is addressed on main by #2112 (merge 1d66538): an over-budget index attempt now fails whole with a named error instead of being OOM-killed. Leaving this open for the remaining (non-OOM) aspects.

  5. DeusData commented on Sep 26, 2026

    @DeusData
    Owner

    Thank you for the fully synthetic repro and for separating the three defects. It made every one of them deterministic!

    All three are on main and shipped in v0.11.0:

    1. d3eb7e9 (PR fix(discover): stack every nested .gitignore on a path, not just the shallowest (#1973) #2095) stacks every nested .gitignore from root to leaf with git's precedence, so an empty storage/.gitignore no longer disables storage/dump/.gitignore. On a reduced copy of your layout, only app/x.php is indexed.
    2. 699be6c (PR fix(pipeline,mcp,daemon): fail an over-budget index attempt whole with a named reason (#1997, #832) #2112) makes an index that can't stay within the memory budget fail as a whole. It no longer overshoots into an OOM kill.
    3. That failure now reaches the caller as a named error instead of a bare "daemon-backed CLI execution failed".

    Could you re-run your repro (and the Laravel repo, if convenient) on v0.11.0 and confirm? If anything is still missing, tell us which part and we'll keep it open for that.

  6. added
    awaiting-reporterWaiting on the reporter for info/repro; stale bot will warn then close
    on Sep 26, 2026
  7. github-actions commented on Sep 26, 2026

    @github-actions

    Thanks for the report! To move this forward we need a bit more so we can reproduce it ourselves:

    • the codebase-memory-mcp --version you're on
    • the exact steps or command you ran
    • a public repo (or a small dummy snippet) that shows the problem — please don't paste proprietary code

    Once that's here we'll pick it straight back up. Heads-up: issues left awaiting-reporter are automatically closed after a few weeks of silence, but a comment reopens the door anytime.

  8. github-actions commented on Oct 21, 2026

    @github-actions

    This issue has been waiting on more information for 21 days (a version, exact steps, or a public repro), so it's now marked stale. It will not be closed automatically — a maintainer reviews stale issues by hand. Adding a comment with the details clears the label and puts it straight back in the queue. We're happy to pick it back up the moment we can reproduce it.

  9. added
    staleNo activity after an info request; will be closed soon
    on Oct 21, 2026
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

    awaiting-reporterWaiting on the reporter for info/repro; stale bot will warn then closebugSomething isn't workingpriority/highNeeds near-term maintainer attention; high-impact bug, regression, safety issue, or release blocker.stability/performanceServer crashes, OOM, hangs, high CPU/memorystaleNo activity after an info request; will be closed soonux/behaviorDisplay bugs, docs, adoption UX

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions