knowledge-rag 4.9.0__tar.gz → 4.9.1__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.
- {knowledge_rag-4.9.0 → knowledge_rag-4.9.1}/PKG-INFO +1 -1
- {knowledge_rag-4.9.0 → knowledge_rag-4.9.1}/mcp_server/__init__.py +1 -1
- knowledge_rag-4.9.1/mcp_server/preflight.py +145 -0
- {knowledge_rag-4.9.0 → knowledge_rag-4.9.1}/mcp_server/server.py +9 -3
- {knowledge_rag-4.9.0 → knowledge_rag-4.9.1}/pyproject.toml +1 -1
- knowledge_rag-4.9.0/mcp_server/preflight.py +0 -74
- {knowledge_rag-4.9.0 → knowledge_rag-4.9.1}/.gitignore +0 -0
- {knowledge_rag-4.9.0 → knowledge_rag-4.9.1}/LICENSE +0 -0
- {knowledge_rag-4.9.0 → knowledge_rag-4.9.1}/README.md +0 -0
- {knowledge_rag-4.9.0 → knowledge_rag-4.9.1}/bench/dashboard/README.md +0 -0
- {knowledge_rag-4.9.0 → knowledge_rag-4.9.1}/config.example.yaml +0 -0
- {knowledge_rag-4.9.0 → knowledge_rag-4.9.1}/mcp_server/config.py +0 -0
- {knowledge_rag-4.9.0 → knowledge_rag-4.9.1}/mcp_server/fts5_index.py +0 -0
- {knowledge_rag-4.9.0 → knowledge_rag-4.9.1}/mcp_server/guarded.py +0 -0
- {knowledge_rag-4.9.0 → knowledge_rag-4.9.1}/mcp_server/health.py +0 -0
- {knowledge_rag-4.9.0 → knowledge_rag-4.9.1}/mcp_server/ingestion.py +0 -0
- {knowledge_rag-4.9.0 → knowledge_rag-4.9.1}/mcp_server/instance_lock.py +0 -0
- {knowledge_rag-4.9.0 → knowledge_rag-4.9.1}/mcp_server/logging_config.py +0 -0
- {knowledge_rag-4.9.0 → knowledge_rag-4.9.1}/mcp_server/metrics.py +0 -0
- {knowledge_rag-4.9.0 → knowledge_rag-4.9.1}/mcp_server/query_router.py +0 -0
- {knowledge_rag-4.9.0 → knowledge_rag-4.9.1}/mcp_server/ratelimit.py +0 -0
- {knowledge_rag-4.9.0 → knowledge_rag-4.9.1}/mcp_server/security.py +0 -0
- {knowledge_rag-4.9.0 → knowledge_rag-4.9.1}/npm/README.md +0 -0
- {knowledge_rag-4.9.0 → knowledge_rag-4.9.1}/presets/cybersecurity.yaml +0 -0
- {knowledge_rag-4.9.0 → knowledge_rag-4.9.1}/presets/developer.yaml +0 -0
- {knowledge_rag-4.9.0 → knowledge_rag-4.9.1}/presets/general.yaml +0 -0
- {knowledge_rag-4.9.0 → knowledge_rag-4.9.1}/presets/multilingual.yaml +0 -0
- {knowledge_rag-4.9.0 → knowledge_rag-4.9.1}/presets/research.yaml +0 -0
- {knowledge_rag-4.9.0 → knowledge_rag-4.9.1}/requirements.txt +0 -0
- {knowledge_rag-4.9.0 → knowledge_rag-4.9.1}/skills/README.md +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: knowledge-rag
|
|
3
|
-
Version: 4.9.
|
|
3
|
+
Version: 4.9.1
|
|
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
|
|
@@ -0,0 +1,145 @@
|
|
|
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
|
+
stamp = datetime.now().strftime("%Y%m%d-%H%M%S")
|
|
54
|
+
backup_dir = config.data_dir / "backups" / f"auto-repair-{stamp}"
|
|
55
|
+
backup_dir.mkdir(parents=True, exist_ok=False)
|
|
56
|
+
|
|
57
|
+
if config.chroma_dir.exists():
|
|
58
|
+
os.rename(str(config.chroma_dir), str(backup_dir / f"chroma_db.{reason}"))
|
|
59
|
+
|
|
60
|
+
metadata_file = config.data_dir / "index_metadata.json"
|
|
61
|
+
if metadata_file.exists():
|
|
62
|
+
os.rename(str(metadata_file), str(backup_dir / f"index_metadata.{reason}.json"))
|
|
63
|
+
|
|
64
|
+
return backup_dir
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _probe_chroma(timeout_seconds: int = PROBE_TIMEOUT_SECONDS) -> subprocess.CompletedProcess[str]:
|
|
68
|
+
"""Check Chroma in a child process so native crashes do not kill MCP startup."""
|
|
69
|
+
code = r"""
|
|
70
|
+
import chromadb
|
|
71
|
+
|
|
72
|
+
from mcp_server.config import config
|
|
73
|
+
|
|
74
|
+
if not config.chroma_dir.exists():
|
|
75
|
+
print("missing")
|
|
76
|
+
raise SystemExit(0)
|
|
77
|
+
|
|
78
|
+
client = chromadb.PersistentClient(path=str(config.chroma_dir))
|
|
79
|
+
collection = client.get_or_create_collection(name=config.collection_name)
|
|
80
|
+
print(collection.count())
|
|
81
|
+
"""
|
|
82
|
+
env = os.environ.copy()
|
|
83
|
+
env.setdefault("KNOWLEDGE_RAG_DIR", str(BASE_DIR))
|
|
84
|
+
return subprocess.run(
|
|
85
|
+
[sys.executable, "-c", code],
|
|
86
|
+
cwd=str(BASE_DIR),
|
|
87
|
+
env=env,
|
|
88
|
+
text=True,
|
|
89
|
+
stdout=subprocess.PIPE,
|
|
90
|
+
stderr=subprocess.PIPE,
|
|
91
|
+
timeout=timeout_seconds,
|
|
92
|
+
check=False,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def run_preflight(timeout_seconds: int = PROBE_TIMEOUT_SECONDS) -> bool:
|
|
97
|
+
"""Return True when active Chroma state was moved aside for repair.
|
|
98
|
+
|
|
99
|
+
Runs the probe up to PROBE_ATTEMPTS times so transient failures
|
|
100
|
+
(file locks lingering from a previous PID, slow disk, cold GPU init)
|
|
101
|
+
do not trigger a destructive quarantine. TimeoutExpired is treated
|
|
102
|
+
as inconclusive — the index is kept and the user can debug from
|
|
103
|
+
preflight.log without losing the corpus. Only PROBE_ATTEMPTS
|
|
104
|
+
consecutive non-zero exits quarantine.
|
|
105
|
+
|
|
106
|
+
If os.rename fails while quarantining (e.g. another process still
|
|
107
|
+
holds a segment file open on Windows) the index is left untouched
|
|
108
|
+
and the process exits with EX_TEMPFAIL (75) instead of crashing
|
|
109
|
+
halfway with a partial move.
|
|
110
|
+
"""
|
|
111
|
+
last_result: subprocess.CompletedProcess[str] | None = None
|
|
112
|
+
for attempt in range(1, PROBE_ATTEMPTS + 1):
|
|
113
|
+
try:
|
|
114
|
+
result = _probe_chroma(timeout_seconds=timeout_seconds)
|
|
115
|
+
except subprocess.TimeoutExpired:
|
|
116
|
+
_log(
|
|
117
|
+
f"[PREFLIGHT] attempt {attempt}/{PROBE_ATTEMPTS}: probe timed out "
|
|
118
|
+
f"after {timeout_seconds}s — inconclusive, keeping index"
|
|
119
|
+
)
|
|
120
|
+
return False
|
|
121
|
+
if result.returncode == 0:
|
|
122
|
+
if attempt > 1:
|
|
123
|
+
_log(f"[PREFLIGHT] attempt {attempt}: OK (recovered from transient failure)")
|
|
124
|
+
return False
|
|
125
|
+
stderr_tail = (result.stderr or "").strip()[-1500:]
|
|
126
|
+
_log(
|
|
127
|
+
f"[PREFLIGHT] attempt {attempt}/{PROBE_ATTEMPTS}: probe failed with code {result.returncode}\n{stderr_tail}"
|
|
128
|
+
)
|
|
129
|
+
last_result = result
|
|
130
|
+
if attempt < PROBE_ATTEMPTS:
|
|
131
|
+
time.sleep(PROBE_RETRY_DELAY_SECONDS)
|
|
132
|
+
|
|
133
|
+
assert last_result is not None
|
|
134
|
+
reason = "segfault" if last_result.returncode in (-11, 139) else "failed"
|
|
135
|
+
_log(f"[RECOVERY] preflight failed {PROBE_ATTEMPTS}x (last exit code {last_result.returncode}); moving index aside")
|
|
136
|
+
try:
|
|
137
|
+
backup_dir = _backup_active_index(reason)
|
|
138
|
+
except OSError as exc:
|
|
139
|
+
_log(
|
|
140
|
+
f"[PREFLIGHT] could not move index aside ({exc}); leaving it untouched "
|
|
141
|
+
f"to avoid partial corruption. Retry after freeing file handles."
|
|
142
|
+
)
|
|
143
|
+
raise SystemExit(75) from exc
|
|
144
|
+
_log(f"[RECOVERY] moved active index to {backup_dir}")
|
|
145
|
+
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
|
-
|
|
4797
|
-
|
|
4798
|
-
|
|
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.
|
|
7
|
+
version = "4.9.1"
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|