How the Multi-Repo Daemon (crg-daemon) Works and Monitors Repositories in code-review-graph
The crg-daemon is a long-running background process that maintains one child process per watched repository, polls their health every 30 seconds, and dynamically reconciles configuration changes from watch.toml without requiring a restart.
The crg-daemon serves as the execution engine for code-review-graph's multi-repository "watch" feature. It manages a fleet of subprocess workers—one for each repository defined in a user-wide TOML configuration—while ensuring only one daemon instance runs via PID-file locking. This architecture allows the tool to continuously update its knowledge graph across any number of codebases with minimal overhead.
Configuration Loading and Validation
The daemon initializes by reading watch.toml from the per-user CRG home directory, resolved via crg_home() in code_review_graph/constants.py. At lines 40‑57 of code_review_graph/daemon.py, helper functions define the default paths:
def default_config_path() -> Path:
"""Path to ``watch.toml`` under the per‑user state directory."""
return crg_home() / "watch.toml"
The load_config() function (lines 176‑215) parses the TOML using tomllib (Python ≥3.11) or tomli, validates repository entries, resolves absolute paths, and checks for repository markers (.git, .svn, or .code-review-graph). It returns a DaemonConfig dataclass (lines 133‑170) containing session_name, log_dir, poll_interval, and the list of repos.
Repository Management (Adding and Removing)
The daemon supports dynamic repository lists through helper functions that modify watch.toml without manual editing.
Adding repositories: The add_repo_to_config() function (lines 297‑342) validates the path, checks for duplicate aliases, appends a WatchRepo entry, and atomically rewrites the configuration file.
Removing repositories: The remove_repo_from_config() function (lines 445‑472) filters out the matching alias and persists the updated list.
These helpers are invoked by the CLI commands crg daemon add /path/to/repo and crg daemon remove <alias>, allowing users to adjust the watch list while the daemon is running.
PID File Management and Single-Instance Enforcement
To prevent duplicate daemon processes, the implementation uses a PID file lock mechanism:
write_pid()(lines 78‑85) creates the parent directory and writes the current process ID todaemon.pid.read_pid()(lines 87‑96) reads the file and validates the process existence.clear_pid()(lines 98‑100) removes the file during shutdown.
When crg daemon start executes, it checks read_pid(); if a valid PID exists and the process is alive, the CLI aborts to prevent double-spawning.
The WatchDaemon Class Architecture
The core runtime logic resides in the WatchDaemon class starting at line 644 in daemon.py. This class orchestrates child process lifecycle, health monitoring, and configuration reconciliation.
Child Process Spawning and Monitoring
Upon start(), the daemon invokes _launch_child() for each repository in the configuration. This method constructs the command:
["code-review-graph", "watch", "--repo", repo.path]
It redirects stdout/stderr to a per-repo log file under log_dir and stores the subprocess.Popen instance in the _children dictionary keyed by repository alias.
A background monitor thread runs _monitor_loop(), which sleeps for _HEALTH_CHECK_INTERVAL (30 seconds) between iterations. The _health_check() method iterates over _children, calling process.poll() to detect terminated processes; any dead child is automatically respawned via _launch_child().
Dynamic Configuration Reconciliation
The daemon detects configuration changes by comparing the modification timestamp of watch.toml during each monitor loop iteration. When a change is detected, it reloads the configuration and calls _reconcile(new_config):
- Added repos: Launches new children via
_launch_child(). - Removed repos: Terminates the corresponding process with
SIGTERMand removes it from_children. - Updated paths: Stops the existing child and starts a new one with the revised path.
This reconciliation occurs without interrupting unaffected repositories, ensuring zero-downtime updates to the watch list.
Signal Handling and Graceful Shutdown
The _setup_signal_handlers() method installs handlers for SIGTERM and SIGINT that invoke _sigterm_handler(). This handler triggers stop(), which:
- Sends
SIGTERMto each child process. - Joins the monitor thread.
- Writes the final state snapshot to
daemon-state.json. - Calls
clear_pid()to remove the lock file. - Exits the process cleanly.
The daemon-state.json file preserves the list of active children, enabling faster state recovery on restart.
CLI Interface and User Commands
User interaction flows through code_review_graph/daemon_cli.py, which exposes the following commands:
crg daemon start– Initializes theWatchDaemon, writes the PID file, and enters the monitoring loop.crg daemon stop– Reads the PID fromdaemon.pidand sends a termination signal.crg daemon status– Verifies the PID file exists and checks process liveness viaos.kill(pid, 0).crg daemon add <path>– Appends a repository towatch.tomland triggers reconciliation on the next poll.crg daemon remove <alias>– Removes a repository from the configuration.
The CLI handles argument parsing and delegates to the WatchDaemon methods defined in daemon.py.
Summary
- The multi-repo daemon (
crg-daemon) manages the lifecycle of per-repository watch processes through theWatchDaemonclass incode_review_graph/daemon.py. - It enforces single-instance execution via PID files (
daemon.pid) and provides graceful shutdown handlers forSIGTERMandSIGINT. - The health check thread polls every 30 seconds (
_HEALTH_CHECK_INTERVAL) to restart failed children automatically. - Dynamic configuration is supported through
watch.tomlpolling; the daemon reconciles added, removed, or updated repositories without restarting. - Child processes execute
code-review-graph watch <repo-path>and log to dedicated files under the configuredlog_dir.
Frequently Asked Questions
How does crg-daemon prevent multiple instances from running simultaneously?
The daemon writes its process ID to daemon.pid via write_pid() (lines 78‑85). On startup, read_pid() checks if the file exists and whether the process is still alive using os.kill(pid, 0). If a valid PID is found, the CLI aborts with an error, ensuring only one daemon instance manages the repository fleet.
What happens when I add a new repository while the daemon is running?
When you run crg daemon add <path>, the CLI updates watch.toml via add_repo_to_config(). During its next poll cycle (interval defined by poll_interval in the configuration), the daemon detects the file modification, reloads the configuration, and calls _reconcile(). This launches a new child process for the added repository without disturbing existing watchers.
How does the daemon handle crashed or killed child processes?
Every 30 seconds, the _health_check() method iterates through the _children dictionary and checks process.poll(). If a child has terminated (returning a non-None exit code), the daemon immediately invokes _launch_child() to respawn the worker for that specific repository, ensuring continuous graph updates.
Where does the daemon store its state and logs?
The daemon writes its PID to daemon.pid and a JSON state snapshot to daemon-state.json, both located in the crg_home() directory. Per-repository logs are stored in the log_dir specified in watch.toml (defaulting to a subdirectory under crg_home()), with each repository writing to a separate file named after its alias.
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 →