How the Multi-Repo Daemon Watches and Updates Graphs in code-review-graph
TLDR: The multi-repo daemon continuously monitors configured repositories by spawning isolated code-review-graph watch subprocesses for each repo, automatically handles configuration changes through reconciliation cycles, and ensures graph consistency via health checks and initial build processes.
The code-review-graph toolset provides a robust daemon architecture designed to maintain up-to-date code review graphs across multiple repositories without manual intervention. The multi-repo watch daemon orchestrates this by combining process management, file system monitoring, and state persistence to create a resilient background service. This implementation delegates the heavy lifting of file watching to child processes while the parent daemon coordinates configuration changes and system health.
Configuration Loading and State Persistence
The daemon initializes by reading ~/.code-review-graph/watch.toml, a TOML configuration file that defines the repository list, aliases, and daemon-wide settings. The load_config function in code_review_graph/daemon.py parses this file and constructs a DaemonConfig object that drives subsequent behavior.
State persistence ensures the daemon remains tractable across process boundaries. The system writes its process ID to ~/.code-review-graph/daemon.pid and maintains child process information in daemon-state.json. Utility functions including write_pid, read_pid, _save_state, and load_state manage these files, enabling external CLI commands to query status even when running in separate processes.
Spawning Repository Watchers
For each repository defined in the configuration, the daemon delegates file system monitoring to dedicated child processes. The _start_watcher method constructs and launches a code-review-graph watch subprocess for each repo, recording process handles in the internal _children mapping.
Before initiating the watch process, the daemon verifies whether the repository requires an initial graph build. The _initial_build method executes code-review-graph build for repos lacking existing databases, ensuring the graph exists before incremental updates begin. This separation of concerns allows the lightweight watcher processes to focus exclusively on detecting file changes while the parent manages lifecycle complexity.
Health Monitoring and Automatic Recovery
The daemon implements a supervisory pattern through the _health_loop thread, which executes every 30 seconds as defined by _HEALTH_CHECK_INTERVAL. The _check_health method verifies each child process status, automatically respawning any watcher that has terminated unexpectedly and refreshing the state file with new process IDs.
This self-healing mechanism ensures that temporary failures in individual repository watchers do not compromise the overall monitoring system. The parent process maintains authority over process lifetime while remaining resilient to subprocess crashes.
Dynamic Configuration Updates
Configuration changes trigger a reconciliation workflow that aligns running processes with the updated watch.toml. The ConfigWatcher class monitors the configuration file using the watchdog library when available, falling back to a polling thread via _start_polling on systems where watchdog is unavailable.
When ConfigWatcher._on_config_changed detects modifications, it invokes WatchDaemon._on_config_change, which reloads the configuration and executes WatchDaemon.reconcile. The reconciliation algorithm computes set differences between desired and current repository sets, performing three critical operations:
- Adding new watchers for newly configured repositories, including initial builds when necessary
- Removing stale watchers and terminating their associated processes
- Updating existing watchers when repository paths change
After reconciliation, the daemon persists the new state to ensure consistency across restarts.
Daemonization and Cross-Platform Support
On POSIX platforms, the daemon supports background operation through the daemonize method, which performs a double-fork to detach from the controlling terminal. The _setup_signal_handlers method installs handlers for graceful shutdown, ensuring child processes terminate cleanly and PID files are removed.
Cross-platform compatibility requires platform-specific process handling. On Windows, the daemon operates strictly in foreground mode without forking, utilizing _pid_alive_windows within the pid_alive function to circumvent os.kill limitations on that platform. This ensures reliable process status detection across operating systems.
Practical Usage Examples
The daemon exposes both programmatic and command-line interfaces for operational control.
Start the daemon programmatically in the foreground:
from code_review_graph.daemon import WatchDaemon
daemon = WatchDaemon()
daemon.start() # spawns one watcher per repo
daemon.run_forever() # blocks until Ctrl-C or SIGTERM
Run as a background service via CLI:
code-review-graph daemon start --daemonize
# Forks, writes ~/.code-review-graph/daemon.pid,
# and returns control to the shell
Add repositories dynamically:
from code_review_graph.daemon import add_repo_to_config
add_repo_to_config("~/projects/my-lib", alias="mylib")
# ConfigWatcher detects the change automatically
# and starts a new watcher process
Query status from external processes:
code-review-graph daemon status --json
# Returns: {"session_name":"crg-watch","running":true,"repos":[...]}
Force manual reconciliation after bulk configuration edits:
from code_review_graph.daemon import WatchDaemon, load_config
cfg = load_config() # reads current watch.toml
daemon = WatchDaemon(cfg)
daemon.reconcile() # aligns watchers with configuration
Summary
- The multi-repo daemon centralizes repository monitoring through a parent process that manages multiple
code-review-graph watchchild processes - Configuration resides in
~/.code-review-graph/watch.toml, parsed byload_configand monitored viaConfigWatcherwith automatic reconciliation - Process state persists through
daemon.pidanddaemon-state.json, enabling external status queries and crash recovery - Health checks run every 30 seconds via
_health_loop, with automatic restart of failed watchers through_check_health - The architecture supports both foreground operation and POSIX daemonization, with Windows-specific process handling for cross-platform compatibility
Frequently Asked Questions
How does the daemon handle configuration file changes while running?
The ConfigWatcher class monitors ~/.code-review-graph/watch.toml using the watchdog library for event-driven updates, falling back to polling via _start_polling if watchdog is absent. When changes occur, WatchDaemon.reconcile computes differences between the current and desired repository sets, then adds, removes, or updates watchers accordingly without requiring a full daemon restart.
What happens when a repository watcher process crashes?
The daemon's _health_loop thread checks child process status every 30 seconds using _check_health. If a watcher has terminated, the daemon automatically invokes _start_watcher to respawn the process for that specific repository, preserving the monitoring coverage for other repos while logging the recovery event.
Can the daemon run on Windows, and what limitations exist?
Yes, the daemon supports Windows but with specific adaptations. It runs exclusively in foreground mode on Windows (no double-fork daemonization), and uses _pid_alive_windows within the pid_alive utility to check process status without sending signals, accommodating Windows' different process management semantics compared to POSIX systems.
Where does the daemon store its runtime state information?
Runtime state persists in two locations: ~/.code-review-graph/daemon.pid contains the parent daemon's process ID for lifecycle management, while daemon-state.json tracks active child watcher processes. These files enable the status command to report daemon health from separate CLI instances and facilitate graceful shutdown sequences.
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 →