Parent-Process Watchdog in Codebase-Memory-MCP: Preventing Orphaned Server Processes

The parent-process watchdog is a lightweight background thread that monitors the server's parent process ID via getppid() and triggers immediate shutdown when the supervising process terminates, ensuring no orphaned MCP server processes persist after job completion.

The parent-process watchdog is a critical reliability mechanism in the DeusData/codebase-memory-mcp repository. It solves the common distributed systems problem where server processes survive as zombies after their launching shell or supervisor dies. By continuously polling the parent PID against the original value captured at startup, the watchdog guarantees that the MCP server exits cleanly alongside its controlling process.

What Is the Parent-Process Watchdog?

The watchdog is a dedicated monitoring thread spawned immediately when the MCP server starts. Unlike signal-based approaches that can miss rapid parent replacements, this implementation uses active polling to detect when the original parent process disappears or is replaced by the init system (PID 1).

Core Architecture

According to the source code in src/main.c, the watchdog thread is created early in the server lifecycle to catch parent death even during initialization. The implementation uses a minimal stack footprint to avoid resource bloat:

  • Stack size: PARENT_WATCHDOG_STACK_SIZE set to 64 * CBM_SZ_1K (64 KB)
  • Polling interval: 100 milliseconds (usleep(100 * 1000))
  • Shutdown flag: Atomic variable g_shutdown synchronized across threads

How the Watchdog Prevents Orphaned Processes

The mechanism relies on POSIX process semantics where orphaned children are re-parented to the init process. The watchdog detects this transition and forces termination before the server becomes a resource-consuming zombie.

Detecting Parent Death via getppid()

In src/watcher/watcher.c, the parent_watchdog_thread() function implements the monitoring loop. It compares the current parent PID against the original_ppid captured at startup:

/* src/watcher/watcher.c – parent monitoring logic */
static void *parent_watchdog_thread(void *arg) {
    pid_t original_ppid = *(pid_t *)arg;
    while (atomic_load(&g_shutdown) == 0) {
        if (getppid() != original_ppid) {
            /* Parent has died or been replaced by init */
            atomic_store(&g_shutdown, 1);
            break;
        }
        usleep(100 * 1000);   /* 100 ms polling interval */
    }
    return NULL;
}

When getppid() returns 1 (init) or any value different from original_ppid, the watchdog knows the supervising process has terminated.

The Shutdown Signal Flow

Once parent death is detected, the watchdog executes a deterministic cleanup sequence:

  1. Sets global flag: atomic_store(&g_shutdown, 1) signals all worker threads to terminate
  2. Breaks polling loop: The watchdog thread exits its monitoring cycle
  3. Main thread joins: In src/main.c, the main thread calls cbm_thread_join(&parent_watchdog_tid) to ensure synchronous cleanup
  4. Resource release: The server closes file descriptors and exits before becoming an orphan

Worker-Mode Safety

The same watchdog mechanism protects worker-mode subprocesses used for indexing operations. As tested in tests/test_worker_watchdog.sh, child worker processes monitor their immediate parent. If the parent worker dies, the child terminates immediately rather than continuing to process jobs that will never be collected.

Implementation in the Codebase-Memory-MCP Source

The watchdog spans three primary files with distinct responsibilities:

  • src/main.c: Creates the watchdog thread via cbm_thread_create() and stores the initial PPID before any forking occurs
  • src/watcher/watcher.c: Contains the parent_watchdog_thread() implementation with the polling logic
  • tests/test_parent_watchdog.sh: Regression test that verifies the server exits within seconds of parent process termination

Thread creation in src/main.c follows this pattern:

/* src/main.c – watchdog startup sequence */
bool parent_watchdog_started = false;
cbm_thread_t parent_watchdog_tid;
pid_t initial_ppid = getppid();  /* Capture before thread starts */

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");
}

/* Later during shutdown... */
if (parent_watchdog_started) {
    cbm_thread_join(&parent_watchdog_tid);
}

Summary

  • The parent-process watchdog is a 64 KB stack thread that polls getppid() every 100 milliseconds to detect supervisor death.
  • It prevents zombie processes by setting the atomic g_shutdown flag when the parent PID changes or becomes init (PID 1).
  • The implementation in src/watcher/watcher.c uses POSIX-compliant process monitoring rather than fragile signal handling.
  • Worker-mode processes reuse the same watchdog logic, ensuring distributed indexing tasks cannot outlive their managing parent.
  • The main thread in src/main.c synchronously joins the watchdog during cleanup, guaranteeing no detached threads persist after server exit.

Frequently Asked Questions

What happens if the parent process crashes suddenly?

The watchdog detects the crash within 100 milliseconds when getppid() returns a different value (typically PID 1 if the parent died). It immediately sets g_shutdown to 1, triggering the server's graceful termination sequence before the process becomes orphaned.

Does the watchdog consume significant CPU or memory?

No. The watchdog thread uses a fixed 64 KB stack (PARENT_WATCHDOG_STACK_SIZE) and sleeps for 100 ms between checks. This polling strategy consumes negligible CPU compared to the I/O-bound MCP server operations, and the memory footprint is smaller than a single file buffer.

Can the watchdog be disabled or configured?

According to the source in src/main.c, watchdog startup is conditional on successful thread creation, but there is no configuration flag to disable it. Failure to create the thread logs a warning (parent.watchdog.unavailable) but allows the server to continue running without protection.

How does this differ from traditional SIGCHLD handling?

Unlike SIGCHLD, which only notifies child processes of parent state changes (and cannot be received by the child about its own parent), the watchdog actively queries the parent relationship via getppid(). This approach works even when the parent is killed with SIGKILL, which cannot be caught or handled by signal handlers in the parent process.

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 →