Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,3 +124,20 @@ Inside the `_is_ignored_impl` hot path in `watchdog`, calling `os.path.relpath`

Action:
Pre-compute `_base_prefix` during initialization (`os.path.join(self.base_path, '')`) and use it in `startswith()` alongside `_abs_base_path` for fast string slicing. Also removed the blind `.removeprefix('./')` behavior to improve robustness.

## 2026-04-29 — Reliability Fix for SIGTERM

Learning:
Command-line file watchers and daemon tools usually listen for KeyboardInterrupt (SIGINT) to clean up subprocesses gracefully. However, they often ignore SIGTERM, which is the standard termination signal sent by containers (Docker/K8s) and process managers. Ignoring SIGTERM causes the main watcher to die instantly, leaking running child processes in the background indefinitely and causing resource exhaustion.

Action:
Always register a SIGTERM handler on POSIX systems (`if platform.system() != "Windows"`) that performs the same graceful shutdown and subprocess termination steps as the KeyboardInterrupt handler.

## 2026-04-29 — Ignore Filter Relpath & Compound Loop Overhead

Learning:
Inside the `_is_ignored_impl` hot path, `os.path.relpath` is computationally expensive because it inherently resolves absolute paths. While optimizations existed for exact prefix matching, simple relative paths (e.g., `src/file.py`) against a `.` base path would fall through and trigger a `relpath` call, slowing down high-volume events. Additionally, reconstructing cumulative directory prefixes (`foo`, `foo/bar`) to test against exact/wildcard ignores consumes significant CPU cycles and is entirely unnecessary if the user specified no compound ignore patterns (i.e., no slashes in any pattern).

Action:
In `watchdog` event path normalization, bypass the computationally expensive `os.path.relpath` for the common case where `base_path` is `.` and the path is already relative by adding a fast-path condition: `elif self.base_path == "." and not os.path.isabs(path) and not path.startswith(".."): pass`.
To optimize ignore pattern matching in hot loops, pre-compute a flag during initialization (e.g., `self._has_compound_ignores = any('/' in p for p in self.ignore_patterns)`) and use it to short-circuit the evaluation of compound directory paths if no slash-based ignore patterns exist.
16 changes: 16 additions & 0 deletions .jules/warden.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,3 +160,19 @@ Observed the preceding agent optimized the ignore file watcher hot path by pre-c

Alignment / Deferred:
Version bumped to `0.1.21` as a patch release. Updated CHANGELOG.md. No dead code or dependency upgrades required.

## 2026-04-29 — Assessment & Lifecycle

Observation / Pruned:
Observed the preceding agent optimized process lifecycle management by adding a POSIX SIGTERM signal handler. This prevents child process leaks when the application is terminated by process managers or containers. Verified test execution, linting, and dead code pruning without issues. No unused imports or variables were found.

Alignment / Deferred:
Version bumped to `0.1.22` as a patch release. Updated CHANGELOG.md. No heavy pruning or major dependency updates required.

## 2026-04-30 — Assessment & Lifecycle

Observation / Pruned:
Observed the preceding agent optimized the ignore file watcher hot paths by explicitly bypassing `os.path.relpath` for the common case, and short-circuiting compound directory evaluations when no slash-based ignore patterns exist. Verified test execution, linting, and dead code pruning without issues. No unused imports or variables were found. No heavy pruning required.

Alignment / Deferred:
Version bumped to `0.1.23` as a patch release. Updated CHANGELOG.md.
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
# Changelog

## [0.1.23] - 2026-04-30

### Changed
* **[Performance]:** Optimized ignore file filtering in hot paths by fast-tracking common relative paths and avoiding compound loop iterations when unnecessary, significantly reducing CPU cycles on burst saves.

## [0.1.22] - 2026-04-29

### Changed
* **[Reliability]:** Added a SIGTERM signal handler to ensure proper cleanup of subprocesses during graceful shutdowns initiated by containers and process managers.

