How the File Watcher in Code-Graph-RAG Enables Real-Time Graph Updates
The file watcher in Code-Graph-RAG uses a watchdog-based observer with hybrid debouncing to synchronize your codebase changes with Memgraph in near real-time.
Code-Graph-RAG maintains a live knowledge graph of your repository by monitoring the file system for changes and updating the graph database on-the-fly. The mechanism centers on realtime_updater.py, which orchestrates a robust pipeline that balances responsiveness with computational efficiency. This ensures that every file save, creation, or deletion is reflected in the Memgraph database without overwhelming the system with redundant updates.
Architecture of the Real-Time File Watcher
The watcher system follows a three-phase architecture: initialization, observation, and debounced processing. Each phase is designed to handle the high-frequency nature of development workflows while maintaining graph consistency.
Watcher Initialization and Baseline Scan
The entry point in realtime_updater.py begins with the main() function (lines 10-42), which parses CLI arguments and delegates to start_watcher(). This function constructs a MemgraphIngestor instance using your configured host, port, batch size, and credentials from settings.MEMGRAPH_*.
Before monitoring begins, _run_watcher_loop() executes updater.run() to perform a complete repository scan. This initial pass populates the graph with all modules, files, and call edges, establishing a comprehensive baseline. Only after this complete state is captured does the system begin watching for incremental changes.
Event Handling and Debouncing Strategy
Once initialized, the system creates a CodeChangeEventHandler and registers it with a watchdog.Observer. The observer recursively monitors the repository directory and forwards every FileSystemEvent to the handler.
To prevent graph updates from flooding the database during rapid save operations, the handler implements a hybrid debounce strategy defined in CodeChangeEventHandler.__init__ (lines 75-92) and the dispatch() method (lines 21-46):
- Debounce period: After detecting a change, the system waits
debounce_seconds(default 5 seconds) for a quiet window before processing. - Max-wait limit: Even during continuous edits, the handler forces an update after
max_wait_seconds(default 30 seconds).
The handler stores the first event timestamp, the latest FileSystemEvent, and a timer for each path. When new events arrive for the same file, the previous timer is cancelled and rescheduled, ensuring only the final state is processed.
The Five-Step Graph Update Pipeline
When the debounce timer fires, _process_debounced_change() triggers _process_change(), which executes a deterministic pipeline commented in the source at lines 24-34. The actual implementation resides in _process_change_locked() (lines 30-33) and surrounding helper methods.
Step 1: Delete Stale Graph Data
The system first removes outdated nodes for the changed path using Cypher queries defined in cypher_queries.py. Specifically, it executes CYPHER_DELETE_MODULE and CYPHER_DELETE_FILE to eliminate the previous module and file nodes from Memgraph.
Step 2: Clear In-Memory Caches
The updater calls updater.remove_file_from_state() to drop the file’s Abstract Syntax Tree (AST) and import information from memory. This prevents stale cached data from contaminating the new graph state.
Step 3: Re-parse Modified Files
For modify or create events, the appropriate language parser (resolved via language_spec.py and parser_loader.py) re-processes the file. This updates the AST cache and registers the newly parsed file. Non-code files still receive a generic File node to maintain repository structure integrity.
Step 4: Re-compute CALLS Edges
The system deletes all existing CALLS edges, resets resolution caches, and invokes updater._process_function_calls() to rebuild the call graph across the entire codebase. This ensures that changes in one file correctly propagate to all dependent functions and modules.
Step 5: Flush Changes to Memgraph
Finally, ingestor.flush_all() commits the accumulated changes to the database in a single transaction. This guarantees that the Memgraph instance reflects the latest code state atomically.
Thread Safety and Concurrency Control
The file watcher employs two distinct locks to prevent race conditions during concurrent updates:
self.lock: Synchronizes timer state and event queue management within the handler.self._update_lock: Serializes the actual graph mutation in_process_change_locked(), ensuring that multiple debounced timers firing simultaneously cannot corrupt the database state.
This dual-lock approach allows the system to accept and queue events continuously while guaranteeing that graph mutations occur sequentially.
Configuration and CLI Usage
You can customize the watcher behavior through command-line arguments when launching realtime_updater.py:
# Run with default settings (5s debounce, 30s max-wait)
python -m realtime_updater /path/to/my/project
# Aggressive batching for background monitoring
python -m realtime_updater /path/to/project --debounce 10 --max-wait 60
# Disable debouncing for immediate updates (legacy behavior)
python -m realtime_updater /path/to/project --debounce 0
Default values for DEFAULT_DEBOUNCE_SECONDS and DEFAULT_MAX_WAIT_SECONDS are defined in codebase_rag/config.py alongside Memgraph connection settings.
Summary
- The file watcher in Code-Graph-RAG combines
watchdogobservers with custom debouncing logic inrealtime_updater.pyto balance responsiveness and efficiency. - A hybrid debounce strategy waits 5 seconds of inactivity or forces an update every 30 seconds to batch rapid file changes.
- Updates follow a strict five-step pipeline: delete old data, clear caches, re-parse files, re-compute call edges, and flush to Memgraph.
- Dual-lock concurrency control ensures thread-safe graph mutations even during high-frequency change events.
- The system supports full CLI configurability for debounce timing and can be tuned for different development workflows.
Frequently Asked Questions
How do I adjust the debounce timing for the file watcher?
Pass the --debounce and --max-wait flags when launching the module. For example, python -m realtime_updater /path --debounce 10 --max-wait 60 sets a 10-second quiet period with a 60-second maximum wait. These values override the DEFAULT_DEBOUNCE_SECONDS and DEFAULT_MAX_WAIT_SECONDS constants defined in codebase_rag/config.py.
What happens if multiple files change simultaneously?
The CodeChangeEventHandler maintains separate timers and event state for each file path. While events are tracked independently, the self._update_lock ensures that graph mutations execute sequentially, preventing race conditions when the debounce timers for multiple files fire concurrently.
Does the file watcher handle file deletions differently than modifications?
Yes. While both trigger the five-step pipeline, Step 3 (re-parsing) behaves differently for deletions. When a file is deleted, the system skips the parsing phase and proceeds directly to updating the call graph and flushing changes, effectively removing the file's contribution to the knowledge graph without attempting to parse non-existent content.
Which database operations are performed during a real-time update?
The watcher executes Cypher queries including CYPHER_DELETE_MODULE, CYPHER_DELETE_FILE, and CYPHER_DELETE_CALLS to remove stale nodes and edges, then uses ingestor.flush_all() to write new data. These operations occur within the locked context of _process_change_locked() to ensure atomic updates to the Memgraph database.
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 →