Repository navigation
Description
Version
codebase-memory-mcp 0.10.8
Platform
Windows (x64)
Install channel
GitHub release archive / install.sh / install.ps1
Binary variant
standard
What happened, and what did you expect?
A codebase-memory-mcp stdio client process consumes 55-86% of one CPU core while completely idle, from the moment it starts, and never settles. Expected: ~0%, which is exactly what 0.9.0 does on the same machine.
This is not agent-specific and needs no MCP traffic to reproduce: spawning the binary from a plain shell and never sending a single JSON-RPC message already burns a core. The daemon itself is idle (1.2% of one core); the whole cost is in the client processes.
Because each editor/agent session spawns its own client, the cost multiplies. Snapshot from this machine with 5 client processes alive (one per open Claude Code session) plus the shared daemon:
PID % of one core RSS
4024 69.5 12.9 MB
37336 67.7 12.2 MB
24208 65.3 13.7 MB
21848 63.9 13.6 MB
10784 47.3 10.4 MB
20812 1.2 6.2 MB <- the daemon, idle as expected
sum: 3.15 cores = 26% of a 12-thread CPU, permanently, doing nothing
RAM is fine (~13 MB per client). This is purely CPU.
Reproduction
No agent, no editor, no handshake required.
- On Windows, spawn the server with stdin held open as a pipe and write nothing to it:
// node spin.js
const { spawn, execSync } = require('child_process');
const p = spawn("C:/path/to/codebase-memory-mcp.exe", [], { stdio: ['pipe','pipe','pipe'] });
p.stdout.on('data',()=>{}); p.stderr.on('data',()=>{});
const cpu = () => parseFloat(execSync(
'powershell -NoProfile -Command "(Get-Process -Id ' + p.pid + ').TotalProcessorTime.TotalMilliseconds"'
).toString().trim());
setTimeout(() => { const a = cpu(), t = Date.now();
setTimeout(() => { const b = cpu();
console.log(((b-a)/(Date.now()-t)*100).toFixed(1) + '% of one core'); p.kill(); }, 10000);
}, 3000);- Run it against 0.9.0 and against 0.10.8.
Metric: delta of TotalProcessorTime over a 10 s wall-clock window, expressed as a percentage of a single core. Measurement starts 3 s after spawn so startup is excluded.
A/B, same machine, alternated runs
Real user profile, 9 indexed projects:
| Version | run 1 | run 2 | run 3 |
|---|---|---|---|
| 0.9.0 | 0.0% | 0.0% | 0.1% |
| 0.10.8 | 70.2% | 76.4% | 85.9% |
Version bisect
Each version run with an isolated LOCALAPPDATA / APPDATA / USERPROFILE / HOME, so every binary got a private daemon endpoint and an empty state directory with zero indexed projects:
| Version | % of one core |
|---|---|
| 0.9.0 | 0.0 / 0.0 / 0.1 |
| 0.9.1-rc.1 | 27.0 / 29.5 / 39.1 |
| 0.10.0 | 26.8 |
| 0.10.1 | 17.3 |
| 0.10.2 | 20.0 |
| 0.10.4 | 34.6 |
| 0.10.6 | 30.4 |
| 0.10.8 | 22.7 |
The regression boundary is exactly v0.9.0 → v0.9.1-rc.1. Absolute numbers are lower in this table than in the A/B above because these runs had no indexed projects; the spin happens regardless.
Logs
Nothing. The loop is completely silent. Measured simultaneously over one 12 s window with CBM_LOG_LEVEL=trace set: 66.8% of one core, and 0 bytes on stderr (0 on stdout too).
Diagnostics trajectory (memory / performance / leak issues)
Thread-level breakdown
One client, alive 24 minutes, still spinning. Two threads carry all of it:
thread 14032 30.5% Running
thread 22428 29.0% Wait / ExecutionDelay
thread 14616 0.0% Wait / Executive
A freshly spawned client shows the same shape: 6 threads, two of them at 50.3% and 54.1%.
Ruled out (each one measured, not assumed)
- Number of concurrent sessions — a single client, alone, spawned from a plain shell, spins.
- MCP traffic — spins before any
initialize; 62.1% with no handshake at all vs 55.0% after one. - The daemon —
daemon statusreports all clients committed; the daemon uses 1.2% of a core. auto_watch— no change with it set tofalse(47-64% either way). Note the watcher thread is created unconditionally insrc/main.c(if (g_watcher)), so the config key does not gate it.auto_index—falsethroughout (this is what distinguishes it from CPU memory usage is too high #1084).- Indexed projects — the isolated-home runs above have zero projects and still spin.
ui_enabled— 64.4% with--ui=false.--tool-profile=scout— 70.0%.- Warm-up — the thread breakdown above was taken on a client that had been alive for 24 minutes; it was still carrying 59.5% of a core across its two hot threads.
- The parent-death watchdog — excluded on Windows (
#ifndef _WIN32) and polls at 500 ms. - The watcher's own loop —
POLL_BASE_MS 5000chunked bySLEEP_CHUNK_MS 500is 10 wakeups per cycle; far too cheap to account for this.
Where I would look (hypothesis, NOT verified)
I could not attribute the CPU to a specific line — the shipped binary is stripped and the loop logs nothing — so please treat this section as a starting point rather than a diagnosis.
The regression window v0.9.0..v0.9.1-rc.1 is 486 commits and contains 0e00ef57 ("feat: coordinate concurrent CBM sessions" — same title as the closed issue #1139, though the commit message itself carries no issue reference), which introduces all of src/daemon/, including src/daemon/frontend.c. That file is the client side: frontend.h describes it as "Stateless stdio bridge for a daemon-backed MCP session", and src/main.c:2946 calls cbm_daemon_frontend_mcp_run(..., stdin, stdout) in the client process. So in this window the client's stdio path went from having no daemon bridge at all to having one built on poll loops:
FRONTEND_WAIT_US = 1000(1 ms) — 3 call sites, including the enqueue path and the join watchdogFRONTEND_IDLE_WAIT_US = 10 * 1000(10 ms) — the idle path offrontend_workerFRONTEND_MAINTENANCE_POLL_MS = 10—frontend_workercallsfrontend_exit_for_maintenance()100 times per second, which callscbm_version_cohort_maintenance_presence_terminal()
FRONTEND_IDLE_WAIT_US did not exist in 0e00ef57; it was added later, which may explain why the figure varies across 0.10.x instead of being flat.
Secondary observation, possibly unrelated
On Windows cbm_usleep is integer division:
#define cbm_usleep(us) Sleep((DWORD)((us) / 1000)) // src/foundation/compat.h:33so any argument below 1000 µs becomes Sleep(0), which does not sleep. All the named poll constants are >= 1000 µs, but version_cohort_retry_sleep() (src/daemon/version_cohort.c:93) passes VERSION_COHORT_RETRY_US / 2U + (mix % VERSION_COHORT_RETRY_US) = 500-1499 µs, so roughly half of its jitter range degrades to Sleep(0) on Windows. I do not think this is the driver here (a single isolated client has no lock contention and still spins), but it looked worth flagging.
Not a duplicate of
- Very high CPU usage #45 "Very high CPU usage" — March 2026, macOS, four months before
src/daemon/existed. - CPU memory usage is too high #1084 "CPU memory usage is too high" — Windows, but triggered by turning auto-indexing on; here
auto_indexisfalse, and 0.9.0 measures 0.0% on the same configuration. - cbm_nanosleep never resumes after EINTR: every timed wait can be silently truncated #1616
cbm_nanosleepEINTR — POSIX only, and none of its four call sites isfrontend.c.
I also searched issues and PRs for frontend.c, FRONTEND_WAIT_US, FRONTEND_IDLE_WAIT_US, "stdio bridge", "idle cpu" and version_cohort and found nothing covering this. No commit on main after v0.10.8 touches frontend.c, version_cohort.c or main.c, and the three constants are unchanged on main, so this should still be live on tip.
Environment
- Windows 11 Business 10.0.26200 (x64)
- 12th Gen Intel Core i7-1255U — 10 physical / 12 logical cores
- 23.7 GB RAM
- The installed 0.10.8 binary is byte-identical to the
codebase-memory-mcp-windows-amd64.ziprelease artifact — SHA256b4b403b1d7c4def3785f148b93f345ce8427858f4f5489ce28580c4387a336a6, matching the build id the daemon reports. Every version in the bisect came from that samewindows-amd64release family, so no build-variant difference is involved. (Note that in 0.10.8 this "standard" artifact does serve the graph UI on 9749.) - The 0.9.0 binary came from the installer's own backup,
%LOCALAPPDATA%\Programs\codebase-memory-mcp\codebase-memory-mcp.exe.old
Project scale (if relevant)
9 indexed projects on the affected machine. Also reproduces with 0 indexed projects (see bisect table), so scale is not a factor.
Confirmations
- I searched existing issues and this is not a duplicate.
- My reproduction uses shareable code (a dummy snippet or a public OSS repository), not proprietary code.
Activity
DeusData commentedon Aug 20, 2026
Thanks for the unusually thorough, reproducible report and version bisect — the per-thread data was especially useful. We checked current main (
dfe67cc7): the daemon frontend currently has two independent 10 ms maintenance observers, and each observation performs the full secure version-cohort file-lock path on Windows. That is a concrete source-level match for the two hot client threads you measured, and these paths remain unchanged on tip.We are treating this as a current regression. The next gate is a deterministic RED on our real Windows runner, followed by a minimal change that preserves update/maintenance fail-stop behavior and a RED-on-revert check. Our local Windows VM is currently unavailable, so I do not want to claim a runtime reproduction or fix before that gate. No more information is needed from you right now; thank you again for the excellent diagnostics.
detrax commentedon Aug 28, 2026
Chiming in with a macOS data point, since the bisect and per-thread breakdown above are Windows-only: this reproduces on macOS too (Intel MacBook Pro, Darwin 25.5.0, 0.10.8 from the release archive), just with a much smaller absolute burn.
Idle clients here sit at roughly 4% of a core each. That sounds harmless compared to the 70% above, but it adds up quietly — I noticed it because I had 11 client processes alive (one per open Claude Code terminal), and the oldest ones had accumulated 128 and 170 CPU-minutes doing literally nothing. Daemon idle at ~0.5%, same picture as your snapshot.
The nice thing about macOS is that
samplecan attribute the time even though the binary is stripped. 5-second sample of one idle client, the busy worker thread (4309 samples total, so 138 ≈ 3.2% of a core — which matches whatpsshows for the whole process):So essentially all of the on-CPU time is filesystem work: an lstat walk plus fstat/acl_get_fd_np, i.e. the secure lock-path validation you described, done ~100×/s per observer. The sleep/wake overhead itself is nearly free here — which would explain the platform gap: on macOS these syscalls are cheap, on Windows the equivalent security checks presumably cost an order of magnitude more, but the loop structure is identical.
One observation on top of #1845: deduplicating the two observers should halve this, but the remaining observer still runs the full lock validation at 10 ms cadence. Given that FRONTEND_MAINTENANCE_GRACE_MS exists anyway, is there a reason maintenance detection needs to react within 10 ms? Even 250–500 ms would cut the remaining burn by 25–50× without meaningfully delaying an update quiesce.
Happy to test a macOS build of #1845 (or a cadence tweak) against the same setup — the 11-client scenario is my daily driver, so regressions would show up fast.
PabloSoage commentedon Aug 28, 2026
Windows counterpart to @detrax's macOS attribution, since that was the piece missing on this side.
I can't get a symbolised stack on Windows (stripped binary, no
sampleequivalent), but the kernel/user split and the per-process I/O counters say the same thing. Idle client, no handshake, sampled over 10 s withWin32_Process(UserModeTime/KernelModeTime, andOtherOperationCount— I/O operations that are neither reads nor writes, i.e. metadata and control):Three runs, alternated, same machine as the original report.
Two things follow:
The burn is not sleep/wake overhead or userspace spinning — ~92% of it is syscall time, with ~20k metadata operations per second and essentially zero reads or writes. That matches the
lstatwalk +fstat/acl_get_fd_npprofile @detrax captured, so the loop structure really does look identical across platforms.I read the source to check the "two independent 10 ms observers" against the counters, and it lines up exactly — including a detail worth spelling out. Both observers poll the same
cbm_version_cohort_manager_t:cbm_daemon_frontend_mcp_runstores it as.cohort_manager = cohort_manager(src/daemon/frontend.c:581) and then hands the very same pointer tocbm_daemon_maintenance_monitor_start(:586-587). Sofrontend_maintenance_monitor_worker(thread at:159) andfrontend_worker(thread at:594) each callcbm_version_cohort_maintenance_presence_terminal()on the same object everyFRONTEND_MAINTENANCE_POLL_MS = 10— the monitor via its owncbm_usleepat:106/:126, the worker vianext_maintenance_check_msat:422. Two hot threads, which is exactly what the per-thread breakdown showed. Identical atdfe67cc7and at currentmain(2cc80f20).At 200 observations/s that puts ~20,000 ops/s at roughly 100 filesystem metadata operations per observation, which is plausible given what each one does:
version_cohort_maintenance_presence_internalperforms a fullcbm_private_file_lock_try_acquire/release cycle onVERSION_COHORT_MAINTENANCE_FILE, andprivate_file_lock.crevalidates on every acquire (fstat+lstat+ inode comparison + ACL checks, and theSECURITY_DESCRIPTOR/DACL path on Windows). On macOS those syscalls are cheap; on Windows the security-descriptor work is not, which would account for the ~4% vs ~60% gap between the two reports rather than any difference in cadence.For anyone else hitting this before the fix lands: downgrading to 0.9.0 is viable and does not require reindexing — the 0.10.8 index stays fully readable (all 9 of my projects report
readywith their node/edge counts intact, and content queries return real results). One caveat: under 0.9.0list_projectsonly enumerated 3 of my 9 projects, deterministically and with no error, though every one of the missing six answers normally when named explicitly. That's a 0.9.0-side wart, not index damage.Happy to run any Windows build against the same setup — the counters above are a two-minute check, so a before/after on #1845 is easy to produce.
DeusData commentedon Aug 29, 2026
Fixed on main in
57435634(PR #1845, "fix(daemon): avoid duplicate idle maintenance polling").Thank you for this one — the diagnosis in this thread did the hard part. @detrax, the macOS data point mattered specifically because the original bisect and per-thread breakdown were Windows-only, so it established the behaviour was not platform-specific. @PabloSoage, the kernel/user split from the Windows side was the complementary half; between them the shape was pinned well before anyone read the scheduling code.
An idle client should not cost measurable CPU, and a regression that only shows up as "warm laptop" is easy to live with and hard to attribute — reporting it with a bisect rather than an impression is what made it actionable.
Tomenatore commentedon Sep 1, 2026
Still reproducing on v0.10.8 (Windows 11 23H2, build 22631), and there is no release containing the fix yet: v0.10.8 shipped 2026-08-19, while #1845 merged 2026-08-27 and this issue closed 2026-08-28.
Measurements from a production workstation (i9-13900HX, 32 logical CPUs, 64 GB RAM):
Per idle client:
Scaling: this host runs Hermes, which registers the MCP server per profile. With 4 concurrent instances:
Second-order effects, all measured before and after stopping the instances:
Configuration had no effect: with both
auto_watch=falseandui_enabled=false, a freshly spawned front-end still spins at 18,783 ops/s. So this is not the file watcher and not the UI server.Workaround in place: limit the MCP server to a single profile, and drive indexing from a scheduled task calling
cli index_repositorybehind a git-fingerprint gate.Would it be possible to cut a 0.10.9 containing #1837 and #1845? v0.10.8 is currently the only version
install.ps1resolves to, and on Windows it costs roughly one core per concurrent session.TaiwanTammy commentedon Sep 8, 2026
Adding a v0.10.6 data point, plus a nudge on the release: @Tomenatore's "no release contains the fix yet" note is eight days old now and still true.
0.10.6 is affected, so downgrading below 0.10.7 is not an escape
Windows 11 Home 26200, x64, 0.10.6 from the release archive,
auto_watch = false. Four live clients (three Claude Code sessions + one Codex session) plus the shared daemon, sampled over 10 s withWin32_PerfRawData_PerfProc_Process:Same shape as the original report and as @PabloSoage's table: zero bytes moved, five figures of "other" ops per second, and the daemon itself innocent.
Clean-room check, since none of these clients had been asked to do anything: spawning the binary over a plain pipe from a Node script, no JSON-RPC sent, no project attached, 78% of a core within six seconds of launch.
The accumulated number is the one that's easy to miss. Client A had been sitting in an open, idle window since the previous lunchtime and had burned 6 h 54 m of CPU time having done no work at all.
Why the release specifically
v0.10.9 is also carrying the
auto_watchreindex-loop fix (6741c29fc24d, #1953). On Windows today that means both paths cost a core: leave the watcher on and it reindexes itself forever, turn it off and every idle client still spins. The only way out of either is a tag that has not shipped, so the two fixes are compounding behind one release rather than one.Anything that gets 0.10.9 out would help, including a pre-release. Happy to test a Windows 11 build against both symptoms and report back if that is useful.