How Daemon Mode in code-review-graph Enables Background Monitoring and Indexing

The daemon mode in code-review-graph runs a long-lived WatchDaemon process that continuously watches configured repositories, automatically rebuilds graph databases when changes occur, and restarts failed watcher processes to ensure uninterrupted background operation.

The code-review-graph tool provides a complete daemonization system for keeping code review graph databases synchronized with live repositories. According to the tirth8205/code-review-graph source code, the implementation centers on the WatchDaemon class in code_review_graph/daemon.py, which handles everything from Unix double-forking to health-checking child processes.

Daemonization: Detaching from the Terminal

The foundation of background operation is WatchDaemon.daemonize() (lines 923-975). This method implements a classic double-fork pattern on Unix systems: the parent forks once, the first child forks again, and the original parent and first child exit, leaving a properly orphaned grandchild that the init system adopts.


# From code_review_graph/daemon.py

daemon = WatchDaemon()
daemon.daemonize()  # Double-fork, write PID file, redirect streams

The method performs several critical setup steps:

  • PID file creation via default_pid_path() — stores the daemon's process ID for later management
  • Stream redirection — stdout and stderr are redirected to a log file to prevent terminal attachment issues
  • Signal handlers — SIGTERM and SIGHUP handlers are installed for graceful shutdown and config reload

On Windows, daemonize() falls back to foreground execution with a warning, since Windows lacks equivalent fork semantics.

Configuration Loading and Repository Registration

Before starting watchers, the daemon loads its configuration through load_config (lines 40-66). The configuration is stored as a TOML file listing repositories to monitor:


# ~/.code-review-graph/watch.toml example

[[repositories]]
alias = "main-project"
path = "/home/dev/projects/main"

[[repositories]]
alias = "legacy-api"
path = "/home/dev/projects/legacy"

The WatchDaemon maintains an internal registry of these repositories and can detect runtime configuration changes through the ConfigWatcher class (lines 333-382). This component either uses the watchdog library for efficient inotify-based watching or falls back to a polling thread when watchdog is unavailable.

When watch.toml is modified, the daemon calls _on_config_change() which triggers reconcile() — dynamically adding, removing, or updating child watchers without requiring a full restart.

Initial Graph Building

When WatchDaemon.start() is invoked, it first checks each configured repository for an existing graph database at .code-review-graph/graph.db. For any repo lacking this file, the daemon runs _initial_build() (lines 997-1026):


# Internal _initial_build spawns a subprocess

def _initial_build(self, repo_path: Path, alias: str):
    # Executes: code-review-graph build --repo <path>

    # Creates the initial graph.db before continuous watching begins

This subprocess approach isolates the potentially lengthy initial indexing from the daemon's main control loop, preventing blocking operations from delaying health checks or config reloads.

Background Child Process Management

The core monitoring capability comes from persistent watcher processes spawned via _start_watcher() (lines 1038-1075). For each repository, the daemon:

  1. Starts a child process running code-review-graph watch --repo <path>
  2. Redirects the child's output to <log_dir>/<alias>.log
  3. Stores the subprocess.Popen object in the _children dictionary mapped by alias

# Daemon maintains running watchers

_children: Dict[str, subprocess.Popen] = {
    "main-project": <Popen object>,
    "legacy-api": <Popen object>
}

These child processes perform the actual file-system watching and incremental graph updates for individual repositories, while the parent daemon focuses on orchestration.

Health Check Loop and Automatic Recovery

The daemon includes a periodic health-check thread (_health_loop, lines 997-1004) that monitors child process status:

def _health_loop(self):
    while self._running:
        self._check_health()  # Polls each child's .poll() status

        time.sleep(self.health_check_interval)

If _check_health() discovers a child process has terminated (non-None poll result), the daemon automatically restarts it via _start_watcher() and updates its state file. This ensures self-healing operation — temporary failures in individual repository watchers don't require manual intervention.

Cross-Process Status Visibility

For management and debugging, the daemon maintains a JSON state file through _save_state (lines 1213-1225):

{
  "pid": 18473,
  "children": {
    "main-project": {"pid": 18474, "path": "/home/dev/projects/main"},
    "legacy-api": {"pid": 18475, "path": "/home/dev/projects/legacy"}
  },
  "started_at": "2024-01-15T09:23:17Z"
}

CLI commands like code-review-graph daemon status read this file and use the cross-platform pid_alive() helper to report actual process status, even when invoked from completely separate shell sessions. This provides visibility into background operations without requiring direct communication with the daemon process.

CLI Usage Patterns

The daemon can be controlled through the CLI interface in code_review_graph/daemon_cli.py:


# Start the daemon in background

code-review-graph daemon start --daemon

# Check daemon and watcher status

code-review-graph daemon status

# Stop the daemon (reads PID file and sends SIGTERM)

code-review-graph daemon stop

The --daemon flag to start internally invokes WatchDaemon.daemonize() before calling start() and run_forever().

Programmatic Control

For integration with other tools, the daemon can be controlled directly from Python:

from code_review_graph.daemon import WatchDaemon, add_repo_to_config

# Start daemon programmatically

daemon = WatchDaemon()
daemon.daemonize()
daemon.start()
daemon.run_forever()  # Blocks until SIGTERM received

Adding repositories dynamically without restarting:

from code_review_graph.daemon import add_repo_to_config

# Updates watch.toml; ConfigWatcher detects change automatically

add_repo_to_config("/new/repo/path", alias="new-project")

This triggers the daemon's reconcile() method to spawn a new watcher process for the added repository.

Summary

The daemon mode in code-review-graph provides comprehensive background operation through:

  • Process detachment via double-fork Unix daemonization with PID file management
  • Automatic initialization that builds graph databases for new repositories before continuous monitoring
  • Isolated watcher processes per repository, with dedicated log files and failure domains
  • Live configuration reloading that adapts to repository additions and removals without restart
  • Health-check monitoring with automatic process restart for self-healing operation
  • Persistent state files enabling cross-process status queries and management

These capabilities eliminate the need for external tools like tmux or screen while ensuring code review graphs remain synchronized with repository changes.

Frequently Asked Questions

How does the daemon handle Windows systems without fork support?

On Windows, WatchDaemon.daemonize() detects the platform and falls back to foreground execution with a user warning. The rest of the daemon functionality — child process management, health checks, and config watching — continues to work normally, but the process remains attached to the console. Production Windows deployments typically use Windows Service wrappers or scheduled tasks to achieve background operation.

What happens when a repository watcher process crashes?

The _health_loop thread polls each child's Popen.poll() status every few seconds. When a non-None return code is detected, _check_health() automatically invokes _start_watcher() to spawn a replacement process for that specific repository. The failure is logged, and the state file is updated with the new child PID. This per-repository isolation means one misbehaving repo cannot destabilize monitoring for others.

Can I change which repositories are watched without restarting the daemon?

Yes. The ConfigWatcher component monitors watch.toml for modifications using either watchdog inotify events or polling. When the file changes, the daemon reloads its configuration and runs reconcile() to compute differences — starting watchers for newly added repos, terminating removed ones, and restarting repos whose paths changed. You can also use add_repo_to_config() from Python code to trigger this flow programmatically.

How do I check if the daemon is actually running?

The CLI command code-review-graph daemon status reads the daemon's JSON state file and verifies each declared PID using pid_alive(), a cross-platform helper that checks process existence without requiring signals. This works even from separate terminal sessions or CI pipelines. The output shows the daemon PID, its start time, and the status of each individual repository watcher.

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 →