knowledge-rag 4.9.0__tar.gz → 4.9.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. {knowledge_rag-4.9.0 → knowledge_rag-4.9.2}/PKG-INFO +3 -1
  2. {knowledge_rag-4.9.0 → knowledge_rag-4.9.2}/README.md +2 -0
  3. {knowledge_rag-4.9.0 → knowledge_rag-4.9.2}/mcp_server/__init__.py +1 -1
  4. knowledge_rag-4.9.2/mcp_server/preflight.py +171 -0
  5. {knowledge_rag-4.9.0 → knowledge_rag-4.9.2}/mcp_server/server.py +9 -3
  6. {knowledge_rag-4.9.0 → knowledge_rag-4.9.2}/pyproject.toml +1 -1
  7. knowledge_rag-4.9.0/mcp_server/preflight.py +0 -74
  8. {knowledge_rag-4.9.0 → knowledge_rag-4.9.2}/.gitignore +0 -0
  9. {knowledge_rag-4.9.0 → knowledge_rag-4.9.2}/LICENSE +0 -0
  10. {knowledge_rag-4.9.0 → knowledge_rag-4.9.2}/bench/dashboard/README.md +0 -0
  11. {knowledge_rag-4.9.0 → knowledge_rag-4.9.2}/config.example.yaml +0 -0
  12. {knowledge_rag-4.9.0 → knowledge_rag-4.9.2}/mcp_server/config.py +0 -0
  13. {knowledge_rag-4.9.0 → knowledge_rag-4.9.2}/mcp_server/fts5_index.py +0 -0
  14. {knowledge_rag-4.9.0 → knowledge_rag-4.9.2}/mcp_server/guarded.py +0 -0
  15. {knowledge_rag-4.9.0 → knowledge_rag-4.9.2}/mcp_server/health.py +0 -0
  16. {knowledge_rag-4.9.0 → knowledge_rag-4.9.2}/mcp_server/ingestion.py +0 -0
  17. {knowledge_rag-4.9.0 → knowledge_rag-4.9.2}/mcp_server/instance_lock.py +0 -0
  18. {knowledge_rag-4.9.0 → knowledge_rag-4.9.2}/mcp_server/logging_config.py +0 -0
  19. {knowledge_rag-4.9.0 → knowledge_rag-4.9.2}/mcp_server/metrics.py +0 -0
  20. {knowledge_rag-4.9.0 → knowledge_rag-4.9.2}/mcp_server/query_router.py +0 -0
  21. {knowledge_rag-4.9.0 → knowledge_rag-4.9.2}/mcp_server/ratelimit.py +0 -0
  22. {knowledge_rag-4.9.0 → knowledge_rag-4.9.2}/mcp_server/security.py +0 -0
  23. {knowledge_rag-4.9.0 → knowledge_rag-4.9.2}/npm/README.md +0 -0
  24. {knowledge_rag-4.9.0 → knowledge_rag-4.9.2}/presets/cybersecurity.yaml +0 -0
  25. {knowledge_rag-4.9.0 → knowledge_rag-4.9.2}/presets/developer.yaml +0 -0
  26. {knowledge_rag-4.9.0 → knowledge_rag-4.9.2}/presets/general.yaml +0 -0
  27. {knowledge_rag-4.9.0 → knowledge_rag-4.9.2}/presets/multilingual.yaml +0 -0
  28. {knowledge_rag-4.9.0 → knowledge_rag-4.9.2}/presets/research.yaml +0 -0
  29. {knowledge_rag-4.9.0 → knowledge_rag-4.9.2}/requirements.txt +0 -0
  30. {knowledge_rag-4.9.0 → knowledge_rag-4.9.2}/skills/README.md +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: knowledge-rag
3
- Version: 4.9.0
3
+ Version: 4.9.2
4
4
  Summary: Local RAG System for Claude Code — Hybrid search + Cross-encoder Reranking + 13 MCP Tools + 20 Format Parsers. Zero external servers.
5
5
  Project-URL: Homepage, https://github.com/lyonzin/knowledge-rag
6
6
  Project-URL: Repository, https://github.com/lyonzin/knowledge-rag
@@ -636,6 +636,8 @@ Native MCP
636
636
 
