Repository navigation
Description
Version
codebase-memory-mcp 0.9.0 (current latest release, published 2026-07-08)
Platform
Linux x86_64, 31 GB RAM
Install channel
GitHub release archive (portable tarball, run directly — no installer)
Summary
Two things:
- Regression / verification failure. Indexer OOM-killed (exit 137): infinite loop in PHP trait flattening when a class shares its short name with an aliased trait import #951 was closed 2026-07-09 with "The fix itself shipped with fix(lsp/php): prevent OOM when a PHP trait uses itself #920's guards in v0.9.0". Running the v0.9.0 release binary, Indexer OOM-killed (exit 137): infinite loop in PHP trait flattening when a class shares its short name with an aliased trait import #951's exact shape still hangs with unbounded memory growth in the
definitionspass. - A new, distinct trigger shape. The collision does not have to be with the composed trait's own short name. It also fires when the class's short name equals an intermediate namespace segment in the composed trait's import path. This shape is not covered by PHP: trait whose name collides with an aliased
use … asimport it composes → unbounded recursion indefinitionspass, OOM (SIGKILL) #765, Indexer OOM-killed (exit 137): infinite loop in PHP trait flattening when a class shares its short name with an aliased trait import #951, or fix(lsp/php): prevent OOM when a PHP trait uses itself #920, and it is very common in Laravel apps.
Both stop at the same place — the log's last line is always pass.start pass=definitions — and both are independent of CBM_LSP_DISABLED / CBM_DISABLE_LSP_CROSS.
Reproduction
Shape B — new: class name == a namespace segment of a composed trait (hangs)
<?php
namespace App\M;
use App\Behaviours\Address\SaveGeofenceAttributes; // segment "Address" == class name below
class Address {
use SaveGeofenceAttributes;
public function m($x) {}
}codebase-memory-mcp cli index_repository --repo-path /path/to/dir --name probeApp\Behaviours\Address\SaveGeofenceAttributes does not need to exist — an unresolved import is enough.
Ablations (each run under a 4 GB cgroup cap, 20 s timeout):
| Variant | Result |
|---|---|
| as above | ❌ HANG |
namespace segment renamed Address → Addr |
✅ OK |
trait imported but not composed (use SaveGeofenceAttributes; removed) |
✅ OK |
| no method declared | ✅ OK |
method present with empty body {} |
❌ HANG |
composed fully-qualified inline (use \App\Behaviours\Address\SaveGeofenceAttributes;, no import statement) |
✅ OK |
So shape B needs: import statement + composition by short name + at least one method declaration. A method declaration is sufficient; the body may be empty.
Shape A — #951's shape, still hanging on v0.9.0
<?php
namespace App\L;
use App\Traits\Card as CardBase;
class Card {
use CardBase;
public function m($x) { $this->a = $x; }
}❌ HANG on the v0.9.0 release binary.
Full shape-A matrix:
| Variant | Result |
|---|---|
class Card + use \App\Traits\Card; inline + method with body |
❌ HANG |
same, class renamed to CardX |
✅ OK |
same, method body empty {} |
❌ HANG |
| same, no method at all | ✅ OK |
same, composed trait short name differs (\App\Traits\Panel) |
✅ OK |
class Card + unqualified use Card; |
❌ HANG |
import without alias (use App\Traits\Card; + use Card;) |
❌ HANG |
| aliased import (#951's exact shape, above) | ❌ HANG |
Note a correction to #765, which reported that an empty method body avoids the bug: for the class shape that is not true — an empty-bodied method declaration still hangs. Only removing the method entirely avoids it.
Logs
Identical for every hanging variant — the last line is always the start of definitions:
level=info msg=mem.init budget_mb=1024 source=CBM_MEM_BUDGET_MB
level=info msg=worker.watchdog.start
level=info msg=pipeline.discover files=1 elapsed_ms=0
level=info msg=pipeline.route path=full
level=info msg=pass.start pass=structure files=1
level=info msg=pass.done pass=structure nodes=3 edges=2
level=info msg=pass.timing pass=structure elapsed_ms=0
level=info msg=pipeline.mode mode=sequential files=1
level=info msg=pkgmap.scan_repo manifests=0
level=info msg=pkgmap.scan manifests_from_files=0 manifests_from_walk=0 entries=0
level=info msg=pass.start pass=definitions files=1
<no further output — memory climbs to budget_mb, watchdog SIGTERMs, exit 15>
With a larger/default budget the same run reaches OOM-kill territory instead of a watchdog SIGTERM, matching #765/#951's exit 137.
Which knobs do and don't help
| Setting | Effect |
|---|---|
CBM_LSP_DISABLED=1 + CBM_DISABLE_LSP_CROSS=1 |
❌ still hangs (confirms it is upstream of the LSP resolvers) |
CBM_INDEX_SINGLE_THREAD=1 |
❌ still hangs |
CBM_LSP_MAX_WALK_DEPTH=2 |
❌ still hangs |
CBM_INDEX_SUPERVISOR=1 + CBM_INDEX_WORKER_TIMEOUT_S=15 + CBM_INDEX_MAX_RESTARTS=5 |
❌ does not rescue the run — the memory watchdog SIGTERMs the process group before the supervisor can time out, quarantine the file, and restart. No quarantine file is written. It would be very useful if the watchdog handed off to the supervisor here instead of killing the group. |
CBM_WALK_DEFS_MAX=100 |
CBM_WALK_DEFS_MAX has a sharp cliff: ≤200 avoids the hang, ≥300 hangs. But it also caps definitions per file, which destroys the graph. Indexing one 258-file directory:
CBM_WALK_DEFS_MAX=100, LSP on → 534 nodes / 806 edgesCBM_LSP_DISABLED=1, no cap → 2,979 nodes / 7,178 edges
A single model file in that directory legitimately has 374 definitions, so a cap low enough to dodge the loop silently truncates real files.
The only practical workaround remains .cbmignore, which requires knowing the trigger files in advance.
Real-world prevalence
In one Laravel 12 application (23,720 .php files scanned, excluding vendor/), 22 files trigger one of the two shapes: 14 shape A, 8 shape B. Because indexing is all-or-nothing, any one of them fails the entire repo index.
Shape B is the one I'd flag as high-impact, because the pattern is idiomatic Laravel rather than unusual: behaviour/concern traits are commonly namespaced per entity, so
namespace App\Models\Account;
use App\Behaviours\Address\SaveGeofenceAttributes;
class Address extends Model { use SaveGeofenceAttributes; /* ... */ }is an ordinary file. In the app above, the shape-B hits are exactly the Eloquent models named Address, Contract, Route, User, Broadcast, Company, PriceModification. Nothing about them is exotic.
With those 22 files excluded via .cbmignore, the same repo indexes fine with LSP enabled — 53,794 nodes / 222,112 edges / skipped_count: 0 in under 20 s — so this really is a small set of poison files rather than a scale problem.
Suggested fix
Same as #951's suggestion 2, extended: resolve trait use through the file's import table (aliases and full namespace paths), never resolve a composed trait to a class, and never match on a partial path segment. Independently, add a visited-set/depth guard in the trait flattener so any resolution cycle degrades gracefully instead of allocating until the watchdog fires — that guard is what would have caught both shapes regardless of how the name resolution behaves.
Anything else?
I have a small Python detector that finds both shapes in a tree (parses namespace, imports, in-class use, and method presence). Happy to attach it or contribute it as a test-corpus generator if useful.
Activity
DeusData commentedon Aug 10, 2026
Thank you for carrying the original reproduction forward and adding the namespace-segment variant. The current PHP guard covers the exact class-versus-canonical-trait self case, but it does not cover the collision shape described here. I have routed this as a high-priority parsing and stability bug in the immediate train.
DeusData commentedon Sep 26, 2026
Thank you, @MoonFuji, for carrying #951's reproduction forward and for the ablation tables. They made this easy to re-check!
I ran your probes on current main and on the v0.11.0 release (macOS arm64) with a 1 GB budget. Shape B (a class named after a namespace segment of the composed trait) and all four shape-A variants now finish well under a second. The shape-A variants were #951's aliased import, inline fully-qualified, unqualified
use Card;and unaliased import. I haven't pinned down which change fixed it, so I don't want to claim more than that. Could you re-run the same probes on Linux x86_64 with v0.11.0 under your 4 GB cgroup cap? If they pass there too, we'll close this.github-actions commentedon Sep 26, 2026
Thanks for the report! To move this forward we need a bit more so we can reproduce it ourselves:
codebase-memory-mcp --versionyou're onOnce that's here we'll pick it straight back up. Heads-up: issues left
awaiting-reporterare automatically closed after a few weeks of silence, but a comment reopens the door anytime.MoonFuji commentedon Sep 30, 2026
Re-ran on Linux x86_64 with v0.11.0 (portable release binary, checksum verified). Same setup as the original report: 4 GB cgroup cap with
MemorySwapMax=0, 20 s timeout,CBM_MEM_BUDGET_MB=1024.status: indexed: 8 shape A and 6 shape B, including Indexer OOM-killed (exit 137): infinite loop in PHP trait flattening when a class shares its short name with an aliased trait import #951's aliased import and the namespace-segment case. Nodes and edges matchexpected_nodes/expected_edges..cbmignoreexclusions: 66,586 nodes / 402,731 edges,skipped_count: 0, 38 s. The graph holds all 21 trigger files that still exist, with their methods, including the model with 394 definitions.pass.start pass=lsp_cross, notpass=definitions, even withCBM_LSP_DISABLED=1andCBM_DISABLE_LSP_CROSS=1. Shape A stops atdefinitionsas reported.You can close this from my side. Thanks for re-checking.