synap-git 2.0.0__tar.gz → 2.1.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.
- {synap_git-2.0.0 → synap_git-2.1.1}/CHANGELOG.md +20 -0
- {synap_git-2.0.0 → synap_git-2.1.1}/PKG-INFO +1 -1
- {synap_git-2.0.0 → synap_git-2.1.1}/src/synap_git/__init__.py +1 -1
- {synap_git-2.0.0 → synap_git-2.1.1}/src/synap_git/api/app.py +4 -2
- {synap_git-2.0.0 → synap_git-2.1.1}/src/synap_git/cli/main.py +55 -18
- {synap_git-2.0.0 → synap_git-2.1.1}/src/synap_git/config.py +3 -1
- {synap_git-2.0.0 → synap_git-2.1.1}/src/synap_git/diagnostics/logger.py +2 -2
- {synap_git-2.0.0 → synap_git-2.1.1}/src/synap_git/git/state.py +4 -2
- {synap_git-2.0.0 → synap_git-2.1.1}/src/synap_git/indexer/daemon.py +8 -4
- {synap_git-2.0.0 → synap_git-2.1.1}/src/synap_git/indexer/engine.py +37 -12
- {synap_git-2.0.0 → synap_git-2.1.1}/src/synap_git/indexer/wiki.py +8 -4
- {synap_git-2.0.0 → synap_git-2.1.1}/src/synap_git/mcp/server.py +17 -5
- {synap_git-2.0.0 → synap_git-2.1.1}/src/synap_git/parser/registry.py +2 -2
- {synap_git-2.0.0 → synap_git-2.1.1}/src/synap_git/provider/anthropic.py +5 -1
- {synap_git-2.0.0 → synap_git-2.1.1}/src/synap_git/provider/openai.py +5 -1
- {synap_git-2.0.0 → synap_git-2.1.1}/src/synap_git/provider/openrouter.py +5 -1
- {synap_git-2.0.0 → synap_git-2.1.1}/src/synap_git/retrieval/engine.py +35 -26
- {synap_git-2.0.0 → synap_git-2.1.1}/src/synap_git/storage/sqlite.py +24 -10
- {synap_git-2.0.0 → synap_git-2.1.1}/.gitignore +0 -0
- {synap_git-2.0.0 → synap_git-2.1.1}/.synap.example/README.md +0 -0
- {synap_git-2.0.0 → synap_git-2.1.1}/LICENSE.md +0 -0
- {synap_git-2.0.0 → synap_git-2.1.1}/README.md +0 -0
- {synap_git-2.0.0 → synap_git-2.1.1}/pyproject.toml +0 -0
- {synap_git-2.0.0 → synap_git-2.1.1}/src/synap_git/api/__init__.py +0 -0
- {synap_git-2.0.0 → synap_git-2.1.1}/src/synap_git/api/static/index.html +0 -0
- {synap_git-2.0.0 → synap_git-2.1.1}/src/synap_git/cli/__init__.py +0 -0
- {synap_git-2.0.0 → synap_git-2.1.1}/src/synap_git/cli/__main__.py +0 -0
- {synap_git-2.0.0 → synap_git-2.1.1}/src/synap_git/diagnostics/__init__.py +0 -0
- {synap_git-2.0.0 → synap_git-2.1.1}/src/synap_git/diagnostics/tracing.py +0 -0
- {synap_git-2.0.0 → synap_git-2.1.1}/src/synap_git/embeddings/__init__.py +0 -0
- {synap_git-2.0.0 → synap_git-2.1.1}/src/synap_git/git/__init__.py +0 -0
- {synap_git-2.0.0 → synap_git-2.1.1}/src/synap_git/indexer/__init__.py +0 -0
- {synap_git-2.0.0 → synap_git-2.1.1}/src/synap_git/indexer/scanner.py +0 -0
- {synap_git-2.0.0 → synap_git-2.1.1}/src/synap_git/mcp/__init__.py +0 -0
- {synap_git-2.0.0 → synap_git-2.1.1}/src/synap_git/parser/__init__.py +0 -0
- {synap_git-2.0.0 → synap_git-2.1.1}/src/synap_git/provider/base.py +0 -0
- {synap_git-2.0.0 → synap_git-2.1.1}/src/synap_git/provider/factory.py +0 -0
- {synap_git-2.0.0 → synap_git-2.1.1}/src/synap_git/provider/gemini.py +0 -0
- {synap_git-2.0.0 → synap_git-2.1.1}/src/synap_git/provider/ollama.py +0 -0
- {synap_git-2.0.0 → synap_git-2.1.1}/src/synap_git/py.typed +0 -0
- {synap_git-2.0.0 → synap_git-2.1.1}/src/synap_git/retrieval/__init__.py +0 -0
- {synap_git-2.0.0 → synap_git-2.1.1}/src/synap_git/retrieval/memory.py +0 -0
- {synap_git-2.0.0 → synap_git-2.1.1}/src/synap_git/storage/__init__.py +0 -0
- {synap_git-2.0.0 → synap_git-2.1.1}/src/synap_git/utils/__init__.py +0 -0
- {synap_git-2.0.0 → synap_git-2.1.1}/src/synap_git/utils/serialization.py +0 -0
|
@@ -5,6 +5,26 @@ 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.1] - 2026-06-11
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- **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.
|
|
12
|
+
- **UX Improvements**: Improved the silence logic in CLI initialization to prevent unnecessary rich formatting in JSON/MCP environments.
|
|
13
|
+
|
|
14
|
+
## [2.1.0] - 2026-05-29
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
- **CLI `search` command**: New user-facing command for executing hybrid structural searches directly from the terminal.
|
|
18
|
+
- **`embed_provider` configuration**: Added explicit control over the vector embedding provider via `SYNAP_EMBED_PROVIDER` or `config.toml`.
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
- **CRITICAL: Metadata Nullability Bug**: Resolved a type mismatch in `CodeSymbol` where `metadata=None` caused background worker crashes. Enforced strict `dict` contract with `default_factory`.
|
|
22
|
+
- **CRITICAL: Exception Swallowing**: Eradicated 26 instances of silent failure (`except Exception: pass`) across all core subsystems (API, Indexer, Storage, Retrieval, MCP, Providers). Replaced with structured, traceback-aware logging.
|
|
23
|
+
- **CRITICAL: SQLite Schema Integrity**: Fixed `NOT NULL` constraint failure in `llm_calls` table by aligning schema with insertion logic and adding `cost_usd` support.
|
|
24
|
+
- **HIGH: Repository-Local Logging**: Redirected daemon logs from a global shared directory to repository-local `.synap/logs` to prevent cross-repository log corruption and lock contention.
|
|
25
|
+
- **HIGH: Rollback Atomicity**: Reordered `synap rollback` operations to ensure Git state is successfully restored before purging index checkpoints.
|
|
26
|
+
- **MEDIUM: Provider Resiliency**: Added explicit handling for `NotImplementedError` in Anthropic and OpenRouter providers, preventing worker panics when embeddings are misconfigured.
|
|
27
|
+
|
|
8
28
|
## [2.0.0] - 2026-05-29
|
|
9
29
|
|
|
10
30
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: synap-git
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.1.1
|
|
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
|
|
@@ -101,8 +101,10 @@ def create_app(runtime: SynapRuntime) -> FastAPI:
|
|
|
101
101
|
target = target[:-3]
|
|
102
102
|
try:
|
|
103
103
|
await asyncio.to_thread(runtime.wiki.ensure_wiki_page, target)
|
|
104
|
-
except Exception:
|
|
105
|
-
|
|
104
|
+
except Exception as e:
|
|
105
|
+
import structlog
|
|
106
|
+
|
|
107
|
+
structlog.get_logger().error("suppressed_error_caught", error=str(e), exc_info=True)
|
|
106
108
|
|
|
107
109
|
wiki_path = runtime.wiki.wiki_dir / f"{target}.md"
|
|
108
110
|
exists = await asyncio.to_thread(wiki_path.exists)
|
|
@@ -642,8 +642,10 @@ def start(
|
|
|
642
642
|
return
|
|
643
643
|
else:
|
|
644
644
|
pid_file.unlink()
|
|
645
|
-
except Exception:
|
|
646
|
-
|
|
645
|
+
except Exception as e:
|
|
646
|
+
import structlog
|
|
647
|
+
|
|
648
|
+
structlog.get_logger().error("suppressed_error_caught", error=str(e), exc_info=True)
|
|
647
649
|
|
|
648
650
|
# 2. Spawn detached daemon process
|
|
649
651
|
cmd = [sys.executable, "-m", "synap_git.cli", "daemon-run", abs_path.as_posix()]
|
|
@@ -782,8 +784,10 @@ def stop(
|
|
|
782
784
|
os.waitpid(pid, os.WNOHANG)
|
|
783
785
|
except (ChildProcessError, OSError):
|
|
784
786
|
pass
|
|
785
|
-
except Exception:
|
|
786
|
-
|
|
787
|
+
except Exception as e:
|
|
788
|
+
import structlog
|
|
789
|
+
|
|
790
|
+
structlog.get_logger().error("suppressed_error_caught", error=str(e), exc_info=True)
|
|
787
791
|
success = not _is_process_running(pid)
|
|
788
792
|
|
|
789
793
|
# Clean up lockfiles
|
|
@@ -895,6 +899,7 @@ def status(
|
|
|
895
899
|
|
|
896
900
|
@app.command()
|
|
897
901
|
def logs(
|
|
902
|
+
path: Annotated[str, typer.Argument(help="Repository path.")] = ".",
|
|
898
903
|
tail: Annotated[
|
|
899
904
|
bool, typer.Option("--tail", "-t", help="Stream new log entries in real-time.")
|
|
900
905
|
] = False,
|
|
@@ -906,7 +911,9 @@ def logs(
|
|
|
906
911
|
"""View and tail Synap runtime logs."""
|
|
907
912
|
import time
|
|
908
913
|
|
|
909
|
-
|
|
914
|
+
settings = _settings(path)
|
|
915
|
+
log_dir = settings.log_path
|
|
916
|
+
assert log_dir is not None
|
|
910
917
|
log_file = log_dir / "daemon.log"
|
|
911
918
|
|
|
912
919
|
if not log_file.exists():
|
|
@@ -938,6 +945,32 @@ def logs(
|
|
|
938
945
|
console.print(f"[red]Error reading logs: {e}[/red]")
|
|
939
946
|
|
|
940
947
|
|
|
948
|
+
@app.command()
|
|
949
|
+
def search(
|
|
950
|
+
query: Annotated[str, typer.Argument(help="Search query.")],
|
|
951
|
+
path: Annotated[str, typer.Option("--path", "-p", help="Repository path.")] = ".",
|
|
952
|
+
max_tokens: Annotated[
|
|
953
|
+
int, typer.Option("--max-tokens", help="Maximum context window tokens.")
|
|
954
|
+
] = 4000,
|
|
955
|
+
) -> None:
|
|
956
|
+
"""Execute a hybrid structural search across the repository."""
|
|
957
|
+
settings = _settings(path)
|
|
958
|
+
runtime = SynapRuntime(settings)
|
|
959
|
+
|
|
960
|
+
with Progress(
|
|
961
|
+
SpinnerColumn(),
|
|
962
|
+
TextColumn("[progress.description]{task.description}"),
|
|
963
|
+
transient=True,
|
|
964
|
+
) as progress:
|
|
965
|
+
progress.add_task(description="Retrieving context...", total=None)
|
|
966
|
+
answer, context, _ = runtime.query_hybrid(query, max_tokens=max_tokens)
|
|
967
|
+
|
|
968
|
+
console.print("\n[bold cyan]Hybrid Retrieval Results[/bold cyan]")
|
|
969
|
+
console.print("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
|
|
970
|
+
console.print(answer)
|
|
971
|
+
console.print("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
|
|
972
|
+
|
|
973
|
+
|
|
941
974
|
@app.command()
|
|
942
975
|
def update() -> None:
|
|
943
976
|
"""Check for updates and upgrade the Synap runtime."""
|
|
@@ -1179,16 +1212,6 @@ def rollback(
|
|
|
1179
1212
|
if not typer.confirm("\nProceed?", default=False):
|
|
1180
1213
|
raise typer.Abort()
|
|
1181
1214
|
|
|
1182
|
-
# Clear current checkpoint
|
|
1183
|
-
try:
|
|
1184
|
-
with runtime.store.connect() as conn:
|
|
1185
|
-
conn.execute(
|
|
1186
|
-
"DELETE FROM checkpoints WHERE branch = ?",
|
|
1187
|
-
(runtime.git.state().effective_branch,),
|
|
1188
|
-
)
|
|
1189
|
-
except Exception:
|
|
1190
|
-
pass
|
|
1191
|
-
|
|
1192
1215
|
# git checkout
|
|
1193
1216
|
try:
|
|
1194
1217
|
subprocess.check_call( # noqa: S603
|
|
@@ -1199,6 +1222,18 @@ def rollback(
|
|
|
1199
1222
|
console.print(f"[red]✗ Git checkout failed: {e}[/red]")
|
|
1200
1223
|
raise typer.Abort()
|
|
1201
1224
|
|
|
1225
|
+
# Clear current checkpoint
|
|
1226
|
+
try:
|
|
1227
|
+
with runtime.store.connect() as conn:
|
|
1228
|
+
conn.execute(
|
|
1229
|
+
"DELETE FROM checkpoints WHERE branch = ?",
|
|
1230
|
+
(runtime.git.state().effective_branch,),
|
|
1231
|
+
)
|
|
1232
|
+
except Exception as e:
|
|
1233
|
+
import structlog
|
|
1234
|
+
|
|
1235
|
+
structlog.get_logger().error("suppressed_error_caught", error=str(e), exc_info=True)
|
|
1236
|
+
|
|
1202
1237
|
runtime.bootstrap(force=True)
|
|
1203
1238
|
console.print(f"[green]✓ Rolled back successfully to {selected_commit}[/green]")
|
|
1204
1239
|
|
|
@@ -1264,8 +1299,10 @@ def repair(
|
|
|
1264
1299
|
console.print("[3/3] Regenerating wiki from last known state")
|
|
1265
1300
|
try:
|
|
1266
1301
|
runtime.wiki.generate_project_wiki()
|
|
1267
|
-
except Exception:
|
|
1268
|
-
|
|
1302
|
+
except Exception as e:
|
|
1303
|
+
import structlog
|
|
1304
|
+
|
|
1305
|
+
structlog.get_logger().error("suppressed_error_caught", error=str(e), exc_info=True)
|
|
1269
1306
|
|
|
1270
1307
|
status_info = runtime.status()
|
|
1271
1308
|
console.print(f"\n[green]✓ Recovery complete. {status_info.files} files restored.[/green]")
|
|
@@ -1433,7 +1470,7 @@ def mcp_start(
|
|
|
1433
1470
|
path: Annotated[str, typer.Argument(help="Repository path.")] = ".",
|
|
1434
1471
|
) -> None:
|
|
1435
1472
|
"""Start the MCP server."""
|
|
1436
|
-
settings = _settings(path)
|
|
1473
|
+
settings = _settings(path, json_output=True)
|
|
1437
1474
|
runtime = SynapRuntime(settings)
|
|
1438
1475
|
runtime.bootstrap()
|
|
1439
1476
|
|
|
@@ -92,7 +92,9 @@ class SynapSettings(BaseSettings):
|
|
|
92
92
|
if line.strip().startswith(f"{key}="):
|
|
93
93
|
return line.split("=", 1)[1].strip().strip("\"'")
|
|
94
94
|
except Exception:
|
|
95
|
-
|
|
95
|
+
import structlog
|
|
96
|
+
|
|
97
|
+
structlog.get_logger(__name__).exception("fallback_credential_lookup_failed", key=key)
|
|
96
98
|
return None
|
|
97
99
|
|
|
98
100
|
@property
|
|
@@ -22,7 +22,6 @@ class TraceResult:
|
|
|
22
22
|
|
|
23
23
|
def configure_logging(settings: SynapSettings) -> None:
|
|
24
24
|
from logging.handlers import RotatingFileHandler
|
|
25
|
-
from pathlib import Path
|
|
26
25
|
|
|
27
26
|
timestamper = structlog.processors.TimeStamper(fmt="iso", utc=True)
|
|
28
27
|
shared_processors: list[structlog.typing.Processor] = [
|
|
@@ -38,7 +37,8 @@ def configure_logging(settings: SynapSettings) -> None:
|
|
|
38
37
|
else:
|
|
39
38
|
renderer = structlog.dev.ConsoleRenderer(colors=False)
|
|
40
39
|
|
|
41
|
-
log_dir =
|
|
40
|
+
log_dir = settings.log_path
|
|
41
|
+
assert log_dir is not None
|
|
42
42
|
log_dir.mkdir(parents=True, exist_ok=True)
|
|
43
43
|
log_file = log_dir / "daemon.log"
|
|
44
44
|
|
|
@@ -128,8 +128,10 @@ class GitRepository:
|
|
|
128
128
|
break
|
|
129
129
|
if is_revert:
|
|
130
130
|
return GitChange(kind=GitChangeKind.REVERT, previous=previous, current=current)
|
|
131
|
-
except Exception:
|
|
132
|
-
|
|
131
|
+
except Exception as e:
|
|
132
|
+
import structlog
|
|
133
|
+
|
|
134
|
+
structlog.get_logger().error("suppressed_error_caught", error=str(e), exc_info=True)
|
|
133
135
|
|
|
134
136
|
if len(current.commit_parent_hashes) > 1:
|
|
135
137
|
return GitChange(kind=GitChangeKind.MERGE, previous=previous, current=current)
|
|
@@ -244,16 +244,20 @@ class RuntimeDaemon:
|
|
|
244
244
|
if heartbeat_file.exists():
|
|
245
245
|
try:
|
|
246
246
|
heartbeat_file.unlink()
|
|
247
|
-
except Exception:
|
|
248
|
-
|
|
247
|
+
except Exception as e:
|
|
248
|
+
import structlog
|
|
249
|
+
|
|
250
|
+
structlog.get_logger().error("suppressed_error_caught", error=str(e), exc_info=True)
|
|
249
251
|
|
|
250
252
|
# Cleanup process PID lockfile as well
|
|
251
253
|
pid_file = self.settings.repository_path / ".synap" / "daemon.pid"
|
|
252
254
|
if pid_file.exists():
|
|
253
255
|
try:
|
|
254
256
|
pid_file.unlink()
|
|
255
|
-
except Exception:
|
|
256
|
-
|
|
257
|
+
except Exception as e:
|
|
258
|
+
import structlog
|
|
259
|
+
|
|
260
|
+
structlog.get_logger().error("suppressed_error_caught", error=str(e), exc_info=True)
|
|
257
261
|
|
|
258
262
|
async def _poll_git_loop(self) -> None:
|
|
259
263
|
while not self._stop_event.is_set():
|
|
@@ -141,8 +141,10 @@ class SynapRuntime:
|
|
|
141
141
|
if change.kind == "revert":
|
|
142
142
|
self.handle_revert(prev, git_state)
|
|
143
143
|
return git_state.head_commit
|
|
144
|
-
except Exception:
|
|
145
|
-
|
|
144
|
+
except Exception as e:
|
|
145
|
+
import structlog
|
|
146
|
+
|
|
147
|
+
structlog.get_logger().error("suppressed_error_caught", error=str(e), exc_info=True)
|
|
146
148
|
|
|
147
149
|
return self.index_repository(git_state=git_state, force=force)
|
|
148
150
|
|
|
@@ -263,11 +265,20 @@ class SynapRuntime:
|
|
|
263
265
|
for sym in batch:
|
|
264
266
|
try:
|
|
265
267
|
# Avoid re-embedding if unchanged?
|
|
266
|
-
text =
|
|
268
|
+
text = (
|
|
269
|
+
sym["name"] + " " + (sym.get("metadata") or {}).get("docstring", "")
|
|
270
|
+
)
|
|
267
271
|
vector = provider.embed(text)
|
|
268
272
|
self.store.put_embedding(
|
|
269
273
|
sym["symbol_id"], model_name, "1.0", "1.0", vector, content_hash
|
|
270
274
|
)
|
|
275
|
+
except NotImplementedError as e:
|
|
276
|
+
logger.warning(
|
|
277
|
+
"embedding_not_supported_by_provider",
|
|
278
|
+
error=str(e),
|
|
279
|
+
suggestion="Configure SYNAP_EMBED_PROVIDER=ollama or openai.",
|
|
280
|
+
)
|
|
281
|
+
return # Stop attempting batch if not supported
|
|
271
282
|
except Exception as e:
|
|
272
283
|
logger.warning("background_embed_failed_for_symbol", error=str(e))
|
|
273
284
|
except Exception as e:
|
|
@@ -306,7 +317,7 @@ class SynapRuntime:
|
|
|
306
317
|
from rich.console import Console
|
|
307
318
|
from rich.progress import BarColumn, Progress, SpinnerColumn, TextColumn, TimeElapsedColumn
|
|
308
319
|
|
|
309
|
-
console = Console()
|
|
320
|
+
console = Console(stderr=True)
|
|
310
321
|
show_progress = self.settings.logging_mode.name == "HUMAN" and num_files > 5
|
|
311
322
|
|
|
312
323
|
if show_progress:
|
|
@@ -676,8 +687,12 @@ class SynapRuntime:
|
|
|
676
687
|
candidate_rel = (target_dir / "/".join(parts)).as_posix()
|
|
677
688
|
for ext in [".py", ".ts", ".go", ".rs"]:
|
|
678
689
|
candidate_paths.add(f"{candidate_rel}{ext}")
|
|
679
|
-
except Exception:
|
|
680
|
-
|
|
690
|
+
except Exception as e:
|
|
691
|
+
import structlog
|
|
692
|
+
|
|
693
|
+
structlog.get_logger().error(
|
|
694
|
+
"suppressed_error_caught", error=str(e), exc_info=True
|
|
695
|
+
)
|
|
681
696
|
else:
|
|
682
697
|
module_keys.add(module_part)
|
|
683
698
|
fts_names.add(target_name)
|
|
@@ -743,8 +758,12 @@ class SynapRuntime:
|
|
|
743
758
|
if cand_path in path_to_file_id:
|
|
744
759
|
target_file_id = path_to_file_id[cand_path]
|
|
745
760
|
break
|
|
746
|
-
except Exception:
|
|
747
|
-
|
|
761
|
+
except Exception as e:
|
|
762
|
+
import structlog
|
|
763
|
+
|
|
764
|
+
structlog.get_logger().error(
|
|
765
|
+
"suppressed_error_caught", error=str(e), exc_info=True
|
|
766
|
+
)
|
|
748
767
|
else:
|
|
749
768
|
target_file_id = module_key_to_file_id.get(module_part)
|
|
750
769
|
|
|
@@ -811,8 +830,12 @@ class SynapRuntime:
|
|
|
811
830
|
if cand_path in path_to_file_id:
|
|
812
831
|
target_file_id = path_to_file_id[cand_path]
|
|
813
832
|
break
|
|
814
|
-
except Exception:
|
|
815
|
-
|
|
833
|
+
except Exception as e:
|
|
834
|
+
import structlog
|
|
835
|
+
|
|
836
|
+
structlog.get_logger().error(
|
|
837
|
+
"suppressed_error_caught", error=str(e), exc_info=True
|
|
838
|
+
)
|
|
816
839
|
else:
|
|
817
840
|
target_file_id = module_key_to_file_id.get(module_part)
|
|
818
841
|
|
|
@@ -888,8 +911,10 @@ class SynapRuntime:
|
|
|
888
911
|
is_dirty = False
|
|
889
912
|
try:
|
|
890
913
|
is_dirty = self.git.state().is_dirty
|
|
891
|
-
except Exception:
|
|
892
|
-
|
|
914
|
+
except Exception as e:
|
|
915
|
+
import structlog
|
|
916
|
+
|
|
917
|
+
structlog.get_logger().error("suppressed_error_caught", error=str(e), exc_info=True)
|
|
893
918
|
return self.retrieval_engine.retrieve(query, max_tokens=max_tokens, is_dirty=is_dirty)
|
|
894
919
|
|
|
895
920
|
def doctor(self, *, auto_recover: bool = False) -> dict[str, Any]:
|
|
@@ -132,8 +132,10 @@ class WikiEngine:
|
|
|
132
132
|
purpose="wiki",
|
|
133
133
|
file_path=file_path,
|
|
134
134
|
)
|
|
135
|
-
except Exception:
|
|
136
|
-
|
|
135
|
+
except Exception as e:
|
|
136
|
+
import structlog
|
|
137
|
+
|
|
138
|
+
structlog.get_logger().error("suppressed_error_caught", error=str(e), exc_info=True)
|
|
137
139
|
except Exception as e:
|
|
138
140
|
logger.error("wiki_generation_failed", path=file_path, error=str(e))
|
|
139
141
|
raise
|
|
@@ -189,7 +191,9 @@ class WikiEngine:
|
|
|
189
191
|
purpose="wiki_project",
|
|
190
192
|
file_path=clean_name,
|
|
191
193
|
)
|
|
192
|
-
except Exception:
|
|
193
|
-
|
|
194
|
+
except Exception as e:
|
|
195
|
+
import structlog
|
|
196
|
+
|
|
197
|
+
structlog.get_logger().error("suppressed_error_caught", error=str(e), exc_info=True)
|
|
194
198
|
except Exception as e:
|
|
195
199
|
logger.error("wiki_project_generation_failed", page=clean_name, error=str(e))
|
|
@@ -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
|
|
|
@@ -138,8 +142,12 @@ class SynapMCPServer:
|
|
|
138
142
|
"dirty_tree": dirty,
|
|
139
143
|
},
|
|
140
144
|
)
|
|
141
|
-
except Exception:
|
|
142
|
-
|
|
145
|
+
except Exception as e:
|
|
146
|
+
import structlog
|
|
147
|
+
|
|
148
|
+
structlog.get_logger().error(
|
|
149
|
+
"suppressed_error_caught", error=str(e), exc_info=True
|
|
150
|
+
)
|
|
143
151
|
|
|
144
152
|
return json.dumps(
|
|
145
153
|
{
|
|
@@ -176,8 +184,12 @@ class SynapMCPServer:
|
|
|
176
184
|
"trace_id": trace_id,
|
|
177
185
|
},
|
|
178
186
|
)
|
|
179
|
-
except Exception:
|
|
180
|
-
|
|
187
|
+
except Exception as e:
|
|
188
|
+
import structlog
|
|
189
|
+
|
|
190
|
+
structlog.get_logger().error(
|
|
191
|
+
"suppressed_error_caught", error=str(e), exc_info=True
|
|
192
|
+
)
|
|
181
193
|
|
|
182
194
|
return json.dumps(
|
|
183
195
|
{
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from dataclasses import dataclass
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
4
|
from pathlib import Path
|
|
5
5
|
from typing import Any
|
|
6
6
|
|
|
@@ -28,7 +28,7 @@ class CodeSymbol:
|
|
|
28
28
|
start_line: int
|
|
29
29
|
end_line: int
|
|
30
30
|
ast_hash: str
|
|
31
|
-
metadata: dict[str, Any]
|
|
31
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
32
32
|
|
|
33
33
|
|
|
34
34
|
@dataclass(frozen=True)
|
|
@@ -5,8 +5,12 @@ import urllib.request
|
|
|
5
5
|
from collections.abc import Iterator
|
|
6
6
|
from typing import Any
|
|
7
7
|
|
|
8
|
+
import structlog
|
|
9
|
+
|
|
8
10
|
from synap_git.provider.base import LLMProvider, LLMResponse
|
|
9
11
|
|
|
12
|
+
logger = structlog.get_logger(__name__)
|
|
13
|
+
|
|
10
14
|
|
|
11
15
|
class AnthropicProvider(LLMProvider):
|
|
12
16
|
"""Anthropic API provider implementation."""
|
|
@@ -109,7 +113,7 @@ class AnthropicProvider(LLMProvider):
|
|
|
109
113
|
if "text" in delta:
|
|
110
114
|
yield delta["text"]
|
|
111
115
|
except Exception:
|
|
112
|
-
|
|
116
|
+
logger.exception("anthropic_stream_chunk_parse_failed", data=data_part)
|
|
113
117
|
except Exception as exc:
|
|
114
118
|
raise RuntimeError(f"Anthropic generate stream failed: {exc}") from exc
|
|
115
119
|
|
|
@@ -5,8 +5,12 @@ import urllib.request
|
|
|
5
5
|
from collections.abc import Iterator
|
|
6
6
|
from typing import Any
|
|
7
7
|
|
|
8
|
+
import structlog
|
|
9
|
+
|
|
8
10
|
from synap_git.provider.base import LLMProvider, LLMResponse
|
|
9
11
|
|
|
12
|
+
logger = structlog.get_logger(__name__)
|
|
13
|
+
|
|
10
14
|
|
|
11
15
|
class OpenAIProvider(LLMProvider):
|
|
12
16
|
"""OpenAI API provider implementation."""
|
|
@@ -101,7 +105,7 @@ class OpenAIProvider(LLMProvider):
|
|
|
101
105
|
if "content" in delta:
|
|
102
106
|
yield delta["content"]
|
|
103
107
|
except Exception:
|
|
104
|
-
|
|
108
|
+
logger.exception("openai_stream_chunk_parse_failed", data=data_part)
|
|
105
109
|
except Exception as exc:
|
|
106
110
|
raise RuntimeError(f"OpenAI generate stream failed: {exc}") from exc
|
|
107
111
|
|
|
@@ -5,9 +5,13 @@ import urllib.request
|
|
|
5
5
|
from collections.abc import Iterator
|
|
6
6
|
from typing import Any
|
|
7
7
|
|
|
8
|
+
import structlog
|
|
9
|
+
|
|
8
10
|
from synap_git.provider.base import LLMResponse
|
|
9
11
|
from synap_git.provider.openai import OpenAIProvider
|
|
10
12
|
|
|
13
|
+
logger = structlog.get_logger(__name__)
|
|
14
|
+
|
|
11
15
|
|
|
12
16
|
class OpenRouterProvider(OpenAIProvider):
|
|
13
17
|
"""OpenRouter API provider implementation using OpenAI-compatible endpoints."""
|
|
@@ -106,7 +110,7 @@ class OpenRouterProvider(OpenAIProvider):
|
|
|
106
110
|
if "content" in delta:
|
|
107
111
|
yield delta["content"]
|
|
108
112
|
except Exception:
|
|
109
|
-
|
|
113
|
+
logger.exception("openrouter_stream_chunk_parse_failed", data=data_part)
|
|
110
114
|
except Exception as exc:
|
|
111
115
|
raise RuntimeError(f"OpenRouter generate stream failed: {exc}") from exc
|
|
112
116
|
|
|
@@ -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
|
|
|
@@ -284,8 +289,12 @@ class HybridRetrievalEngine:
|
|
|
284
289
|
output_tokens=response.completion_tokens,
|
|
285
290
|
purpose="retrieval",
|
|
286
291
|
)
|
|
287
|
-
except Exception:
|
|
288
|
-
|
|
292
|
+
except Exception as e:
|
|
293
|
+
import structlog
|
|
294
|
+
|
|
295
|
+
structlog.get_logger().error(
|
|
296
|
+
"suppressed_error_caught", error=str(e), exc_info=True
|
|
297
|
+
)
|
|
289
298
|
else:
|
|
290
299
|
err_msg = str(last_err)
|
|
291
300
|
answer_content = (
|
|
@@ -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
|
|
@@ -156,6 +156,7 @@ class SynapStore:
|
|
|
156
156
|
model TEXT NOT NULL,
|
|
157
157
|
input_tokens INTEGER NOT NULL,
|
|
158
158
|
output_tokens INTEGER NOT NULL,
|
|
159
|
+
cost_usd REAL NOT NULL,
|
|
159
160
|
purpose TEXT NOT NULL,
|
|
160
161
|
file_path TEXT,
|
|
161
162
|
created_at INTEGER NOT NULL
|
|
@@ -244,9 +245,9 @@ class SynapStore:
|
|
|
244
245
|
CREATE INDEX IF NOT EXISTS idx_files_content_hash ON files(content_hash);
|
|
245
246
|
CREATE INDEX IF NOT EXISTS idx_files_git_oid ON files(git_oid);
|
|
246
247
|
|
|
247
|
-
CREATE TRIGGER IF NOT EXISTS tgr_symbols_insert
|
|
248
|
+
CREATE TRIGGER IF NOT EXISTS tgr_symbols_insert
|
|
248
249
|
AFTER INSERT ON symbols BEGIN
|
|
249
|
-
INSERT INTO symbols_fts(symbol_id, name, kind, body)
|
|
250
|
+
INSERT INTO symbols_fts(symbol_id, name, kind, body)
|
|
250
251
|
VALUES (new.symbol_id, new.name, new.kind, new.metadata_json);
|
|
251
252
|
END;
|
|
252
253
|
|
|
@@ -286,7 +287,7 @@ class SynapStore:
|
|
|
286
287
|
conn.create_function("cosine_similarity", 2, _cosine_similarity)
|
|
287
288
|
try:
|
|
288
289
|
conn.execute("PRAGMA foreign_keys=ON")
|
|
289
|
-
conn.execute("PRAGMA synchronous=
|
|
290
|
+
conn.execute("PRAGMA synchronous=NORMAL")
|
|
290
291
|
yield conn
|
|
291
292
|
conn.commit()
|
|
292
293
|
except Exception:
|
|
@@ -394,7 +395,16 @@ class SynapStore:
|
|
|
394
395
|
).fetchall()
|
|
395
396
|
return [dict(row) for row in rows]
|
|
396
397
|
|
|
397
|
-
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)
|
|
398
408
|
with self.connect() as conn:
|
|
399
409
|
rows = conn.execute(
|
|
400
410
|
"""
|
|
@@ -406,7 +416,7 @@ class SynapStore:
|
|
|
406
416
|
ORDER BY rank
|
|
407
417
|
LIMIT ?
|
|
408
418
|
""",
|
|
409
|
-
(
|
|
419
|
+
(match_expr, limit),
|
|
410
420
|
).fetchall()
|
|
411
421
|
return [dict(row) for row in rows]
|
|
412
422
|
|
|
@@ -682,8 +692,10 @@ class SynapStore:
|
|
|
682
692
|
res = conn.execute("PRAGMA quick_check").fetchone()[0]
|
|
683
693
|
if res == "ok":
|
|
684
694
|
return False
|
|
685
|
-
except Exception:
|
|
686
|
-
|
|
695
|
+
except Exception as e:
|
|
696
|
+
import structlog
|
|
697
|
+
|
|
698
|
+
structlog.get_logger().error("suppressed_error_caught", error=str(e), exc_info=True)
|
|
687
699
|
|
|
688
700
|
# If we got here, it's corrupted or unreachable. Wipe it securely.
|
|
689
701
|
for ext in ["", "-wal", "-shm"]:
|
|
@@ -702,6 +714,7 @@ class SynapStore:
|
|
|
702
714
|
input_tokens: int,
|
|
703
715
|
output_tokens: int,
|
|
704
716
|
purpose: str,
|
|
717
|
+
cost_usd: float = 0.0,
|
|
705
718
|
file_path: str | None = None,
|
|
706
719
|
) -> None:
|
|
707
720
|
import uuid
|
|
@@ -712,8 +725,8 @@ class SynapStore:
|
|
|
712
725
|
with self.connect() as conn:
|
|
713
726
|
conn.execute(
|
|
714
727
|
"""
|
|
715
|
-
INSERT INTO llm_calls (call_id, provider, model, input_tokens, output_tokens, purpose, file_path, created_at)
|
|
716
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
728
|
+
INSERT INTO llm_calls (call_id, provider, model, input_tokens, output_tokens, cost_usd, purpose, file_path, created_at)
|
|
729
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
717
730
|
""",
|
|
718
731
|
(
|
|
719
732
|
call_id,
|
|
@@ -721,6 +734,7 @@ class SynapStore:
|
|
|
721
734
|
model or "unknown",
|
|
722
735
|
input_tokens,
|
|
723
736
|
output_tokens,
|
|
737
|
+
cost_usd,
|
|
724
738
|
purpose,
|
|
725
739
|
file_path,
|
|
726
740
|
now,
|
|
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
|