Parent-Process Watchdog Mechanism in codebase-memory-mcp: Graceful Shutdown Explained

The codebase-memory-mcp server implements a parent-process watchdog that polls getppid() every 500 milliseconds to detect when its parent process exits, triggering immediate cleanup via _exit(0) to prevent orphaned processes.

The codebase-memory-mcp repository provides a long-lived stdio-based JSON-RPC server for indexing and querying codebases. When the launching process—such as an editor or supervisor—terminates unexpectedly, the server would otherwise remain blocked on stdin indefinitely, leaking resources. To prevent this, the project implements a parent-process watchdog that monitors the parent PID and initiates a graceful shutdown as soon as the parent disappears.

Why a Parent-Process Watchdog Is Necessary

As a stdio-based MCP (Model Context Protocol) server, codebase-memory-mcp relies on standard input for JSON-RPC messages. If the parent process dies while the server is blocked on a getline or read operation, the server process would persist as an orphan, consuming memory and file descriptors without a functional supervisor. This resource leak is particularly problematic in editor environments where extensions may restart or crash frequently.

The solution implements a POSIX-specific watchdog thread that compares the current parent process ID (getppid()) against the PID captured at startup, detecting orphaning when the process is re-parented to PID 1.

Core Architecture of the Watchdog System

The graceful shutdown system consists of coordinated signal handling, a centralized shutdown routine, and a background watchdog thread defined in src/main.c.

Signal Handling and the Unified Shutdown Routine

External termination signals converge on a single cleanup path. In src/main.c at lines 72–81, the application installs handlers for SIGTERM and SIGINT that invoke request_shutdown():

// src/main.c L72-L81
static void signal_handler(int sig) {
    (void)sig;
    request_shutdown();
}

The request_shutdown() function (lines 77–100) acts as the central coordinator for all shutdown scenarios. It atomically marks the global shutdown flag, cancels active pipelines, stops background services, and unblocks the main loop by closing stdin:

static void request_shutdown(void) {
    if (atomic_exchange(&g_shutdown, 1)) return; /* already shutting down */

    if (g_server) {
        cbm_pipeline_t *p = cbm_mcp_server_active_pipeline(g_server);
        if (p) cbm_pipeline_cancel(p);
    }
    cbm_pipeline_unlock();

    if (g_watcher)   cbm_watcher_stop(g_watcher);
    if (g_http_server) cbm_http_server_stop(g_http_server);

    (void)fclose(stdin);   /* unblock the MCP read loop */
}

The Background Watchdog Thread

The parent-process watchdog runs in a dedicated thread spawned early in main() at lines 699–709. Using the platform abstraction layer (foundation/compat_thread.h), the thread is created with a minimal stack size (PARENT_WATCHDOG_STACK_SIZE):

// Excerpt from src/main.c L699-L709
bool parent_watchdog_started = false;
if (cbm_thread_create(&parent_watchdog_tid,
                      PARENT_WATCHDOG_STACK_SIZE,
                      parent_watchdog_thread,
                      &initial_ppid) == 0) {
    parent_watchdog_started = true;
    cbm_log_info("parent.watchdog.start");
} else {
    cbm_log_warn("parent.watchdog.unavailable",
                 "reason", "thread_create_failed");
}

The watchdog thread implementation (lines 107–136) polls every 500 milliseconds via cbm_usleep(500000). It checks if the parent PID has changed from the initial value captured at startup:

static void *parent_watchdog_thread(void *arg) {
    pid_t initial_ppid = *(pid_t *)arg;
    const unsigned int poll_interval_us = 500000; /* 500 ms */

    while (!atomic_load(&g_shutdown)) {
        cbm_usleep(poll_interval_us);
        if (atomic_load(&g_shutdown)) break;
        if (initial_ppid > 1 && getppid() != initial_ppid) {
            static const char msg[] =
                "level=warn msg=parent.exited reason=ppid_changed\n";
            (void)write(STDERR_FILENO, msg, sizeof(msg) - 1);
            _exit(0);
        }
    }
    return NULL;
}

When the parent exits, the kernel re-parents the server to PID 1 (init). The watchdog detects this change and calls _exit(0) immediately, avoiding async-signal-unsafe cleanup that could deadlock.

Worker Mode Integration

The same mechanism protects index-worker subprocesses (lines 59–70). When running with the --index-worker flag, the watchdog ensures worker processes also terminate if their supervising parent dies, preventing stale workers from accumulating during large indexing operations.

Step-by-Step Detection Logic

The parent-process watchdog follows this deterministic sequence:

  1. Startup Capture: main() records the initial parent PID using initial_ppid = getppid();.
  2. Thread Launch: cbm_thread_create() spawns parent_watchdog_thread before the server begins accepting connections.
  3. Polling Loop: The thread sleeps for 500ms, then checks atomic_load(&g_shutdown) to see if a normal shutdown is already in progress.
  4. PPID Validation: If initial_ppid > 1 and getppid() no longer matches the initial value, the watchdog detects orphaning.
  5. Immediate Termination: The thread writes a warning to stderr and invokes _exit(0), bypassing standard library cleanup to ensure the process exits even if signal handlers are unsafe.

Platform Support and Limitations

The parent-process watchdog is implemented only for POSIX platforms (#ifndef _WIN32). Windows builds rely on job-object termination semantics, which provide equivalent functionality through native OS mechanisms. The POSIX implementation specifically requires getppid() and _exit(), available on Linux, macOS, and other Unix-like systems.

Summary

  • Resource Safety: The watchdog prevents orphaned server processes that would otherwise persist indefinitely blocked on stdin.
  • Polling Mechanism: It checks getppid() every 500ms against the startup PID to detect when the parent process exits.
  • Dual Path Shutdown: Both signal handlers (SIGTERM/SIGINT) and the watchdog converge on request_shutdown() for consistent cleanup.
  • Safe Termination: Uses _exit(0) instead of exit() to avoid deadlocks in async-signal-unsafe states.
  • Cross-Platform: Implemented for POSIX only; Windows uses alternative job-object mechanisms.

Frequently Asked Questions

How does the watchdog detect parent process death?

The watchdog stores the initial parent PID at startup and compares it with the current getppid() every 500 milliseconds. When the original parent exits, the operating system re-parents the process to PID 1 (init), causing the comparison to fail and triggering _exit(0).

What is the difference between request_shutdown() and _exit()?

request_shutdown() is the graceful path used during normal shutdowns (signals or manual requests) that cancels pipelines, stops services, and closes stdin. _exit() is used by the watchdog only when detecting parent death, as it terminates immediately without invoking cleanup routines that could deadlock in an async-signal-unsafe context.

Why does the watchdog use a 500ms polling interval?

The 500ms interval (500,000 microseconds) balances responsiveness with CPU efficiency. It detects parent death quickly enough to prevent resource leaks while minimizing overhead from the getppid() system call and sleep operations.

Is the parent-process watchdog available on Windows?

No. The watchdog is conditional on #ifndef _WIN32 because Windows provides job-object termination semantics that automatically kill child processes when the parent dies. The POSIX-specific getppid() and _exit() mechanisms are not required on Windows.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →