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.
Files changed (45) hide show
  1. {synap_git-2.0.0 → synap_git-2.1.1}/CHANGELOG.md +20 -0
  2. {synap_git-2.0.0 → synap_git-2.1.1}/PKG-INFO +1 -1
  3. {synap_git-2.0.0 → synap_git-2.1.1}/src/synap_git/__init__.py +1 -1
  4. {synap_git-2.0.0 → synap_git-2.1.1}/src/synap_git/api/app.py +4 -2
  5. {synap_git-2.0.0 → synap_git-2.1.1}/src/synap_git/cli/main.py +55 -18
  6. {synap_git-2.0.0 → synap_git-2.1.1}/src/synap_git/config.py +3 -1
  7. {synap_git-2.0.0 → synap_git-2.1.1}/src/synap_git/diagnostics/logger.py +2 -2
  8. {synap_git-2.0.0 → synap_git-2.1.1}/src/synap_git/git/state.py +4 -2
  9. {synap_git-2.0.0 → synap_git-2.1.1}/src/synap_git/indexer/daemon.py +8 -4
  10. {synap_git-2.0.0 → synap_git-2.1.1}/src/synap_git/indexer/engine.py +37 -12
  11. {synap_git-2.0.0 → synap_git-2.1.1}/src/synap_git/indexer/wiki.py +8 -4
  12. {synap_git-2.0.0 → synap_git-2.1.1}/src/synap_git/mcp/server.py +17 -5
  13. {synap_git-2.0.0 → synap_git-2.1.1}/src/synap_git/parser/registry.py +2 -2
  14. {synap_git-2.0.0 → synap_git-2.1.1}/src/synap_git/provider/anthropic.py +5 -1
  15. {synap_git-2.0.0 → synap_git-2.1.1}/src/synap_git/provider/openai.py +5 -1
  16. {synap_git-2.0.0 → synap_git-2.1.1}/src/synap_git/provider/openrouter.py +5 -1
  17. {synap_git-2.0.0 → synap_git-2.1.1}/src/synap_git/retrieval/engine.py +35 -26
  18. {synap_git-2.0.0 → synap_git-2.1.1}/src/synap_git/storage/sqlite.py +24 -10
  19. {synap_git-2.0.0 → synap_git-2.1.1}/.gitignore +0 -0
  20. {synap_git-2.0.0 → synap_git-2.1.1}/.synap.example/README.md +0 -0
  21. {synap_git-2.0.0 → synap_git-2.1.1}/LICENSE.md +0 -0
  22. {synap_git-2.0.0 → synap_git-2.1.1}/README.md +0 -0
  23. {synap_git-2.0.0 → synap_git-2.1.1}/pyproject.toml +0 -0
  24. {synap_git-2.0.0 → synap_git-2.1.1}/src/synap_git/api/__init__.py +0 -0
  25. {synap_git-2.0.0 → synap_git-2.1.1}/src/synap_git/api/static/index.html +0 -0
  26. {synap_git-2.0.0 → synap_git-2.1.1}/src/synap_git/cli/__init__.py +0 -0
  27. {synap_git-2.0.0 → synap_git-2.1.1}/src/synap_git/cli/__main__.py +0 -0
  28. {synap_git-2.0.0 → synap_git-2.1.1}/src/synap_git/diagnostics/__init__.py +0 -0
  29. {synap_git-2.0.0 → synap_git-2.1.1}/src/synap_git/diagnostics/tracing.py +0 -0
  30. {synap_git-2.0.0 → synap_git-2.1.1}/src/synap_git/embeddings/__init__.py +0 -0
  31. {synap_git-2.0.0 → synap_git-2.1.1}/src/synap_git/git/__init__.py +0 -0
  32. {synap_git-2.0.0 → synap_git-2.1.1}/src/synap_git/indexer/__init__.py +0 -0
  33. {synap_git-2.0.0 → synap_git-2.1.1}/src/synap_git/indexer/scanner.py +0 -0
  34. {synap_git-2.0.0 → synap_git-2.1.1}/src/synap_git/mcp/__init__.py +0 -0
  35. {synap_git-2.0.0 → synap_git-2.1.1}/src/synap_git/parser/__init__.py +0 -0
  36. {synap_git-2.0.0 → synap_git-2.1.1}/src/synap_git/provider/base.py +0 -0
  37. {synap_git-2.0.0 → synap_git-2.1.1}/src/synap_git/provider/factory.py +0 -0
  38. {synap_git-2.0.0 → synap_git-2.1.1}/src/synap_git/provider/gemini.py +0 -0
  39. {synap_git-2.0.0 → synap_git-2.1.1}/src/synap_git/provider/ollama.py +0 -0
  40. {synap_git-2.0.0 → synap_git-2.1.1}/src/synap_git/py.typed +0 -0
  41. {synap_git-2.0.0 → synap_git-2.1.1}/src/synap_git/retrieval/__init__.py +0 -0
  42. {synap_git-2.0.0 → synap_git-2.1.1}/src/synap_git/retrieval/memory.py +0 -0
  43. {synap_git-2.0.0 → synap_git-2.1.1}/src/synap_git/storage/__init__.py +0 -0
  44. {synap_git-2.0.0 → synap_git-2.1.1}/src/synap_git/utils/__init__.py +0 -0
  45. {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.0.0
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
@@ -2,4 +2,4 @@
2
2
 
3
3
  __all__ = ["__version__"]
4
4
 
5
- __version__ = "2.0.0"
5
+ __version__ = "2.1.1"
@@ -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
- pass
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
- pass
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
- pass
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
- log_dir = Path("~/.config/synap/logs").expanduser()
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
- pass
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
- pass
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 = Path("~/.config/synap/logs").expanduser()
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
- pass
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
- pass
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
- pass
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
- pass
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 = sym["name"] + " " + sym.get("metadata", {}).get("docstring", "")
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
- pass
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
- pass
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
- pass
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
- pass
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
- pass
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
- pass
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
- print(f"\n[Synapse] ⚠ Agent at {percentage:.0%} context — checkpoint recommended.")
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
- pass
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
- pass
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] | None = None
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
- pass
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
- pass
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
- pass
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
- def _get_snippet(repo_path: Path, rel_path: str, start_line: int, end_line: int) -> str:
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
- lines = p.read_text(encoding="utf-8", errors="replace").splitlines()
29
- s_idx = max(0, start_line - 1)
30
- e_idx = min(len(lines), end_line)
31
- return "\n".join(lines[s_idx:e_idx])
32
- except Exception:
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
- try:
38
- p = repo_path / rel_path
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
- for word in query_words:
97
- symbols = self.store.get_symbols_by_name(word)
98
- for rank, sym in enumerate(symbols):
99
- sid = sym["symbol_id"]
100
- if sid not in lexical_candidates:
101
- lexical_candidates[sid] = {
102
- **sym,
103
- "reason": f"lexical:'{word}'",
104
- "lexical_rank": rank,
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
- pass
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=FULL")
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=FULL")
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 get_symbols_by_name(self, clean_name: str, limit: int = 50) -> list[dict[str, Any]]:
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
- (f'"{clean_name}"* OR body:"{clean_name}"*', limit),
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
- pass
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