Implement the shared filesystem watcher utility used by both the Rich Filesystem and Obsidian adapters. This module wraps watchdog behind a unified interface with debouncing and atomic-save protection.
Requirements
- Support
watchdog as the primary backend; attempt watchfiles fallback if watchdog is not installed
- Each adapter must instantiate its own
FileSystemWatcher scoped to its own path — no shared singleton
- Translate watchdog filesystem event objects into
SourceRef-compatible path values via a FileEvent dataclass
- Map filesystem events to
PollStrategy.PUSH
- Expose
PollStrategy enum (from models.py) for use in calling code
- Implement debouncing: buffer events for 500ms, coalesce same-path events before dispatching
- Handle editor atomic-save patterns (rename-based saves that produce
deleted + created sequences) — must NOT emit spurious deletion events for the original file path
- Filter events by file extension when an
extensions parameter is provided
- Add
watchdog to dev dependencies in pyproject.toml
Design Guidance
File to create: src/context_library/adapters/_watching.py
The underscore prefix marks this as an internal module not part of the public API.
FileEvent dataclass:
from dataclasses import dataclass
from pathlib import Path
@dataclass
class FileEvent:
path: Path
event_type: str # 'created', 'modified', 'deleted'
FileSystemWatcher class:
class FileSystemWatcher:
def __init__(
self,
watch_path: Path,
callback: Callable[[FileEvent], None],
extensions: set[str] | None = None,
debounce_ms: int = 500,
) -> None: ...
def start(self) -> None: ...
def stop(self) -> None: ...
Debouncing implementation:
- Maintain a pending-events buffer dict keyed by path
- Use a
threading.Timer that fires after debounce_ms to flush the buffer
- On each raw event: cancel the timer, update the buffer entry (later events overwrite earlier ones for the same path), restart the timer
- On timer fire: call
callback for each buffered event, clear the buffer
Atomic save handling (FR-3.6):
- Many editors (vim, emacs, VS Code) save via: write to temp file → rename temp to original → this produces
deleted(original) + created(original) sequence
- Detection: when a
deleted event is followed within debounce window by a created event for the same path, coalesce to a single modified event
- In practice, debouncing the 500ms window achieves this because both events land in the same buffer flush cycle
Watchdog integration:
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler, FileSystemEvent
class _WatchdogHandler(FileSystemEventHandler):
def __init__(self, buffer_callback): ...
def on_created(self, event): ...
def on_modified(self, event): ...
def on_deleted(self, event): ...
def on_moved(self, event): ... # treat as modified on dest path
pyproject.toml change:
[project.optional-dependencies]
dev = ["ruff", "mypy", "pytest", "watchdog"]
For production use by adapters, watchdog should also be added to a watching optional group or the adapter-specific groups (rich-fs, obsidian).
Acceptance Criteria
Dependencies
Phase 1: Model Additions (PollStrategy + MessageMetadata)
Parent Issue
Part of #83
Discussion
This work is detailed in discussion 84
Implement the shared filesystem watcher utility used by both the Rich Filesystem and Obsidian adapters. This module wraps watchdog behind a unified interface with debouncing and atomic-save protection.
Requirements
watchdogas the primary backend; attemptwatchfilesfallback if watchdog is not installedFileSystemWatcherscoped to its own path — no shared singletonSourceRef-compatible path values via aFileEventdataclassPollStrategy.PUSHPollStrategyenum (from models.py) for use in calling codedeleted+createdsequences) — must NOT emit spurious deletion events for the original file pathextensionsparameter is providedwatchdogto dev dependencies inpyproject.tomlDesign Guidance
File to create:
src/context_library/adapters/_watching.pyThe underscore prefix marks this as an internal module not part of the public API.
FileEvent dataclass:
FileSystemWatcher class:
Debouncing implementation:
threading.Timerthat fires afterdebounce_msto flush the buffercallbackfor each buffered event, clear the bufferAtomic save handling (FR-3.6):
deleted(original)+created(original)sequencedeletedevent is followed within debounce window by acreatedevent for the same path, coalesce to a singlemodifiedeventWatchdog integration:
pyproject.toml change:
For production use by adapters,
watchdogshould also be added to awatchingoptional group or the adapter-specific groups (rich-fs,obsidian).Acceptance Criteria
FileSystemWatcher(path, callback)instantiates without error when watchdog is installedstart()begins observing the watch_path;stop()halts the observercreatedevent on a file with a matching extension triggerscallbackwithFileEvent(path=..., event_type='created')modifiedevent triggerscallbackwithevent_type='modified'deleted+createdsequence for the same path within the debounce window does NOT result in adeletedcallback — only amodifiedorcreatedcallbackextensions={'.md'}is set, events for.txtfiles are not dispatched to callbackFileSystemWatcherinstances on different paths operate independently (no shared state)stop()on an unstarted watcher does not raise an exceptionfrom context_library.adapters._watching import FileSystemWatcher, FileEventDependencies
Phase 1: Model Additions (PollStrategy + MessageMetadata)
Parent Issue
Part of #83
Discussion
This work is detailed in discussion 84