# How the Multi-Repo Daemon (crg-daemon) Works and Monitors Repositories in code-review-graph

> Discover how the crg-daemon monitors repositories in code-review-graph. Learn about its process per repo, health polling, and dynamic configuration updates without restarts.

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

---

**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`](https://github.com/tirth8205/code-review-graph/blob/main/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`](https://github.com/tirth8205/code-review-graph/blob/main/watch.toml) from the per-user CRG home directory, resolved via `crg_home()` in [`code_review_graph/constants.py`](https://github.com/tirth8205/code-review-graph/blob/main/code_review_graph/constants.py). At **lines 40‑57** of [`code_review_graph/daemon.py`](https://github.com/tirth8205/code-review-graph/blob/main/code_review_graph/daemon.py), helper functions define the default paths:

```python
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`](https://github.com/tirth8205/code-review-graph/blob/main/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 to `daemon.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`](https://github.com/tirth8205/code-review-graph/blob/main/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:

```python
["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`](https://github.com/tirth8205/code-review-graph/blob/main/watch.toml) during each monitor loop iteration. When a change is detected, it reloads the configuration and calls `_reconcile(new_config)`:

1. **Added repos:** Launches new children via `_launch_child()`.
2. **Removed repos:** Terminates the corresponding process with `SIGTERM` and removes it from `_children`.
3. **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:

1. Sends `SIGTERM` to each child process.
2. Joins the monitor thread.
3. Writes the final state snapshot to [`daemon-state.json`](https://github.com/tirth8205/code-review-graph/blob/main/daemon-state.json).
4. Calls `clear_pid()` to remove the lock file.
5. Exits the process cleanly.

The [`daemon-state.json`](https://github.com/tirth8205/code-review-graph/blob/main/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`](https://github.com/tirth8205/code-review-graph/blob/main/code_review_graph/daemon_cli.py), which exposes the following commands:

- `crg daemon start` – Initializes the `WatchDaemon`, writes the PID file, and enters the monitoring loop.
- `crg daemon stop` – Reads the PID from `daemon.pid` and sends a termination signal.
- `crg daemon status` – Verifies the PID file exists and checks process liveness via `os.kill(pid, 0)`.
- `crg daemon add <path>` – Appends a repository to [`watch.toml`](https://github.com/tirth8205/code-review-graph/blob/main/watch.toml) and 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`](https://github.com/tirth8205/code-review-graph/blob/main/daemon.py).

## Summary

- The **multi-repo daemon** (`crg-daemon`) manages the lifecycle of per-repository watch processes through the `WatchDaemon` class in [`code_review_graph/daemon.py`](https://github.com/tirth8205/code-review-graph/blob/main/code_review_graph/daemon.py).
- It enforces **single-instance execution** via PID files (`daemon.pid`) and provides **graceful shutdown** handlers for `SIGTERM` and `SIGINT`.
- The **health check thread** polls every **30 seconds** (`_HEALTH_CHECK_INTERVAL`) to restart failed children automatically.
- **Dynamic configuration** is supported through [`watch.toml`](https://github.com/tirth8205/code-review-graph/blob/main/watch.toml) polling; 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 configured `log_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`](https://github.com/tirth8205/code-review-graph/blob/main/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`](https://github.com/tirth8205/code-review-graph/blob/main/daemon-state.json), both located in the `crg_home()` directory. Per-repository logs are stored in the `log_dir` specified in [`watch.toml`](https://github.com/tirth8205/code-review-graph/blob/main/watch.toml) (defaulting to a subdirectory under `crg_home()`), with each repository writing to a separate file named after its alias.