synap-git 2.0.0__tar.gz → 2.1.0__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.0}/CHANGELOG.md +14 -0
- {synap_git-2.0.0 → synap_git-2.1.0}/PKG-INFO +1 -1
- {synap_git-2.0.0 → synap_git-2.1.0}/src/synap_git/__init__.py +1 -1
- {synap_git-2.0.0 → synap_git-2.1.0}/src/synap_git/api/app.py +4 -2
- {synap_git-2.0.0 → synap_git-2.1.0}/src/synap_git/cli/main.py +54 -17
- {synap_git-2.0.0 → synap_git-2.1.0}/src/synap_git/config.py +3 -1
- {synap_git-2.0.0 → synap_git-2.1.0}/src/synap_git/diagnostics/logger.py +2 -2
- {synap_git-2.0.0 → synap_git-2.1.0}/src/synap_git/git/state.py +4 -2
- {synap_git-2.0.0 → synap_git-2.1.0}/src/synap_git/indexer/daemon.py +8 -4
- {synap_git-2.0.0 → synap_git-2.1.0}/src/synap_git/indexer/engine.py +36 -11
- {synap_git-2.0.0 → synap_git-2.1.0}/src/synap_git/indexer/wiki.py +8 -4
- {synap_git-2.0.0 → synap_git-2.1.0}/src/synap_git/mcp/server.py +12 -4
- {synap_git-2.0.0 → synap_git-2.1.0}/src/synap_git/parser/registry.py +2 -2
- {synap_git-2.0.0 → synap_git-2.1.0}/src/synap_git/provider/anthropic.py +5 -1
- {synap_git-2.0.0 → synap_git-2.1.0}/src/synap_git/provider/openai.py +5 -1
- {synap_git-2.0.0 → synap_git-2.1.0}/src/synap_git/provider/openrouter.py +5 -1
- {synap_git-2.0.0 → synap_git-2.1.0}/src/synap_git/retrieval/engine.py +6 -2
- {synap_git-2.0.0 → synap_git-2.1.0}/src/synap_git/storage/sqlite.py +11 -6
- {synap_git-2.0.0 → synap_git-2.1.0}/.gitignore +0 -0
- {synap_git-2.0.0 → synap_git-2.1.0}/.synap.example/README.md +0 -0
- {synap_git-2.0.0 → synap_git-2.1.0}/LICENSE.md +0 -0
- {synap_git-2.0.0 → synap_git-2.1.0}/README.md +0 -0
- {synap_git-2.0.0 → synap_git-2.1.0}/pyproject.toml +0 -0
- {synap_git-2.0.0 → synap_git-2.1.0}/src/synap_git/api/__init__.py +0 -0
- {synap_git-2.0.0 → synap_git-2.1.0}/src/synap_git/api/static/index.html +0 -0
- {synap_git-2.0.0 → synap_git-2.1.0}/src/synap_git/cli/__init__.py +0 -0
- {synap_git-2.0.0 → synap_git-2.1.0}/src/synap_git/cli/__main__.py +0 -0
- {synap_git-2.0.0 → synap_git-2.1.0}/src/synap_git/diagnostics/__init__.py +0 -0
- {synap_git-2.0.0 → synap_git-2.1.0}/src/synap_git/diagnostics/tracing.py +0 -0
- {synap_git-2.0.0 → synap_git-2.1.0}/src/synap_git/embeddings/__init__.py +0 -0
- {synap_git-2.0.0 → synap_git-2.1.0}/src/synap_git/git/__init__.py +0 -0
- {synap_git-2.0.0 → synap_git-2.1.0}/src/synap_git/indexer/__init__.py +0 -0
- {synap_git-2.0.0 → synap_git-2.1.0}/src/synap_git/indexer/scanner.py +0 -0
- {synap_git-2.0.0 → synap_git-2.1.0}/src/synap_git/mcp/__init__.py +0 -0
- {synap_git-2.0.0 → synap_git-2.1.0}/src/synap_git/parser/__init__.py +0 -0
- {synap_git-2.0.0 → synap_git-2.1.0}/src/synap_git/provider/base.py +0 -0
- {synap_git-2.0.0 → synap_git-2.1.0}/src/synap_git/provider/factory.py +0 -0
- {synap_git-2.0.0 → synap_git-2.1.0}/src/synap_git/provider/gemini.py +0 -0
- {synap_git-2.0.0 → synap_git-2.1.0}/src/synap_git/provider/ollama.py +0 -0
- {synap_git-2.0.0 → synap_git-2.1.0}/src/synap_git/py.typed +0 -0
- {synap_git-2.0.0 → synap_git-2.1.0}/src/synap_git/retrieval/__init__.py +0 -0
- {synap_git-2.0.0 → synap_git-2.1.0}/src/synap_git/retrieval/memory.py +0 -0
- {synap_git-2.0.0 → synap_git-2.1.0}/src/synap_git/storage/__init__.py +0 -0
- {synap_git-2.0.0 → synap_git-2.1.0}/src/synap_git/utils/__init__.py +0 -0
- {synap_git-2.0.0 → synap_git-2.1.0}/src/synap_git/utils/serialization.py +0 -0
|
@@ -5,6 +5,20 @@ 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.0] - 2026-05-29
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **CLI `search` command**: New user-facing command for executing hybrid structural searches directly from the terminal.
|
|
12
|
+
- **`embed_provider` configuration**: Added explicit control over the vector embedding provider via `SYNAP_EMBED_PROVIDER` or `config.toml`.
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
- **CRITICAL: Metadata Nullability Bug**: Resolved a type mismatch in `CodeSymbol` where `metadata=None` caused background worker crashes. Enforced strict `dict` contract with `default_factory`.
|
|
16
|
+
- **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.
|
|
17
|
+
- **CRITICAL: SQLite Schema Integrity**: Fixed `NOT NULL` constraint failure in `llm_calls` table by aligning schema with insertion logic and adding `cost_usd` support.
|
|
18
|
+
- **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.
|
|
19
|
+
- **HIGH: Rollback Atomicity**: Reordered `synap rollback` operations to ensure Git state is successfully restored before purging index checkpoints.
|
|
20
|
+
- **MEDIUM: Provider Resiliency**: Added explicit handling for `NotImplementedError` in Anthropic and OpenRouter providers, preventing worker panics when embeddings are misconfigured.
|
|
21
|
+
|
|
8
22
|
## [2.0.0] - 2026-05-29
|
|
9
23
|
|
|
10
24
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: synap-git
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.1.0
|
|
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]")
|
|
@@ -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:
|
|
@@ -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))
|
|
@@ -138,8 +138,12 @@ class SynapMCPServer:
|
|
|
138
138
|
"dirty_tree": dirty,
|
|
139
139
|
},
|
|
140
140
|
)
|
|
141
|
-
except Exception:
|
|
142
|
-
|
|
141
|
+
except Exception as e:
|
|
142
|
+
import structlog
|
|
143
|
+
|
|
144
|
+
structlog.get_logger().error(
|
|
145
|
+
"suppressed_error_caught", error=str(e), exc_info=True
|
|
146
|
+
)
|
|
143
147
|
|
|
144
148
|
return json.dumps(
|
|
145
149
|
{
|
|
@@ -176,8 +180,12 @@ class SynapMCPServer:
|
|
|
176
180
|
"trace_id": trace_id,
|
|
177
181
|
},
|
|
178
182
|
)
|
|
179
|
-
except Exception:
|
|
180
|
-
|
|
183
|
+
except Exception as e:
|
|
184
|
+
import structlog
|
|
185
|
+
|
|
186
|
+
structlog.get_logger().error(
|
|
187
|
+
"suppressed_error_caught", error=str(e), exc_info=True
|
|
188
|
+
)
|
|
181
189
|
|
|
182
190
|
return json.dumps(
|
|
183
191
|
{
|
|
@@ -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
|
|
|
@@ -284,8 +284,12 @@ class HybridRetrievalEngine:
|
|
|
284
284
|
output_tokens=response.completion_tokens,
|
|
285
285
|
purpose="retrieval",
|
|
286
286
|
)
|
|
287
|
-
except Exception:
|
|
288
|
-
|
|
287
|
+
except Exception as e:
|
|
288
|
+
import structlog
|
|
289
|
+
|
|
290
|
+
structlog.get_logger().error(
|
|
291
|
+
"suppressed_error_caught", error=str(e), exc_info=True
|
|
292
|
+
)
|
|
289
293
|
else:
|
|
290
294
|
err_msg = str(last_err)
|
|
291
295
|
answer_content = (
|
|
@@ -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
|
|
|
@@ -682,8 +683,10 @@ class SynapStore:
|
|
|
682
683
|
res = conn.execute("PRAGMA quick_check").fetchone()[0]
|
|
683
684
|
if res == "ok":
|
|
684
685
|
return False
|
|
685
|
-
except Exception:
|
|
686
|
-
|
|
686
|
+
except Exception as e:
|
|
687
|
+
import structlog
|
|
688
|
+
|
|
689
|
+
structlog.get_logger().error("suppressed_error_caught", error=str(e), exc_info=True)
|
|
687
690
|
|
|
688
691
|
# If we got here, it's corrupted or unreachable. Wipe it securely.
|
|
689
692
|
for ext in ["", "-wal", "-shm"]:
|
|
@@ -702,6 +705,7 @@ class SynapStore:
|
|
|
702
705
|
input_tokens: int,
|
|
703
706
|
output_tokens: int,
|
|
704
707
|
purpose: str,
|
|
708
|
+
cost_usd: float = 0.0,
|
|
705
709
|
file_path: str | None = None,
|
|
706
710
|
) -> None:
|
|
707
711
|
import uuid
|
|
@@ -712,8 +716,8 @@ class SynapStore:
|
|
|
712
716
|
with self.connect() as conn:
|
|
713
717
|
conn.execute(
|
|
714
718
|
"""
|
|
715
|
-
INSERT INTO llm_calls (call_id, provider, model, input_tokens, output_tokens, purpose, file_path, created_at)
|
|
716
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
719
|
+
INSERT INTO llm_calls (call_id, provider, model, input_tokens, output_tokens, cost_usd, purpose, file_path, created_at)
|
|
720
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
717
721
|
""",
|
|
718
722
|
(
|
|
719
723
|
call_id,
|
|
@@ -721,6 +725,7 @@ class SynapStore:
|
|
|
721
725
|
model or "unknown",
|
|
722
726
|
input_tokens,
|
|
723
727
|
output_tokens,
|
|
728
|
+
cost_usd,
|
|
724
729
|
purpose,
|
|
725
730
|
file_path,
|
|
726
731
|
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
|