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

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 instantiates the WatchDaemon class and invokes daemonize() → start() (lines 588-596 in 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 (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) 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

$ 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

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

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

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 orchestrates child watcher processes through WatchDaemon.start(), ensuring each repository has a dedicated filesystem monitor.
  • Incremental updates in 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, 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 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 (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). 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. 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 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.

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 →