## [0.1.21] - 2026-04-28

### Changed
Expand Down
45 changes: 45 additions & 0 deletions fix_merge.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import sys

content = open("src/echo/watcher.py").read()

conflict = """<<<<<<< HEAD
def handle_sigterm(signum, frame):
observer.stop()
console.print("\\n[magenta]Echo shutting down. Peace ✨[/magenta]")
event_handler.shutdown()
sys.exit(0)

if platform.system() != "Windows":
if signal.getsignal(signal.SIGTERM) != handle_sigterm:
signal.signal(signal.SIGTERM, handle_sigterm)
=======
def handle_sigterm(_signum, _frame):
try:
observer.stop()
console.print("\\n[magenta]Echo shutting down. Peace ✨[/magenta]")
event_handler.shutdown()
except Exception:
pass
sys.exit(0)

if platform.system() != "Windows":
signal.signal(signal.SIGTERM, handle_sigterm)
>>>>>>> origin/main"""

resolution = """ def handle_sigterm(_signum, _frame):
try:
observer.stop()
console.print("\\n[magenta]Echo shutting down. Peace ✨[/magenta]")
event_handler.shutdown()
except Exception:
pass
sys.exit(0)

if platform.system() != "Windows":
if signal.getsignal(signal.SIGTERM) != handle_sigterm:
signal.signal(signal.SIGTERM, handle_sigterm)"""

content = content.replace(conflict, resolution)

with open("src/echo/watcher.py", "w") as f:
f.write(content)
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "echo-watcher"
version = "0.1.21"
version = "0.1.23"
description = "📡 Lightweight file watcher. Trigger commands on changes. <5MB RAM, single binary."
authors = [
{ name = "shenald-dev", email = "bot@shenald.dev" }
Expand Down
18 changes: 17 additions & 1 deletion src/echo/watcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ def __init__(self, command: str, base_path: str = ".", ignore_patterns: list[str
self.exact_ignores = {p for p in self.ignore_patterns if not any(c in p for c in ('*', '?', '['))}
wildcard_ignores = [p for p in self.ignore_patterns if any(c in p for c in ('*', '?', '['))]
self.wildcard_regex = None
self._has_compound_ignores = any('/' in p for p in self.ignore_patterns)
if wildcard_ignores:
regex_str = "|".join(f"(?:{fnmatch.translate(p)})" for p in wildcard_ignores)
self.wildcard_regex = re.compile(regex_str)
Expand Down Expand Up @@ -171,6 +172,8 @@ def _is_ignored_impl(self, path: str) -> bool:
path = path[len(self._base_prefix):]
elif path == self.base_path or path == self._abs_base_path.rstrip(os.sep):
path = "."
elif self.base_path == "." and not os.path.isabs(path) and not path.startswith(".."):
pass
else:
try:
path = os.path.relpath(path, self.base_path)
Expand All @@ -191,7 +194,7 @@ def _is_ignored_impl(self, path: str) -> bool:
return True

# Check for exact and wildcard ignore patterns matching cumulative prefix directories
if len(parts) > 1:
if self._has_compound_ignores and len(parts) > 1:
prefix = parts[0]
# Prefix for parts[0] is already evaluated via earlier exact match `isdisjoint()`
# and wildcard matching, so we start accumulating from the second part.
Expand Down Expand Up @@ -266,6 +269,19 @@ def main():
sys.exit(1)
raise

def handle_sigterm(_signum, _frame):
try:
observer.stop()
console.print("\n[magenta]Echo shutting down. Peace ✨[/magenta]")
event_handler.shutdown()
except Exception:
pass
sys.exit(0)

if platform.system() != "Windows":
if signal.getsignal(signal.SIGTERM) != handle_sigterm:
signal.signal(signal.SIGTERM, handle_sigterm)

try:
while True:
time.sleep(1)
Expand Down