# How Daemon Mode and File Watching Maintain Graph Consistency in Code-Review-Graph

> Discover how the code-review-graph daemon and file watching ensure graph consistency through continuous synchronization, debounced updates, and self-healing health checks. Learn more.

- Repository: [Tirth Kanani/code-review-graph](https://github.com/tirth8205/code-review-graph)
- Tags: internals
- Published: 2026-07-20

---

**The code-review-graph daemon ensures continuous synchronization by spawning child watcher processes per repository, debouncing filesystem events into atomic incremental updates, and implementing self-healing health checks that automatically restart failed watchers.**

The **code-review-graph** project maintains a persistent SQLite graph database that must remain perfectly synchronized with rapidly evolving source code across multiple repositories. Its daemon mode orchestrates a multi-process architecture that guarantees the on-disk graph reflects the current filesystem state without requiring expensive full rebuilds.

## The Daemon Lifecycle: Initialization and Child Process Management

When you execute `code-review-graph daemon start`, the CLI entry point in [`code_review_graph/daemon_cli.py`](https://github.com/tirth8205/code-review-graph/blob/main/code_review_graph/daemon_cli.py) instantiates the `WatchDaemon` class and invokes `daemonize()` → `start()` (lines 588-596 in [`code_review_graph/daemon.py`](https://github.com/tirth8205/code-review-graph/blob/main/code_review_graph/daemon.py)).

The `start()` method performs three critical operations to establish baseline consistency:

1. **Repository Registration**: Each configured repository is added to the global `Registry` (lines 20-25), establishing the authoritative set of watched paths.
2. **Initial Graph Construction**: For any repository lacking a `graph.db` file, `_initial_build()` (lines 27-31) executes a one-time full parse to create the baseline graph database.
3. **Watcher Spawning**: `_start_watcher()` (lines 32-40 and 88-96) forks a child process executing `code-review-graph watch --repo <path>` for each repository, storing the `Popen` objects in `self._children` with PID tracking.

Child processes inherit the daemon's environment and write logs to `~/.code-review-graph/logs/`, immediately beginning filesystem observation.

## Per-Repository File Watching and Incremental Updates

Each child process executes the `watch()` function in [`code_review_graph/incremental.py`](https://github.com/tirth8205/code-review-graph/blob/main/code_review_graph/incremental.py) (lines 58-71), establishing a `watchdog.Observer` with a custom `GraphUpdateHandler`. This handler implements the core consistency mechanism: converting filesystem events into precise graph mutations.

### Debouncing Rapid Changes

To avoid redundant parsing during rapid save operations, the handler implements debouncing using `threading.Timer` with `_DEBOUNCE_SECONDS = 0.3` (lines 33-38).

When filesystem events occur (`on_modified`, `on_created`), the absolute path is added to a `self._pending` set, and the timer resets. Only after 300 milliseconds of event inactivity does `_flush()` trigger, batching rapid changes into a single logical update.

### Atomic Graph Mutations

When the debounce timer fires, `_flush()` iterates pending paths and calls `_update_file()` (lines 44-51), which performs:

- **Parsing**: The file is read, hashed, and processed by `CodeParser` for language-specific extraction.
- **Storage**: Nodes and edges are written atomically via `store.store_file_nodes_edges()` (lines 70-79).
- **Metadata Commit**: The store's `last_updated` timestamp refreshes and the transaction commits via `store.commit()`.

For deletions, the handler immediately invokes `store.remove_file_data()` and commits (lines 22-30), ensuring the graph never references non-existent files.

## Handling Configuration Drift with ConfigWatcher

The daemon monitors `~/.code-review-graph/watch.toml` for runtime configuration changes through the `ConfigWatcher` class (lines 998-1014). When the configuration file changes, `_on_config_changed()` triggers `WatchDaemon.reconcile()` (lines 744-751).

The `reconcile()` method computes the symmetric difference between the current in-memory repository set and the new configuration:

- **New repositories** undergo registration, initial building if needed, and fresh watcher spawning via `_start_watcher()`.
- **Removed repositories** trigger child process termination and removal from the persisted state file via `_save_state()`.

This ensures the daemon's process tree always matches the user's configuration intent, preventing orphaned watchers or missing repositories.

## Self-Healing Through Health Checks

To prevent silent watcher failures from causing graph staleness, the daemon implements a background health checking thread `_health_loop()` (lines 40-47) that executes every `_HEALTH_CHECK_INTERVAL = 30` seconds.

For each known repository alias, the loop checks `proc.poll()` to detect process termination. Dead processes trigger immediate respawning via `_start_watcher()` (lines 54-60), followed by `_save_state()` to update the PID mapping. This guarantees **self-healing**: even after crashes or out-of-memory kills, the incremental update loop resumes automatically.

## Cross-Process State Persistence

The daemon maintains a JSON state file ([`daemon-state.json`](https://github.com/tirth8205/code-review-graph/blob/main/daemon-state.json)) mapping repository aliases to child PIDs and paths via `_save_state()` (lines 56-62).

When users query status via `code-review-graph daemon status`, the `WatchDaemon.status()` method reads this file (falling back to the in-memory `_children` dict) and verifies process liveness through `pid_alive()` (with platform-specific implementations in `_pid_alive_windows()`). This enables reliable external monitoring of graph synchronization health across multiple repositories.

## Practical Examples

### Launching the Daemon

```bash
$ code-review-graph daemon start

# Daemonizes, writes PID to ~/.code-review-graph/daemon.pid

# Spawns a child watcher for each repo in ~/.code-review-graph/watch.toml

```

### Adding a Repository at Runtime

```bash
$ code-review-graph daemon add-repo ~/my-project

# Updates watch.toml; ConfigWatcher detects the change,

# reconciles the daemon, and spawns a fresh child watcher.

```

### Manual File Watching (Development Mode)

```python
from pathlib import Path
from code_review_graph.graph import GraphStore
from code_review_graph.incremental import watch

repo = Path("/path/to/repo")
store = GraphStore(incremental_path=repo / ".code-review-graph" / "graph.db")
watch(repo, store)   # Blocks until Ctrl-C; performs same incremental updates as daemon mode

```

### Checking Daemon Status Programmatically

```python
from code_review_graph.daemon import WatchDaemon

daemon = WatchDaemon()
status = daemon.status()
print(status)

# {

#   "session_name": "crg-watch",

#   "running": True,

#   "repos": [

#       {"alias": "my-project", "path": "/home/user/my-project", "alive": True, "pid": 12345}

#   ]

# }

```

## Summary

- **Daemon mode** in [`code_review_graph/daemon.py`](https://github.com/tirth8205/code-review-graph/blob/main/code_review_graph/daemon.py) orchestrates child watcher processes through `WatchDaemon.start()`, ensuring each repository has a dedicated filesystem monitor.
- **Incremental updates** in [`code_review_graph/incremental.py`](https://github.com/tirth8205/code-review-graph/blob/main/code_review_graph/incremental.py) use 300ms debouncing and atomic SQLite transactions via `store.store_file_nodes_edges()` to keep the graph database synchronized with source changes.
- **Configuration drift** is handled by `ConfigWatcher` monitoring [`watch.toml`](https://github.com/tirth8205/code-review-graph/blob/main/watch.toml), triggering `reconcile()` to spawn or terminate watchers as repositories are added or removed.
- **Process resilience** is maintained through 30-second health checks in `_health_loop()` that automatically restart failed child processes.
- **State persistence** via [`daemon-state.json`](https://github.com/tirth8205/code-review-graph/blob/main/daemon-state.json) enables cross-process status queries and verification that the graph remains up-to-date.

## Frequently Asked Questions

### How does the daemon handle rapid file saves without redundant parsing?

The file watcher implements debouncing through a `threading.Timer` set to `_DEBOUNCE_SECONDS = 0.3` (300 milliseconds) in [`code_review_graph/incremental.py`](https://github.com/tirth8205/code-review-graph/blob/main/code_review_graph/incremental.py) (lines 33-38). Each filesystem event resets this timer, and only when the timer expires does `_flush()` execute the incremental update. This batches rapid save operations into a single graph mutation, preventing redundant parsing while maintaining eventual consistency.

### What happens if a repository watcher process crashes?

The daemon's `_health_loop()` thread runs every 30 seconds (`_HEALTH_CHECK_INTERVAL = 30`) to check child process health via `proc.poll()` (lines 40-47 in [`daemon.py`](https://github.com/tirth8205/code-review-graph/blob/main/daemon.py)). If a watcher has terminated, the daemon automatically invokes `_start_watcher()` to respawn the process and updates the state file via `_save_state()`. This self-healing mechanism ensures that temporary crashes do not result in stale graph data.

### How does the daemon detect when repositories are added or removed from the configuration?

The `ConfigWatcher` class monitors `~/.code-review-graph/watch.toml` using `watchdog` (with a polling fallback) at lines 998-1014 in [`daemon.py`](https://github.com/tirth8205/code-review-graph/blob/main/daemon.py). When the file changes, `_on_config_changed()` invokes `WatchDaemon.reconcile()` (lines 744-751), which computes the configuration diff and either spawns new watchers for added repositories or terminates existing processes for removed ones.

### Are graph updates atomic during file watching?

Yes. Each incremental update in [`code_review_graph/incremental.py`](https://github.com/tirth8205/code-review-graph/blob/main/code_review_graph/incremental.py) writes nodes and edges through `store.store_file_nodes_edges()` followed by `store.commit()` (lines 70-79). The debouncing mechanism ensures that related changes are batched, and the SQLite transaction guarantees that the graph database transitions directly from one valid state to another without intermediate corruption, even if the update involves multiple dependent files.