synap-git 2.1.0__tar.gz → 2.1.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.
- {synap_git-2.1.0 → synap_git-2.1.2}/CHANGELOG.md +12 -0
- {synap_git-2.1.0 → synap_git-2.1.2}/PKG-INFO +1 -1
- {synap_git-2.1.0 → synap_git-2.1.2}/src/synap_git/__init__.py +1 -1
- {synap_git-2.1.0 → synap_git-2.1.2}/src/synap_git/cli/main.py +1 -1
- {synap_git-2.1.0 → synap_git-2.1.2}/src/synap_git/indexer/daemon.py +1 -0
- {synap_git-2.1.0 → synap_git-2.1.2}/src/synap_git/indexer/engine.py +13 -10
- {synap_git-2.1.0 → synap_git-2.1.2}/src/synap_git/mcp/server.py +5 -1
- {synap_git-2.1.0 → synap_git-2.1.2}/src/synap_git/retrieval/engine.py +29 -24
- {synap_git-2.1.0 → synap_git-2.1.2}/src/synap_git/storage/sqlite.py +13 -4
- {synap_git-2.1.0 → synap_git-2.1.2}/.gitignore +0 -0
- {synap_git-2.1.0 → synap_git-2.1.2}/.synap.example/README.md +0 -0
- {synap_git-2.1.0 → synap_git-2.1.2}/LICENSE.md +0 -0
- {synap_git-2.1.0 → synap_git-2.1.2}/README.md +0 -0
- {synap_git-2.1.0 → synap_git-2.1.2}/pyproject.toml +0 -0
- {synap_git-2.1.0 → synap_git-2.1.2}/src/synap_git/api/__init__.py +0 -0
- {synap_git-2.1.0 → synap_git-2.1.2}/src/synap_git/api/app.py +0 -0
- {synap_git-2.1.0 → synap_git-2.1.2}/src/synap_git/api/static/index.html +0 -0
- {synap_git-2.1.0 → synap_git-2.1.2}/src/synap_git/cli/__init__.py +0 -0
- {synap_git-2.1.0 → synap_git-2.1.2}/src/synap_git/cli/__main__.py +0 -0
- {synap_git-2.1.0 → synap_git-2.1.2}/src/synap_git/config.py +0 -0
- {synap_git-2.1.0 → synap_git-2.1.2}/src/synap_git/diagnostics/__init__.py +0 -0
- {synap_git-2.1.0 → synap_git-2.1.2}/src/synap_git/diagnostics/logger.py +0 -0
- {synap_git-2.1.0 → synap_git-2.1.2}/src/synap_git/diagnostics/tracing.py +0 -0
- {synap_git-2.1.0 → synap_git-2.1.2}/src/synap_git/embeddings/__init__.py +0 -0
- {synap_git-2.1.0 → synap_git-2.1.2}/src/synap_git/git/__init__.py +0 -0
- {synap_git-2.1.0 → synap_git-2.1.2}/src/synap_git/git/state.py +0 -0
- {synap_git-2.1.0 → synap_git-2.1.2}/src/synap_git/indexer/__init__.py +0 -0
- {synap_git-2.1.0 → synap_git-2.1.2}/src/synap_git/indexer/scanner.py +0 -0
- {synap_git-2.1.0 → synap_git-2.1.2}/src/synap_git/indexer/wiki.py +0 -0
- {synap_git-2.1.0 → synap_git-2.1.2}/src/synap_git/mcp/__init__.py +0 -0
- {synap_git-2.1.0 → synap_git-2.1.2}/src/synap_git/parser/__init__.py +0 -0
- {synap_git-2.1.0 → synap_git-2.1.2}/src/synap_git/parser/registry.py +0 -0
- {synap_git-2.1.0 → synap_git-2.1.2}/src/synap_git/provider/anthropic.py +0 -0
- {synap_git-2.1.0 → synap_git-2.1.2}/src/synap_git/provider/base.py +0 -0
- {synap_git-2.1.0 → synap_git-2.1.2}/src/synap_git/provider/factory.py +0 -0
- {synap_git-2.1.0 → synap_git-2.1.2}/src/synap_git/provider/gemini.py +0 -0
- {synap_git-2.1.0 → synap_git-2.1.2}/src/synap_git/provider/ollama.py +0 -0
- {synap_git-2.1.0 → synap_git-2.1.2}/src/synap_git/provider/openai.py +0 -0
- {synap_git-2.1.0 → synap_git-2.1.2}/src/synap_git/provider/openrouter.py +0 -0
- {synap_git-2.1.0 → synap_git-2.1.2}/src/synap_git/py.typed +0 -0
- {synap_git-2.1.0 → synap_git-2.1.2}/src/synap_git/retrieval/__init__.py +0 -0
- {synap_git-2.1.0 → synap_git-2.1.2}/src/synap_git/retrieval/memory.py +0 -0
- {synap_git-2.1.0 → synap_git-2.1.2}/src/synap_git/storage/__init__.py +0 -0
- {synap_git-2.1.0 → synap_git-2.1.2}/src/synap_git/utils/__init__.py +0 -0
- {synap_git-2.1.0 → synap_git-2.1.2}/src/synap_git/utils/serialization.py +0 -0
|
@@ -5,6 +5,18 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [2.1.2] - 2026-06-11
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- **CRITICAL: Daemon Thread Explosion**: Patched a massive concurrency edge case in `SynapRuntime` where large repository indexing would trigger unbounded `threading.Thread` loops for LLM embeddings. A bounded `ThreadPoolExecutor` has been introduced to prevent OOMs, OS thread starvation, and rate-limit hits during first-run indexing.
|
|
12
|
+
- **Daemon Lifecycle**: Ensure `embedding_executor` shuts down cleanly without leaking background threads when the daemon loop receives SIGTERM.
|
|
13
|
+
|
|
14
|
+
## [2.1.1] - 2026-06-11
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
- **CRITICAL: MCP StdIO Stream Corruption**: Fixed a bug where `synap mcp start` and `signal_low_context` would accidentally print progress bars or notifications to `sys.stdout` instead of `sys.stderr`, which corrupted the MCP protocol stream and disconnected IDEs like Cursor/Windsurf.
|
|
18
|
+
- **UX Improvements**: Improved the silence logic in CLI initialization to prevent unnecessary rich formatting in JSON/MCP environments.
|
|
19
|
+
|
|
8
20
|
## [2.1.0] - 2026-05-29
|
|
9
21
|
|
|
10
22
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: synap-git
|
|
3
|
-
Version: 2.1.
|
|
3
|
+
Version: 2.1.2
|
|
4
4
|
Summary: Persistent structural context infrastructure for AI coding agents.
|
|
5
5
|
Project-URL: Homepage, https://github.com/saahilpal/synap-git
|
|
6
6
|
Project-URL: Repository, https://github.com/saahilpal/synap-git
|
|
@@ -1470,7 +1470,7 @@ def mcp_start(
|
|
|
1470
1470
|
path: Annotated[str, typer.Argument(help="Repository path.")] = ".",
|
|
1471
1471
|
) -> None:
|
|
1472
1472
|
"""Start the MCP server."""
|
|
1473
|
-
settings = _settings(path)
|
|
1473
|
+
settings = _settings(path, json_output=True)
|
|
1474
1474
|
runtime = SynapRuntime(settings)
|
|
1475
1475
|
runtime.bootstrap()
|
|
1476
1476
|
|
|
@@ -81,6 +81,17 @@ class SynapRuntime:
|
|
|
81
81
|
self.wiki = WikiEngine(settings, self.store)
|
|
82
82
|
self.commit_count = 0
|
|
83
83
|
|
|
84
|
+
import concurrent.futures
|
|
85
|
+
|
|
86
|
+
self.embedding_executor = concurrent.futures.ThreadPoolExecutor(
|
|
87
|
+
max_workers=5, thread_name_prefix="embed_worker"
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
def shutdown(self) -> None:
|
|
91
|
+
"""Gracefully shutdown background tasks and executors."""
|
|
92
|
+
self.logger.info("shutting_down_embedding_executor")
|
|
93
|
+
self.embedding_executor.shutdown(wait=False)
|
|
94
|
+
|
|
84
95
|
def initialize_storage(self, *, auto_recover: bool = True) -> None:
|
|
85
96
|
self.settings.ensure_directories()
|
|
86
97
|
if auto_recover and self.store.path.exists() and self.store.recover_if_corrupted():
|
|
@@ -284,15 +295,7 @@ class SynapRuntime:
|
|
|
284
295
|
except Exception as e:
|
|
285
296
|
logger.warning("background_embed_failed", error=str(e))
|
|
286
297
|
|
|
287
|
-
|
|
288
|
-
import asyncio
|
|
289
|
-
|
|
290
|
-
loop = asyncio.get_running_loop()
|
|
291
|
-
loop.create_task(asyncio.to_thread(_worker))
|
|
292
|
-
except RuntimeError:
|
|
293
|
-
import threading
|
|
294
|
-
|
|
295
|
-
threading.Thread(target=_worker, daemon=True).start()
|
|
298
|
+
self.embedding_executor.submit(_worker)
|
|
296
299
|
|
|
297
300
|
def _first_run_index(self, git_state: GitState) -> str | None:
|
|
298
301
|
self.logger.info("first_run_indexing_started", branch=git_state.effective_branch)
|
|
@@ -317,7 +320,7 @@ class SynapRuntime:
|
|
|
317
320
|
from rich.console import Console
|
|
318
321
|
from rich.progress import BarColumn, Progress, SpinnerColumn, TextColumn, TimeElapsedColumn
|
|
319
322
|
|
|
320
|
-
console = Console()
|
|
323
|
+
console = Console(stderr=True)
|
|
321
324
|
show_progress = self.settings.logging_mode.name == "HUMAN" and num_files > 5
|
|
322
325
|
|
|
323
326
|
if show_progress:
|
|
@@ -83,7 +83,11 @@ class SynapMCPFacade:
|
|
|
83
83
|
if should_checkpoint:
|
|
84
84
|
message += f"Threshold ({threshold:.1%}) reached. Checkpoint recommended."
|
|
85
85
|
# Emit CLI notification
|
|
86
|
-
|
|
86
|
+
import sys
|
|
87
|
+
|
|
88
|
+
sys.stderr.write(
|
|
89
|
+
f"\n[Synapse] ⚠ Agent at {percentage:.0%} context — checkpoint recommended.\n"
|
|
90
|
+
)
|
|
87
91
|
else:
|
|
88
92
|
message += f"Context is within limits (threshold {threshold:.1%})."
|
|
89
93
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import functools
|
|
3
4
|
from dataclasses import dataclass
|
|
4
5
|
from datetime import UTC, datetime
|
|
5
6
|
from pathlib import Path
|
|
@@ -22,23 +23,32 @@ class TraceElement:
|
|
|
22
23
|
tokens: int
|
|
23
24
|
|
|
24
25
|
|
|
25
|
-
|
|
26
|
+
@functools.lru_cache(maxsize=256)
|
|
27
|
+
def _get_file_lines(repo_path: Path, rel_path: str) -> list[str] | None:
|
|
26
28
|
try:
|
|
27
29
|
p = repo_path / rel_path
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
return p.read_text(encoding="utf-8", errors="replace").splitlines()
|
|
31
|
+
except Exception as e:
|
|
32
|
+
import structlog
|
|
33
|
+
|
|
34
|
+
structlog.get_logger().warning("file_read_failed", path=rel_path, error=str(e))
|
|
35
|
+
return None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _get_snippet(repo_path: Path, rel_path: str, start_line: int, end_line: int) -> str:
|
|
39
|
+
lines = _get_file_lines(repo_path, rel_path)
|
|
40
|
+
if lines is None:
|
|
33
41
|
return ""
|
|
42
|
+
s_idx = max(0, start_line - 1)
|
|
43
|
+
e_idx = min(len(lines), end_line)
|
|
44
|
+
return "\n".join(lines[s_idx:e_idx])
|
|
34
45
|
|
|
35
46
|
|
|
36
47
|
def _get_full_file(repo_path: Path, rel_path: str) -> str:
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
return p.read_text(encoding="utf-8", errors="replace")
|
|
40
|
-
except Exception:
|
|
48
|
+
lines = _get_file_lines(repo_path, rel_path)
|
|
49
|
+
if lines is None:
|
|
41
50
|
return ""
|
|
51
|
+
return "\n".join(lines)
|
|
42
52
|
|
|
43
53
|
|
|
44
54
|
class HybridRetrievalEngine:
|
|
@@ -93,20 +103,15 @@ class HybridRetrievalEngine:
|
|
|
93
103
|
# 2. Lexical Matches (BM25)
|
|
94
104
|
query_words = {word.lower().strip() for word in query.split() if len(word) > 2}
|
|
95
105
|
lexical_candidates: dict[str, dict[str, Any]] = {}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
}
|
|
106
|
-
else:
|
|
107
|
-
lexical_candidates[sid]["lexical_rank"] = min(
|
|
108
|
-
lexical_candidates[sid].get("lexical_rank", rank), rank
|
|
109
|
-
)
|
|
106
|
+
symbols = self.store.get_symbols_by_lexical_query(query_words)
|
|
107
|
+
for rank, sym in enumerate(symbols):
|
|
108
|
+
sid = sym["symbol_id"]
|
|
109
|
+
if sid not in lexical_candidates:
|
|
110
|
+
lexical_candidates[sid] = {
|
|
111
|
+
**sym,
|
|
112
|
+
"reason": "lexical_match",
|
|
113
|
+
"lexical_rank": rank,
|
|
114
|
+
}
|
|
110
115
|
|
|
111
116
|
t_lexical = time.perf_counter()
|
|
112
117
|
|
|
@@ -45,7 +45,7 @@ class SynapStore:
|
|
|
45
45
|
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
46
46
|
with self.connect() as conn:
|
|
47
47
|
conn.execute("PRAGMA journal_mode=WAL")
|
|
48
|
-
conn.execute("PRAGMA synchronous=
|
|
48
|
+
conn.execute("PRAGMA synchronous=NORMAL")
|
|
49
49
|
conn.execute("PRAGMA foreign_keys=ON")
|
|
50
50
|
|
|
51
51
|
# Get current user version
|
|
@@ -287,7 +287,7 @@ class SynapStore:
|
|
|
287
287
|
conn.create_function("cosine_similarity", 2, _cosine_similarity)
|
|
288
288
|
try:
|
|
289
289
|
conn.execute("PRAGMA foreign_keys=ON")
|
|
290
|
-
conn.execute("PRAGMA synchronous=
|
|
290
|
+
conn.execute("PRAGMA synchronous=NORMAL")
|
|
291
291
|
yield conn
|
|
292
292
|
conn.commit()
|
|
293
293
|
except Exception:
|
|
@@ -395,7 +395,16 @@ class SynapStore:
|
|
|
395
395
|
).fetchall()
|
|
396
396
|
return [dict(row) for row in rows]
|
|
397
397
|
|
|
398
|
-
def
|
|
398
|
+
def get_symbols_by_lexical_query(
|
|
399
|
+
self, query_words: set[str], limit: int = 50
|
|
400
|
+
) -> list[dict[str, Any]]:
|
|
401
|
+
if not query_words:
|
|
402
|
+
return []
|
|
403
|
+
parts = []
|
|
404
|
+
for w in query_words:
|
|
405
|
+
parts.append(f'"{w}"*')
|
|
406
|
+
parts.append(f'body:"{w}"*')
|
|
407
|
+
match_expr = " OR ".join(parts)
|
|
399
408
|
with self.connect() as conn:
|
|
400
409
|
rows = conn.execute(
|
|
401
410
|
"""
|
|
@@ -407,7 +416,7 @@ class SynapStore:
|
|
|
407
416
|
ORDER BY rank
|
|
408
417
|
LIMIT ?
|
|
409
418
|
""",
|
|
410
|
-
(
|
|
419
|
+
(match_expr, limit),
|
|
411
420
|
).fetchall()
|
|
412
421
|
return [dict(row) for row in rows]
|
|
413
422
|
|
|
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
|
|
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
|