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.
Files changed (45) hide show
  1. {synap_git-2.0.0 → synap_git-2.1.0}/CHANGELOG.md +14 -0
  2. {synap_git-2.0.0 → synap_git-2.1.0}/PKG-INFO +1 -1
  3. {synap_git-2.0.0 → synap_git-2.1.0}/src/synap_git/__init__.py +1 -1
  4. {synap_git-2.0.0 → synap_git-2.1.0}/src/synap_git/api/app.py +4 -2
  5. {synap_git-2.0.0 → synap_git-2.1.0}/src/synap_git/cli/main.py +54 -17
  6. {synap_git-2.0.0 → synap_git-2.1.0}/src/synap_git/config.py +3 -1
  7. {synap_git-2.0.0 → synap_git-2.1.0}/src/synap_git/diagnostics/logger.py +2 -2
  8. {synap_git-2.0.0 → synap_git-2.1.0}/src/synap_git/git/state.py +4 -2
  9. {synap_git-2.0.0 → synap_git-2.1.0}/src/synap_git/indexer/daemon.py +8 -4
  10. {synap_git-2.0.0 → synap_git-2.1.0}/src/synap_git/indexer/engine.py +36 -11
  11. {synap_git-2.0.0 → synap_git-2.1.0}/src/synap_git/indexer/wiki.py +8 -4
  12. {synap_git-2.0.0 → synap_git-2.1.0}/src/synap_git/mcp/server.py +12 -4
  13. {synap_git-2.0.0 → synap_git-2.1.0}/src/synap_git/parser/registry.py +2 -2
  14. {synap_git-2.0.0 → synap_git-2.1.0}/src/synap_git/provider/anthropic.py +5 -1
  15. {synap_git-2.0.0 → synap_git-2.1.0}/src/synap_git/provider/openai.py +5 -1
  16. {synap_git-2.0.0 → synap_git-2.1.0}/src/synap_git/provider/openrouter.py +5 -1
  17. {synap_git-2.0.0 → synap_git-2.1.0}/src/synap_git/retrieval/engine.py +6 -2
  18. {synap_git-2.0.0 → synap_git-2.1.0}/src/synap_git/storage/sqlite.py +11 -6
  19. {synap_git-2.0.0 → synap_git-2.1.0}/.gitignore +0 -0
  20. {synap_git-2.0.0 → synap_git-2.1.0}/.synap.example/README.md +0 -0
  21. {synap_git-2.0.0 → synap_git-2.1.0}/LICENSE.md +0 -0
  22. {synap_git-2.0.0 → synap_git-2.1.0}/README.md +0 -0
  23. {synap_git-2.0.0 → synap_git-2.1.0}/pyproject.toml +0 -0
  24. {synap_git-2.0.0 → synap_git-2.1.0}/src/synap_git/api/__init__.py +0 -0
  25. {synap_git-2.0.0 → synap_git-2.1.0}/src/synap_git/api/static/index.html +0 -0
  26. {synap_git-2.0.0 → synap_git-2.1.0}/src/synap_git/cli/__init__.py +0 -0
  27. {synap_git-2.0.0 → synap_git-2.1.0}/src/synap_git/cli/__main__.py +0 -0
  28. {synap_git-2.0.0 → synap_git-2.1.0}/src/synap_git/diagnostics/__init__.py +0 -0
  29. {synap_git-2.0.0 → synap_git-2.1.0}/src/synap_git/diagnostics/tracing.py +0 -0
  30. {synap_git-2.0.0 → synap_git-2.1.0}/src/synap_git/embeddings/__init__.py +0 -0
  31. {synap_git-2.0.0 → synap_git-2.1.0}/src/synap_git/git/__init__.py +0 -0
  32. {synap_git-2.0.0 → synap_git-2.1.0}/src/synap_git/indexer/__init__.py +0 -0
  33. {synap_git-2.0.0 → synap_git-2.1.0}/src/synap_git/indexer/scanner.py +0 -0
  34. {synap_git-2.0.0 → synap_git-2.1.0}/src/synap_git/mcp/__init__.py +0 -0
  35. {synap_git-2.0.0 → synap_git-2.1.0}/src/synap_git/parser/__init__.py +0 -0
  36. {synap_git-2.0.0 → synap_git-2.1.0}/src/synap_git/provider/base.py +0 -0
  37. {synap_git-2.0.0 → synap_git-2.1.0}/src/synap_git/provider/factory.py +0 -0
  38. {synap_git-2.0.0 → synap_git-2.1.0}/src/synap_git/provider/gemini.py +0 -0
  39. {synap_git-2.0.0 → synap_git-2.1.0}/src/synap_git/provider/ollama.py +0 -0
  40. {synap_git-2.0.0 → synap_git-2.1.0}/src/synap_git/py.typed +0 -0
  41. {synap_git-2.0.0 → synap_git-2.1.0}/src/synap_git/retrieval/__init__.py +0 -0
  42. {synap_git-2.0.0 → synap_git-2.1.0}/src/synap_git/retrieval/memory.py +0 -0
  43. {synap_git-2.0.0 → synap_git-2.1.0}/src/synap_git/storage/__init__.py +0 -0
  44. {synap_git-2.0.0 → synap_git-2.1.0}/src/synap_git/utils/__init__.py +0 -0
  45. {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.0.0
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
@@ -2,4 +2,4 @@
2
2
 
3
3
  __all__ = ["__version__"]
4
4
 
5
- __version__ = "2.0.0"
5
+ __version__ = "2.1.0"
@@ -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]")
@@ -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:
@@ -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))
@@ -138,8 +138,12 @@ class SynapMCPServer:
138
138
  "dirty_tree": dirty,
139
139
  },
140
140
  )
141
- except Exception:
142
- pass
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
- pass
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] | 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
 
@@ -284,8 +284,12 @@ class HybridRetrievalEngine:
284
284
  output_tokens=response.completion_tokens,
285
285
  purpose="retrieval",
286
286
  )
287
- except Exception:
288
- pass
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
- pass
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