637
637
  **Complete client configuration guide with JSON schemas per client:** [docs/INSTALLATION.md#use-with-other-mcp-clients →](docs/INSTALLATION.md#use-with-other-mcp-clients)
638
638
 
639
+ > ⚠️ **Operating model — read once.** Your MCP client (Claude Desktop, Code, Cursor, etc.) **spawns and owns the knowledge-rag server process**. Do **NOT** also run `knowledge-rag` in a terminal while your MCP client is open — two writer processes against the same `data_dir` corrupt the ChromaDB HNSW segment on Windows (see [issue #216](https://github.com/lyonzin/knowledge-rag/issues/216)). Recommended baseline: add `"KNOWLEDGE_RAG_SINGLE_INSTANCE": "1"` to the `env` block of your MCP config as a safety net. Full explanation + how to run CLI commands safely: [docs/single-instance.md](docs/single-instance.md).
640
+
639
641
  ---
640
642
 
641
643
  ## ⚙️ Configuration in 30 seconds
@@ -588,6 +588,8 @@ Native MCP
588
588
 
589
589
  **Complete client configuration guide with JSON schemas per client:** [docs/INSTALLATION.md#use-with-other-mcp-clients →](docs/INSTALLATION.md#use-with-other-mcp-clients)
590
590
 
591
+ > ⚠️ **Operating model — read once.** Your MCP client (Claude Desktop, Code, Cursor, etc.) **spawns and owns the knowledge-rag server process**. Do **NOT** also run `knowledge-rag` in a terminal while your MCP client is open — two writer processes against the same `data_dir` corrupt the ChromaDB HNSW segment on Windows (see [issue #216](https://github.com/lyonzin/knowledge-rag/issues/216)). Recommended baseline: add `"KNOWLEDGE_RAG_SINGLE_INSTANCE": "1"` to the `env` block of your MCP config as a safety net. Full explanation + how to run CLI commands safely: [docs/single-instance.md](docs/single-instance.md).
592
+
591
593
  ---
592
594
 
593
595
  ## ⚙️ Configuration in 30 seconds
@@ -8,7 +8,7 @@ import sys # noqa: I001
8
8
  _original_stdout = sys.stdout
9
9
  sys.stdout = sys.stderr
10
10
 
11
- __version__ = "4.9.0"
11
+ __version__ = "4.9.2"
12
12
  __author__ = "Ailton Rocha (Lyon.)"
13
13
 
14
14
  from .config import Config # noqa: E402
@@ -0,0 +1,171 @@
1
+ """Startup preflight checks for persistent ChromaDB state.
2
+
3
+ GH #216 (v4.9.1): probe retries + inconclusive-on-timeout + atomic rename.
4
+ The previous single-attempt quarantine turned any transient failure
5
+ (file locks after client restart, slow disk, cold GPU init) into a
6
+ full-corpus reindex loop on Windows stdio MCP clients. The reporter's
7
+ forensic analysis on issue #216 documented the failure mode and the
8
+ fixes below were validated on their environment before merging.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import os
14
+ import subprocess
15
+ import sys
16
+ import time
17
+ from datetime import datetime
18
+ from pathlib import Path
19
+
20
+ from .config import BASE_DIR, config
21
+
22
+ PROBE_ATTEMPTS = 3
23
+ PROBE_RETRY_DELAY_SECONDS = 5
24
+ PROBE_TIMEOUT_SECONDS = 30
25
+
26
+
27
+ def _log(msg: str) -> None:
28
+ """Emit preflight message to stderr and append to data/preflight.log.
29
+
30
+ Persisting to a file matters because stdio MCP clients hide the server's
31
+ stderr in their own logs — without the file, users could only ever see
32
+ the truncated exit code, not the actual Chroma stack trace.
33
+ """
34
+ line = f"[{datetime.now().isoformat(timespec='seconds')}] {msg}"
35
+ print(line, file=sys.stderr)
36
+ try:
37
+ log_file = config.data_dir / "preflight.log"
38
+ log_file.parent.mkdir(parents=True, exist_ok=True)
39
+ with log_file.open("a", encoding="utf-8") as f:
40
+ f.write(line + "\n")
41
+ except OSError:
42
+ pass
43
+
44
+
45
+ def _backup_active_index(reason: str) -> Path:
46
+ """Atomically move active ChromaDB state aside so the server can rebuild.
47
+
48
+ Uses os.rename (atomic on same volume) instead of shutil.move — the
49
+ fallback copy+rmtree path in shutil.move leaks a partial copy in
50
+ backups/ AND a partially deleted live chroma_db/ when another process
51
+ holds a segment file open (WinError 32).
52
+
53
+ The two moves (metadata JSON + chroma_db dir) must be all-or-nothing.
54
+ If we moved chroma_db first and metadata failed (antivirus scanning
55
+ the JSON, brief file lock), restart would find `chroma_db` gone but
56
+ `index_metadata.json` still live — incremental reindex would then
57
+ read the orphan metadata, mark every doc as "already indexed", and
58
+ silently produce an empty collection. To prevent that, metadata is
59
+ moved first (small file, low lock probability) and chroma_db is
60
+ wrapped in try/except that rolls the metadata rename back before
61
+ re-raising. Restart then sees the pristine pre-quarantine state
62
+ and re-runs preflight cleanly.
63
+ """
64
+ stamp = datetime.now().strftime("%Y%m%d-%H%M%S")
65
+ backup_dir = config.data_dir / "backups" / f"auto-repair-{stamp}"
66
+ backup_dir.mkdir(parents=True, exist_ok=False)
67
+
68
+ metadata_file = config.data_dir / "index_metadata.json"
69
+ metadata_backup = backup_dir / f"index_metadata.{reason}.json"
70
+ metadata_moved = False
71
+ if metadata_file.exists():
72
+ os.rename(str(metadata_file), str(metadata_backup))
73
+ metadata_moved = True
74
+
75
+ if config.chroma_dir.exists():
76
+ try:
77
+ os.rename(str(config.chroma_dir), str(backup_dir / f"chroma_db.{reason}"))
78
+ except OSError:
79
+ if metadata_moved:
80
+ # Rollback the metadata move so the two files stay consistent.
81
+ # If the rollback itself fails (unlikely — dst dir is fresh)
82
+ # we still re-raise the original OSError so the caller can
83
+ # exit cleanly instead of leaving the system in a wedged state.
84
+ try:
85
+ os.rename(str(metadata_backup), str(metadata_file))
86
+ except OSError:
87
+ pass
88
+ raise
89
+
90
+ return backup_dir
91
+
92
+
93
+ def _probe_chroma(timeout_seconds: int = PROBE_TIMEOUT_SECONDS) -> subprocess.CompletedProcess[str]:
94
+ """Check Chroma in a child process so native crashes do not kill MCP startup."""
95
+ code = r"""
96
+ import chromadb
97
+
98
+ from mcp_server.config import config
99
+
100
+ if not config.chroma_dir.exists():
101
+ print("missing")
102
+ raise SystemExit(0)
103
+
104
+ client = chromadb.PersistentClient(path=str(config.chroma_dir))
105
+ collection = client.get_or_create_collection(name=config.collection_name)
106
+ print(collection.count())
107
+ """
108
+ env = os.environ.copy()
109
+ env.setdefault("KNOWLEDGE_RAG_DIR", str(BASE_DIR))
110
+ return subprocess.run(
111
+ [sys.executable, "-c", code],
112
+ cwd=str(BASE_DIR),
113
+ env=env,
114
+ text=True,
115
+ stdout=subprocess.PIPE,
116
+ stderr=subprocess.PIPE,
117
+ timeout=timeout_seconds,
118
+ check=False,
119
+ )
120
+
121
+
122
+ def run_preflight(timeout_seconds: int = PROBE_TIMEOUT_SECONDS) -> bool:
123
+ """Return True when active Chroma state was moved aside for repair.
124
+
125
+ Runs the probe up to PROBE_ATTEMPTS times so transient failures
126
+ (file locks lingering from a previous PID, slow disk, cold GPU init)
127
+ do not trigger a destructive quarantine. TimeoutExpired is treated
128
+ as inconclusive — the index is kept and the user can debug from
129
+ preflight.log without losing the corpus. Only PROBE_ATTEMPTS
130
+ consecutive non-zero exits quarantine.
131
+
132
+ If os.rename fails while quarantining (e.g. another process still
133
+ holds a segment file open on Windows) the index is left untouched
134
+ and the process exits with EX_TEMPFAIL (75) instead of crashing
135
+ halfway with a partial move.
136
+ """
137
+ last_result: subprocess.CompletedProcess[str] | None = None
138
+ for attempt in range(1, PROBE_ATTEMPTS + 1):
139
+ try:
140
+ result = _probe_chroma(timeout_seconds=timeout_seconds)
141
+ except subprocess.TimeoutExpired:
142
+ _log(
143
+ f"[PREFLIGHT] attempt {attempt}/{PROBE_ATTEMPTS}: probe timed out "
144
+ f"after {timeout_seconds}s — inconclusive, keeping index"
145
+ )
146
+ return False
147
+ if result.returncode == 0:
148
+ if attempt > 1:
149
+ _log(f"[PREFLIGHT] attempt {attempt}: OK (recovered from transient failure)")
150
+ return False
151
+ stderr_tail = (result.stderr or "").strip()[-1500:]
152
+ _log(
153
+ f"[PREFLIGHT] attempt {attempt}/{PROBE_ATTEMPTS}: probe failed with code {result.returncode}\n{stderr_tail}"
154
+ )
155
+ last_result = result
156
+ if attempt < PROBE_ATTEMPTS:
157
+ time.sleep(PROBE_RETRY_DELAY_SECONDS)
158
+
159
+ assert last_result is not None
160
+ reason = "segfault" if last_result.returncode in (-11, 139) else "failed"
161
+ _log(f"[RECOVERY] preflight failed {PROBE_ATTEMPTS}x (last exit code {last_result.returncode}); moving index aside")
162
+ try:
163
+ backup_dir = _backup_active_index(reason)
164
+ except OSError as exc:
165
+ _log(
166
+ f"[PREFLIGHT] could not move index aside ({exc}); leaving it untouched "
167
+ f"to avoid partial corruption. Retry after freeing file handles."
168
+ )
169
+ raise SystemExit(75) from exc
170
+ _log(f"[RECOVERY] moved active index to {backup_dir}")
171
+ return True
@@ -4793,9 +4793,15 @@ def main():
4793
4793
  print("[FALLBACK] Attempting regular index instead...")
4794
4794
  stats = orchestrator.index_all(force=True)
4795
4795
  elif orchestrator.collection.count() == 0:
4796
- print("[INFO] No documents indexed. Running initial indexing...")
4797
- stats = orchestrator.index_all()
4798
- print(f"[INFO] Indexed {stats['indexed']} documents with {stats['chunks_added']} chunks")
4796
+ # GH #216: initial indexing runs in a background thread so it
4797
+ # does not block the MCP `initialize` handshake. Blocking here
4798
+ # for a large corpus (~150k chunks takes minutes) caused stdio
4799
+ # clients to timeout, kill the process, and restart into an
4800
+ # empty index — an endless full-reindex loop. Users poll
4801
+ # get_reindex_status() to track progress.
4802
+ print("[INFO] No documents indexed. Starting initial indexing in background...")
4803
+ orchestrator.start_reindex_background("incremental")
4804
+ print("[INFO] Initial indexing runs in background. Track progress with get_reindex_status.")
4799
4805
 
4800
4806
  # Start file watcher for auto-reindex on document changes
4801
4807
  if os.environ.get("KNOWLEDGE_RAG_WATCHER_DISABLED", "").strip() == "1":
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "knowledge-rag"
7
- version = "4.9.0"
7
+ version = "4.9.2"
8
8
  description = "Local RAG System for Claude Code — Hybrid search + Cross-encoder Reranking + 13 MCP Tools + 20 Format Parsers. Zero external servers."
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
@@ -1,74 +0,0 @@
1
- """Startup preflight checks for persistent ChromaDB state."""
2
-
3
- from __future__ import annotations
4
-
5
- import os
6
- import shutil
7
- import subprocess
8
- import sys
9
- from datetime import datetime
10
- from pathlib import Path
11
-
12
- from .config import BASE_DIR, config
13
-
14
-
15
- def _backup_active_index(reason: str) -> Path:
16
- """Move active ChromaDB state aside so the server can rebuild cleanly."""
17
- stamp = datetime.now().strftime("%Y%m%d-%H%M%S")
18
- backup_dir = config.data_dir / "backups" / f"auto-repair-{stamp}"
19
- backup_dir.mkdir(parents=True, exist_ok=False)
20
-
21
- if config.chroma_dir.exists():
22
- shutil.move(str(config.chroma_dir), str(backup_dir / f"chroma_db.{reason}"))
23
-
24
- metadata_file = config.data_dir / "index_metadata.json"
25
- if metadata_file.exists():
26
- shutil.move(str(metadata_file), str(backup_dir / f"index_metadata.{reason}.json"))
27
-
28
- return backup_dir
29
-
30
-
31
- def _probe_chroma(timeout_seconds: int = 30) -> subprocess.CompletedProcess[str]:
32
- """Check Chroma in a child process so native crashes do not kill MCP startup."""
33
- code = r"""
34
- import chromadb
35
-
36
- from mcp_server.config import config
37
-
38
- if not config.chroma_dir.exists():
39
- print("missing")
40
- raise SystemExit(0)
41
-
42
- client = chromadb.PersistentClient(path=str(config.chroma_dir))
43
- collection = client.get_or_create_collection(name=config.collection_name)
44
- print(collection.count())
45
- """
46
- env = os.environ.copy()
47
- env.setdefault("KNOWLEDGE_RAG_DIR", str(BASE_DIR))
48
- return subprocess.run(
49
- [sys.executable, "-c", code],
50
- cwd=str(BASE_DIR),
51
- env=env,
52
- text=True,
53
- stdout=subprocess.PIPE,
54
- stderr=subprocess.PIPE,
55
- timeout=timeout_seconds,
56
- check=False,
57
- )
58
-
59
-
60
- def run_preflight(timeout_seconds: int = 30) -> bool:
61
- """Return True when active Chroma state was moved aside for repair."""
62
- result = _probe_chroma(timeout_seconds=timeout_seconds)
63
- if result.returncode == 0:
64
- return False
65
-
66
- reason = "segfault" if result.returncode in (-11, 139) else "failed"
67
- backup_dir = _backup_active_index(reason)
68
- print(
69
- f"[RECOVERY] Chroma preflight failed with code {result.returncode}; moved active index to {backup_dir}",
70
- file=sys.stderr,
71
- )
72
- if result.stderr:
73
- print(result.stderr[-2000:], file=sys.stderr)
74
- return True
File without changes
File without changes