Skip to content

Windows: killing one endpoint's process takes down the shared daemon and every attached connection ("Connection closed") #2796

Open

Description

@Minervaowl7

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 → instant connection_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 — no runtime_stopping, no lifetime_end, no error. They vanished without running any graceful path (consistent with external termination). The failed tool calls never reached a handler (no tools/call line).

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):

  1. 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.
  2. 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

  1. added
    bugSomething isn't working
    stability/performanceServer crashes, OOM, hangs, high CPU/memory
    awaiting-reporterWaiting on the reporter for info/repro; stale bot will warn then close
    on Oct 25, 2026
  2. DeusData commented on Oct 25, 2026

    @DeusData
    Owner

    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.

  3. github-actions commented on Oct 25, 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.

  4. Minervaowl7 commented on Oct 26, 2026

    @Minervaowl7
    Author

    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 new daemon_lost callback from main(), 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.

  5. Minervaowl7 commented on Oct 26, 2026

    @Minervaowl7
    Author

    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.

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 workingeditor/integrationEditor compatibility and CLI integrationstability/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