Repository navigation
Windows: killing one endpoint's process takes down the shared daemon and every attached connection ("Connection closed") #2796
Description
Version
codebase-memory-mcp 0.12.1
Platform
Windows x64 (10.0.26300)
Install channel
GitHub release archive (windows-amd64.zip)
Binary variant
standard
What happened, and what did you expect?
Short version: when an MCP client kills one of its codebase-memory-mcp.exe child processes (e.g. an idle-connection eviction sweep), the shared daemon dies with it, and every other endpoint exe that was attached to that daemon exits with code 1. The next tool call on any surviving pooled connection fails with Connection closed. Reconnect heals it, but every in-flight/idle connection breaks at once.
Observed with the ZCode desktop client (Electron), whose MCP pool keeps several stdio connections per server and evicts idle ones ~30s after their lease refcount drops to 0 — the eviction path terminates the child process. Two failures on the first evening of multi-session usage, both with the identical signature.
Topology observed (v0.12.1, Windows):
- Each stdio spawn is a thin endpoint; endpoints share one detached daemon that listens on
127.0.0.1:9749(default UI port) and owns the cache/index. - Daemon lifetime is managed by a "last committed client disconnected" heuristic (
runtime_stopping→lifetime_end).
Evidence from both sides (same incident):
- Client pool log: victim connection idle-but-leased; death discovered at next
tools/call→ instantconnection_lost→Connection closed. In both incidents the victim died 4s / 20s after a sibling pooled connection was closed by the pool. - Server
cbm-daemon.log: zero entries for the victim endpoints' death — noruntime_stopping, nolifetime_end, no error. They vanished without running any graceful path (consistent with external termination). The failed tool calls never reached a handler (notools/callline).
Reproduction
Deterministic lab repro (Node, Windows — two pooled endpoints + kill the daemon):
// A and B = two spawned `codebase-memory-mcp --tool-profile=analysis` stdio endpoints
// (initialize handshake done on both; a tools/call completed on each)
// daemon = the process listening on 127.0.0.1:9749 (spawned by A's launch)
killProcess(daemonPid) // simulate a client sweep killing a child
// → B's exe exits with code 1 immediately
// → B's next JSON-RPC request hits EOF → client reports "Connection closed"Control: closing endpoint A's stdin cleanly leaves the daemon and B unharmed (daemon survives, B keeps serving). Only abrupt process termination of the daemon cascades.
Full repro script and analysis artifacts: I can attach on request (client-side pool events with traceIds, server log timeline, process-table correlation).
Logs
Server-side (C:/cbm-cache/logs/cbm-daemon.log) around an incident — note the absence of any shutdown trail for the victim:
level=info msg=mcp.request protocol=jsonrpc method=tools/call tool=list_projects status=ok duration_ms=1678
level=info msg=mcp.request protocol=jsonrpc method=tools/call tool=index_status status=ok duration_ms=109
... 15 minutes of no server-side activity (client idle) ...
[victim endpoint dies: NO log line at all — no runtime_stopping, no lifetime_end]
level=info msg=mcp.request protocol=jsonrpc method=tools/call tool=get_architecture status=ok duration_ms=1219 <- after client reconnect
Client-side pool events (ZCode zcode-<date>.jsonl), same incident:
12:06:25Z info mcp.pool.lease... (idle begins)
12:20:14Z info mcp.pool.lease.released (sibling connection, ref=0)
12:20:45Z info mcp.server.closed (sibling, status=completed)
12:21:05Z info tool.call.started <- my next call on the victim connection
12:21:05Z warn mcp.server.connection_lost status=failed
12:21:05Z error tool.call.failed "Connection closed"
Diagnosis and suggested fix directions
Root cause chain: the daemon is spawned (detached) by an endpoint, so on Windows it remains inside the spawning client's job/process tree; a client that reaps its children with a kill sweep takes down the shared daemon that other, still-live connections depend on. Endpoints treat daemon loss as fatal (exit 1) rather than re-attaching.
Two candidate fixes (either one breaks the cascade; both is best):
- Escape the sweep (Windows, minimal): spawn the daemon with
CREATE_BREAKAWAY_FROM_JOB/ assign it to its own job object, so a client-side kill-on-close sweep of its tree cannot reach a daemon that serves multiple clients. - Endpoint resilience: on daemon connection loss, attempt to spawn/re-attach to a daemon before giving up, instead of exiting.
Workaround currently deployed
A "keeper" process outside the client's tree holds an initialized MCP connection, so the daemon is owned by the keeper and client sweeps can't reach it. Works reliably; happy to share as a script if useful.
Diagnostics trajectory (memory / performance / leak issues)
Not a memory/leak issue; lifecycle only. Side observation: killed endpoints leak as orphaned codebase-memory-mcp.exe processes (several MB each) — related to #2400's "multiple exe" report, but the cascade above is the core defect.
Activity
DeusData commentedon Oct 25, 2026
Thank you for the process timeline and clean-stdin control. One distinction matters: the supplied lab reproduction kills the daemon directly, while the reported incident starts with an endpoint being evicted. Could you share a small sanitized reproduction that kills only endpoint A and records the daemon and endpoint B outcomes, plus whether the client uses a process-tree/job kill? That would separate the trigger from the resulting disconnect cascade.
github-actions commentedon Oct 25, 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.Minervaowl7 commentedon Oct 26, 2026
A fix is up for review: #2803.
Design note after digging into the frontend failure path: the immediate-exit behavior is deliberate — the worker calls
_Exit(EXIT_FAILURE)precisely because a failed daemon transport cannot portably wake the thread blocked in stdio. So the fix does not try to wake the reader; it recovers on the worker: on exchange failure it swaps in a replacement client supplied by a newdaemon_lostcallback frommain(), re-mints the request token, and re-issues the very request whose exchange was interrupted (bounded to 3 attempts). The client never notices.Verified end-to-end on Linux/WSL with a real daemon: kill -9 the daemon mid-session, the next request is served by a re-spawned daemon; pre-fix the same scenario exits 1 and loses the request. Details and scope notes (mid-reconnect cancellation is dropped; UI flags not re-applied) are in the PR.
fix(daemon): re-issue the interrupted request against a replacement d…
fix(daemon): re-issue the interrupted request against a replacement d…
fix(daemon): re-issue the interrupted request against a replacement d…
fix(daemon): re-issue the interrupted request against a replacement d…
fix(daemon): re-issue the interrupted request against a replacement d…
fix(daemon): re-issue the interrupted request against a replacement d…
fix(daemon): re-issue the interrupted request against a replacement d…
fix(daemon): re-issue the interrupted request against a replacement d…
Minervaowl7 commentedon Oct 26, 2026
Quick follow-up on the trigger question, and thanks for separating it from the disconnect cascade.
Current state: the lab repro we have kills the daemon directly, so it reproduces the cascade but not the eviction trigger. The incident evidence only shows endpoint A disappearing without a graceful shutdown line; we have not yet isolated "kill only endpoint A" or confirmed whether ZCode's eviction is a process-tree/job kill rather than a single-process termination.
I can run both variants on Windows — A only, and A's tree — with two live endpoints, and record the daemon PID/outcome plus endpoint B's response. The keeper workaround is currently holding the daemon, so I'd need to stop it for a clean run; after that I can post the sanitized script and the process-tree evidence here. I've also replied on #2803 with the current review state.