How the Multi-Repo Daemon in code-review-graph Watches and Updates Multiple Projects
The WatchDaemon class implements a parent-child process architecture that loads repository configurations from watch.toml, spawns isolated subprocess watchers for each project via _start_watcher(), and dynamically reconciles changes through filesystem monitoring and periodic health checks.
The code-review-graph tool provides a robust multi-repo daemon capable of monitoring an arbitrary number of Git repositories simultaneously. Unlike single-process solutions, this design isolates each project watcher in its own subprocess to prevent cascading failures, while dynamically managing their lifecycles as configurations change.
Loading Configuration from watch.toml
The daemon initializes by reading a TOML configuration file through the load_config() function. This parses watch.toml (typically located under ~/.code-review-graph via crg_home() in code_review_graph/constants.py) into a DaemonConfig object containing a list of WatchRepo instances.
Each WatchRepo encapsulates the repository path, optional alias, and database settings required for graph construction. The daemon validates these paths during startup, ensuring that only accessible repositories are processed before any subprocesses are spawned.
Spawning Isolated Child Watchers
For every configured repository, the daemon invokes _start_watcher() to create an isolated monitoring environment. This method constructs a command vector (code-review-graph watch --repo <path>), redirects the child’s stdout and stderr to dedicated per-repo log files, and stores the resulting Popen handle in the internal _children dictionary.
This subprocess isolation is critical for multi-repository scaling. Because each watcher operates independently, a segmentation fault or infinite loop in one repository’s analyzer cannot compromise the daemon’s ability to monitor others. The implementation in code_review_graph/daemon.py handles binary detection automatically, falling back to module entry-points when the compiled binary is unavailable.
Dynamic Reconciliation with ConfigWatcher
The daemon supports hot-reloading of configurations through the ConfigWatcher class. Using the watchdog library when available (or a polling fallback), this component monitors watch.toml for modifications. Upon detecting a change, it triggers reconcile(), which performs a three-way diff between the current and new configuration states:
- Detects deltas – Identifies added, removed, or renamed repositories by comparing
WatchRepoidentifiers - Updates registry – Registers new repositories in the central
Registry(code_review_graph/registry.py) - Triggers builds – Executes
_initial_build()for repositories lacking an existing graph database - Manages processes – Starts new watchers, gracefully terminates removed ones, and restarts modified configurations
This reconciliation loop ensures that editing watch.toml never requires a full daemon restart, enabling zero-downtime configuration updates in production deployments.
Health Monitoring and Automatic Recovery
A background _health_loop thread executes every 30 seconds to verify the integrity of the child process pool. The _check_health() method iterates over the _children dictionary, checking process exit codes. If a watcher has terminated unexpectedly, the daemon automatically invokes _start_watcher() to respawn it and calls _save_state() to persist the new PID to daemon-state.json.
This self-healing mechanism, combined with state persistence, allows the daemon to recover from crashes and provides the is_daemon_running() utility function with accurate runtime information for external CLI queries.
Practical Usage Examples
Initialize and start the daemon in the foreground:
from code_review_graph.daemon import WatchDaemon
daemon = WatchDaemon() # Loads watch.toml from default location
daemon.start() # Spawns watchers for all configured repos
daemon.run_forever() # Blocks main thread; monitors children
Run as a background service on Unix systems:
daemon.daemonize() # Forks process, writes PID file
daemon.run_forever()
Add a repository programmatically and trigger hot-reload:
from code_review_graph.daemon import add_repo_to_config
add_repo_to_config("~/projects/myapp", alias="myapp")
# ConfigWatcher detects the change automatically; alternatively:
daemon.reconcile()
Query daemon status from external processes:
from code_review_graph.daemon import WatchDaemon, is_daemon_running
if is_daemon_running():
print(WatchDaemon().status())
Summary
- The multi-repo daemon uses the
WatchDaemonclass incode_review_graph/daemon.pyto orchestrate multiple repository watchers through subprocess isolation. - Configuration is defined in
watch.tomland parsed intoDaemonConfigobjects, with each repository represented as aWatchRepo. - The
_start_watcher()method launches isolated child processes that executecode-review-graph watchcommands and log to per-repository files. - Dynamic reconciliation via
reconcile()andConfigWatcherenables runtime configuration updates without daemon restarts. - Health checks every 30 seconds via
_health_loopautomatically restart failed watchers and persist state todaemon-state.json.
Frequently Asked Questions
How does the multi-repo daemon handle configuration changes without restarting?
The ConfigWatcher monitors watch.toml using filesystem events (via watchdog) or polling intervals. When changes are detected, it invokes reconcile() in code_review_graph/daemon.py, which calculates differences between the current and new configurations, then starts, stops, or restarts child watcher processes accordingly.
What happens if an individual repository watcher crashes?
Every 30 seconds, the _health_loop thread calls _check_health() to verify all entries in _children are responsive. If a process has exited, the daemon automatically respawns it via _start_watcher() and updates daemon-state.json, ensuring continuous monitoring without manual intervention.
Can the daemon scale to monitor dozens of repositories simultaneously?
Yes. The architecture intentionally isolates each repository in its own subprocess, preventing resource contention and failure propagation between projects. This design scales linearly with repository count and eliminates the need for external terminal multiplexers like tmux or screen.
Where does the daemon store its runtime state and logs?
The daemon persists child process PIDs and repository paths to daemon-state.json (located via crg_home() in code_review_graph/constants.py). Per-repository logs are written to separate files during _start_watcher() initialization, enabling isolated debugging for each monitored project.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →