synap-git 0.2.0__tar.gz → 1.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 (50) hide show
  1. {synap_git-0.2.0 → synap_git-1.1.0}/CHANGELOG.md +40 -3
  2. {synap_git-0.2.0 → synap_git-1.1.0}/PKG-INFO +20 -6
  3. {synap_git-0.2.0 → synap_git-1.1.0}/README.md +15 -4
  4. {synap_git-0.2.0 → synap_git-1.1.0}/pyproject.toml +10 -4
  5. {synap_git-0.2.0 → synap_git-1.1.0}/src/synap_git/__init__.py +1 -1
  6. {synap_git-0.2.0 → synap_git-1.1.0}/src/synap_git/api/app.py +40 -18
  7. {synap_git-0.2.0 → synap_git-1.1.0}/src/synap_git/api/static/index.html +33 -23
  8. synap_git-1.1.0/src/synap_git/cli/main.py +1930 -0
  9. {synap_git-0.2.0 → synap_git-1.1.0}/src/synap_git/config.py +11 -0
  10. {synap_git-0.2.0 → synap_git-1.1.0}/src/synap_git/diagnostics/logger.py +24 -4
  11. synap_git-1.1.0/src/synap_git/indexer/daemon.py +343 -0
  12. synap_git-1.1.0/src/synap_git/indexer/engine.py +677 -0
  13. synap_git-1.1.0/src/synap_git/indexer/wiki.py +188 -0
  14. {synap_git-0.2.0 → synap_git-1.1.0}/src/synap_git/parser/registry.py +38 -1
  15. {synap_git-0.2.0 → synap_git-1.1.0}/src/synap_git/provider/anthropic.py +48 -0
  16. {synap_git-0.2.0 → synap_git-1.1.0}/src/synap_git/provider/base.py +14 -0
  17. {synap_git-0.2.0 → synap_git-1.1.0}/src/synap_git/provider/factory.py +19 -6
  18. {synap_git-0.2.0 → synap_git-1.1.0}/src/synap_git/provider/gemini.py +15 -0
  19. {synap_git-0.2.0 → synap_git-1.1.0}/src/synap_git/provider/mock.py +15 -0
  20. {synap_git-0.2.0 → synap_git-1.1.0}/src/synap_git/provider/ollama.py +42 -0
  21. {synap_git-0.2.0 → synap_git-1.1.0}/src/synap_git/provider/openai.py +49 -0
  22. synap_git-1.1.0/src/synap_git/provider/openrouter.py +121 -0
  23. {synap_git-0.2.0 → synap_git-1.1.0}/src/synap_git/retrieval/engine.py +42 -17
  24. {synap_git-0.2.0 → synap_git-1.1.0}/src/synap_git/retrieval/memory.py +1 -1
  25. {synap_git-0.2.0 → synap_git-1.1.0}/src/synap_git/storage/sqlite.py +299 -163
  26. synap_git-0.2.0/src/synap_git/cli/main.py +0 -1174
  27. synap_git-0.2.0/src/synap_git/indexer/daemon.py +0 -143
  28. synap_git-0.2.0/src/synap_git/indexer/engine.py +0 -327
  29. synap_git-0.2.0/src/synap_git/indexer/wiki.py +0 -78
  30. {synap_git-0.2.0 → synap_git-1.1.0}/.gitignore +0 -0
  31. {synap_git-0.2.0 → synap_git-1.1.0}/.synap.example/README.md +0 -0
  32. {synap_git-0.2.0 → synap_git-1.1.0}/LICENSE.md +0 -0
  33. {synap_git-0.2.0 → synap_git-1.1.0}/src/synap_git/api/__init__.py +0 -0
  34. {synap_git-0.2.0 → synap_git-1.1.0}/src/synap_git/cli/__init__.py +0 -0
  35. {synap_git-0.2.0 → synap_git-1.1.0}/src/synap_git/cli/__main__.py +0 -0
  36. {synap_git-0.2.0 → synap_git-1.1.0}/src/synap_git/diagnostics/__init__.py +0 -0
  37. {synap_git-0.2.0 → synap_git-1.1.0}/src/synap_git/diagnostics/tracing.py +0 -0
  38. {synap_git-0.2.0 → synap_git-1.1.0}/src/synap_git/embeddings/__init__.py +0 -0
  39. {synap_git-0.2.0 → synap_git-1.1.0}/src/synap_git/git/__init__.py +0 -0
  40. {synap_git-0.2.0 → synap_git-1.1.0}/src/synap_git/git/state.py +0 -0
  41. {synap_git-0.2.0 → synap_git-1.1.0}/src/synap_git/indexer/__init__.py +0 -0
  42. {synap_git-0.2.0 → synap_git-1.1.0}/src/synap_git/indexer/scanner.py +0 -0
  43. {synap_git-0.2.0 → synap_git-1.1.0}/src/synap_git/mcp/__init__.py +0 -0
  44. {synap_git-0.2.0 → synap_git-1.1.0}/src/synap_git/mcp/server.py +0 -0
  45. {synap_git-0.2.0 → synap_git-1.1.0}/src/synap_git/parser/__init__.py +0 -0
  46. {synap_git-0.2.0 → synap_git-1.1.0}/src/synap_git/py.typed +0 -0
  47. {synap_git-0.2.0 → synap_git-1.1.0}/src/synap_git/retrieval/__init__.py +0 -0
  48. {synap_git-0.2.0 → synap_git-1.1.0}/src/synap_git/storage/__init__.py +0 -0
  49. {synap_git-0.2.0 → synap_git-1.1.0}/src/synap_git/utils/__init__.py +0 -0
  50. {synap_git-0.2.0 → synap_git-1.1.0}/src/synap_git/utils/serialization.py +0 -0
@@ -5,14 +5,51 @@ 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
+ ## [1.1.0] - 2026-05-28
9
+
10
+ ### Added — Git-Snapshot Projection & Performance Refactoring
11
+ - **Split Two-Path Indexing:** Separated initialization and incremental indexing into `_first_run_index` (full scan, CPU-parallelized) and `_incremental_index` (Git delta change detector).
12
+ - **Asynchronous Wiki Generation Queue:** Decoupled slow, non-deterministic LLM wiki generation from structural indexing using a persistent database queue (`wiki_queue`) processed asynchronously by a daemon worker.
13
+ - **Lazy Wiki Caching:** Added synchronous wiki generation fallback to CLI (`wiki show`), Web API, and MCP tools to dynamically build missing or stale pages on-demand.
14
+ - **Process Pool Parallel Parsing:** Parallelized Tree-sitter parsing on first run across all CPU cores utilizing process-based concurrency with independent parser instances.
15
+ - **SQLite Performance Hardening:**
16
+ - WAL mode and NORMAL synchronous configuration enabled during writes.
17
+ - Multi-row symbol and edge inserts batched into a single transaction via `executemany`.
18
+ - Dot-separated `module_key` pre-computation and indexing for $O(1)$ module resolution.
19
+ - SQLite FTS5 index integration for fast sub-millisecond symbol searches, avoiding full-table scans.
20
+ - **Web API Lazy Refreshes:** Updated the `/wiki/{filepath}` GET endpoint to perform lazy refreshes on stale or missing pages before returning content.
21
+
22
+ ### Fixed
23
+ - **FTS5 Cascade Delete:** Added database trigger `tgr_symbols_delete` to automatically clean up virtual `symbols_fts` entries when parent symbols are deleted.
24
+ - **Duplicate File ID Collision:** Handled unique, path-scoped file identifier generation ensuring files with identical content (like empty `__init__.py`) do not conflict.
25
+
26
+ ## [0.2.1] - 2026-05-27
27
+
28
+ ### Added — Final Production Hardening & Release Execution
29
+ - `synap rollback --commit <ref>` option: directly target a commit by hash/reference without interactive selection prompt.
30
+ - `synap rollback --yes` / `-y` option: suppress confirmation prompt for non-interactive and scripted rollback flows.
31
+ - Non-interactive guard in `synap rollback`: fails fast with a clear error when used in piped/CI contexts without `--commit` or `--yes`.
32
+ - `synap rollback` invalid commit detection: validates commit reference via `git rev-parse --verify` and rejects unknown refs with a clear message.
33
+
34
+ ### Fixed
35
+ - **SQLite migration short-circuit bug**: legacy un-versioned databases (user_version = 0) incorrectly skipped `CREATE TABLE IF NOT EXISTS` execution, leaving the `symbols` table and others uninitialized. The premature short-circuit is removed; all schema tables are now created correctly before bumping to version 1.
36
+ - **Python `import_from_statement` missing symbol extraction**: Tree-sitter AST parser only extracted the module identifier from `from X import Y` statements, discarding `Y`. Now correctly emits `module:symbol` pairs for all imported names, aliases, and grouped imports.
37
+ - **Namespace-aware call edge resolution**: Pass 2 import resolver now splits `module:symbol` import entries to narrow edge targets to the correct module file, eliminating false-positive dependency edges to duplicate class names in sibling namespaces.
38
+ - **FastAPI app version hardcoded**: `create_app()` used a hardcoded version string `"0.2.0"` instead of the canonical `__version__`. Now dynamically imported from `synap_git.__init__`.
39
+ - **Streaming generator cancellation safety**: confirmed `httpx` stream connections are closed cleanly on partial consumption (no socket leaks).
40
+ - **Degraded mode retry logic**: confirmed 2-stage exponential backoff and graceful structural fallback under fully-offline and timeout conditions.
41
+
42
+ ### Changed
43
+ - Daemon resilience test (`test_daemon_resilience.py`) hardened against race condition where `SIGKILL` test read a stale PID from a prior run that had already exited.
44
+
8
45
  ## [0.2.0] - 2026-05-26
9
46
 
10
47
  ### Added — Final Polish & Release Readiness
11
- - CLI cost management: `synap cost show` (displays Rich aggregated pricing table and summary panel) and `synap cost clear`.
48
+ - CLI usage management: `synap usage show` (displays Rich aggregated usage table and summary panel) and `synap usage clear`.
12
49
  - CLI wiki management: `synap wiki list` and `synap wiki show <filepath>` (renders page in terminal via Rich Markdown).
13
- - LLM call database logging: records `prompt_tokens`, `completion_tokens`, and calculates `cost_usd` dynamically for retrieval and wiki generation passes.
50
+ - LLM call database logging: records `prompt_tokens`, `completion_tokens` for retrieval and wiki generation passes.
14
51
  - Real-time daemon state: heartbeats integrated into `synap status`, `synap doctor`, and the Web UI status endpoints.
15
- - Premium Web UI dashboard polish: dual L3 memory (Approved vs Pending) view, real-time LLM cost analytics, and active daemon PID badge.
52
+ - Premium Web UI dashboard polish: dual L3 memory (Approved vs Pending) view, real-time LLM usage analytics, and active daemon PID badge.
16
53
  - Defensive GHA release pipeline: `.github/workflows/release.yml` automates TestPyPI and PyPI publishing, tag alignment checking, and draft release generation.
17
54
  - Clean Typer execution wrapper: intercepts configuration and credential exceptions to output actionable suggestions (e.g. `synap setup`) instead of tracebacks.
18
55
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: synap-git
3
- Version: 0.2.0
3
+ Version: 1.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
@@ -22,12 +22,15 @@ Requires-Python: >=3.11
22
22
  Requires-Dist: click==8.1.8
23
23
  Requires-Dist: fastapi==0.115.11
24
24
  Requires-Dist: gitpython==3.1.44
25
+ Requires-Dist: httpx==0.28.1
25
26
  Requires-Dist: keyring==25.6.0
26
27
  Requires-Dist: markdown-it-py==3.0.0
27
28
  Requires-Dist: mcp==1.3.0
28
29
  Requires-Dist: msgpack==1.1.0
30
+ Requires-Dist: prompt-toolkit<3.0.44
29
31
  Requires-Dist: pydantic-settings==2.8.1
30
32
  Requires-Dist: pydantic==2.10.6
33
+ Requires-Dist: questionary==2.1.0
31
34
  Requires-Dist: structlog==25.1.0
32
35
  Requires-Dist: tiktoken==0.9.0
33
36
  Requires-Dist: tree-sitter-languages==1.10.2
@@ -36,11 +39,11 @@ Requires-Dist: typer==0.15.1
36
39
  Requires-Dist: uvicorn==0.34.0
37
40
  Provides-Extra: dev
38
41
  Requires-Dist: bandit==1.8.3; extra == 'dev'
39
- Requires-Dist: httpx==0.28.1; extra == 'dev'
40
42
  Requires-Dist: mypy==1.15.0; extra == 'dev'
41
43
  Requires-Dist: pre-commit==4.1.0; extra == 'dev'
42
44
  Requires-Dist: pytest-asyncio==0.25.3; extra == 'dev'
43
45
  Requires-Dist: pytest-cov==6.0.0; extra == 'dev'
46
+ Requires-Dist: pytest-timeout==2.3.1; extra == 'dev'
44
47
  Requires-Dist: pytest==8.3.4; extra == 'dev'
45
48
  Requires-Dist: ruff==0.9.9; extra == 'dev'
46
49
  Description-Content-Type: text/markdown
@@ -71,6 +74,16 @@ Synap is **NOT a RAG system.** It is a strict, deterministic background daemon t
71
74
 
72
75
  ---
73
76
 
77
+ ## ⚡ Performance Architecture (v1.1.0)
78
+
79
+ Synap is built on the **Git-Snapshot Paradigm** to guarantee sub-100ms response times for everyday agent workflows:
80
+ - **Two separate code paths:** First-run indexing uses process-pool parallel parsing (`ProcessPoolExecutor`) to build the initial codebase structure, while subsequent indexing uses a Git delta change detector (`git diff-tree`) to process only changed files. (Reason: Avoids full filesystem scans on every run).
81
+ - **No filesystem scan / file hashing on incremental runs:** Synap uses Git blob OIDs to detect changed files. (Reason: Git already hashes everything, making filesystem reads redundant).
82
+ - **Decoupled non-deterministic LLM pipeline:** Structural parsing (deterministic, fast) completes immediately and enqueues wiki generation (non-deterministic, slow) to a background worker, while lazy caching fallback handles CLI/API wiki requests. (Reason: Never block structural indexing or CLI commands on LLM API response latency).
83
+ - **SQLite optimizations:** SQLite writes are grouped into a single transaction per batch (Reason: Avoids disk sync overhead per row). SQLite FTS5 index matches symbols in sub-milliseconds (Reason: Eliminates slow wildcard `LIKE` table scans). Pre-computed dot-separated `module_key` columns allow O(1) module resolution (Reason: Prevents suffix-matching scans).
84
+
85
+ ---
86
+
74
87
  ## 🏗️ High-Level Architecture (HLD)
75
88
 
76
89
  Synap bridges the gap between your local file system, Git history, and the LLM via a 3-layer indexing strategy.
@@ -180,9 +193,9 @@ sequenceDiagram
180
193
  ### 1. Installation
181
194
  Install the Synap CLI via `pip` or `uv`:
182
195
  ```bash
183
- pip install synapse
196
+ pip install synap-git
184
197
  # or using uv:
185
- uv tool install synapse
198
+ uv tool install synap-git
186
199
  ```
187
200
 
188
201
  ### 2. Setup & Initialize
@@ -234,8 +247,8 @@ Synap uses a powerful, strict CLI interface. Every destructive action prompts fo
234
247
  ### Developer Tools
235
248
  - `synap wiki list .` : List all generated wiki documentation files.
236
249
  - `synap wiki show <filepath> .` : Render a specific wiki markdown page to the console.
237
- - `synap cost show .` : Display detailed aggregated LLM token usage and estimated costs.
238
- - `synap cost clear .` : Purge all LLM call cost history.
250
+ - `synap usage show .` : Display detailed aggregated LLM token usage.
251
+ - `synap usage clear .` : Purge all LLM call history.
239
252
  - `synap doctor .` : Validate SQLite integrity, Tree-sitter, tokenizers, LLM providers, and daemon heartbeat.
240
253
  - `synap mcp verify .` : Verify MCP protocol, tool schemas, and contract stability.
241
254
 
@@ -249,3 +262,4 @@ Synap is built on the philosophy that AI tools must be transparent and controlla
249
262
  - Ensure all states are stored exclusively in `synap.db` or `.synap/wiki/`.
250
263
 
251
264
  License: [Apache 2.0](LICENSE.md)
265
+ ](LICENSE.md)
@@ -24,6 +24,16 @@ Synap is **NOT a RAG system.** It is a strict, deterministic background daemon t
24
24
 
25
25
  ---
26
26
 
27
+ ## ⚡ Performance Architecture (v1.1.0)
28
+
29
+ Synap is built on the **Git-Snapshot Paradigm** to guarantee sub-100ms response times for everyday agent workflows:
30
+ - **Two separate code paths:** First-run indexing uses process-pool parallel parsing (`ProcessPoolExecutor`) to build the initial codebase structure, while subsequent indexing uses a Git delta change detector (`git diff-tree`) to process only changed files. (Reason: Avoids full filesystem scans on every run).
31
+ - **No filesystem scan / file hashing on incremental runs:** Synap uses Git blob OIDs to detect changed files. (Reason: Git already hashes everything, making filesystem reads redundant).
32
+ - **Decoupled non-deterministic LLM pipeline:** Structural parsing (deterministic, fast) completes immediately and enqueues wiki generation (non-deterministic, slow) to a background worker, while lazy caching fallback handles CLI/API wiki requests. (Reason: Never block structural indexing or CLI commands on LLM API response latency).
33
+ - **SQLite optimizations:** SQLite writes are grouped into a single transaction per batch (Reason: Avoids disk sync overhead per row). SQLite FTS5 index matches symbols in sub-milliseconds (Reason: Eliminates slow wildcard `LIKE` table scans). Pre-computed dot-separated `module_key` columns allow O(1) module resolution (Reason: Prevents suffix-matching scans).
34
+
35
+ ---
36
+
27
37
  ## 🏗️ High-Level Architecture (HLD)
28
38
 
29
39
  Synap bridges the gap between your local file system, Git history, and the LLM via a 3-layer indexing strategy.
@@ -133,9 +143,9 @@ sequenceDiagram
133
143
  ### 1. Installation
134
144
  Install the Synap CLI via `pip` or `uv`:
135
145
  ```bash
136
- pip install synapse
146
+ pip install synap-git
137
147
  # or using uv:
138
- uv tool install synapse
148
+ uv tool install synap-git
139
149
  ```
140
150
 
141
151
  ### 2. Setup & Initialize
@@ -187,8 +197,8 @@ Synap uses a powerful, strict CLI interface. Every destructive action prompts fo
187
197
  ### Developer Tools
188
198
  - `synap wiki list .` : List all generated wiki documentation files.
189
199
  - `synap wiki show <filepath> .` : Render a specific wiki markdown page to the console.
190
- - `synap cost show .` : Display detailed aggregated LLM token usage and estimated costs.
191
- - `synap cost clear .` : Purge all LLM call cost history.
200
+ - `synap usage show .` : Display detailed aggregated LLM token usage.
201
+ - `synap usage clear .` : Purge all LLM call history.
192
202
  - `synap doctor .` : Validate SQLite integrity, Tree-sitter, tokenizers, LLM providers, and daemon heartbeat.
193
203
  - `synap mcp verify .` : Verify MCP protocol, tool schemas, and contract stability.
194
204
 
@@ -202,3 +212,4 @@ Synap is built on the philosophy that AI tools must be transparent and controlla
202
212
  - Ensure all states are stored exclusively in `synap.db` or `.synap/wiki/`.
203
213
 
204
214
  License: [Apache 2.0](LICENSE.md)
215
+ ](LICENSE.md)
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "synap-git"
3
- version = "0.2.0"
3
+ dynamic = ["version"]
4
4
  description = "Persistent structural context infrastructure for AI coding agents."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -22,12 +22,15 @@ dependencies = [
22
22
  "click==8.1.8",
23
23
  "fastapi==0.115.11",
24
24
  "gitpython==3.1.44",
25
+ "httpx==0.28.1",
25
26
  "keyring==25.6.0",
26
27
  "markdown-it-py==3.0.0",
27
28
  "mcp==1.3.0",
28
29
  "msgpack==1.1.0",
30
+ "prompt-toolkit<3.0.44",
29
31
  "pydantic==2.10.6",
30
32
  "pydantic-settings==2.8.1",
33
+ "questionary==2.1.0",
31
34
  "structlog==25.1.0",
32
35
  "tiktoken==0.9.0",
33
36
  "tree-sitter==0.20.4",
@@ -46,12 +49,12 @@ Changelog = "https://github.com/saahilpal/synap-git/blob/main/CHANGELOG.md"
46
49
  [project.optional-dependencies]
47
50
  dev = [
48
51
  "bandit==1.8.3",
49
- "httpx==0.28.1",
50
52
  "mypy==1.15.0",
51
53
  "pre-commit==4.1.0",
52
54
  "pytest==8.3.4",
53
55
  "pytest-asyncio==0.25.3",
54
56
  "pytest-cov==6.0.0",
57
+ "pytest-timeout==2.3.1",
55
58
  "ruff==0.9.9",
56
59
  ]
57
60
 
@@ -62,6 +65,9 @@ synap = "synap_git.cli:app"
62
65
  requires = ["hatchling"]
63
66
  build-backend = "hatchling.build"
64
67
 
68
+ [tool.hatch.version]
69
+ path = "src/synap_git/__init__.py"
70
+
65
71
  [tool.hatch.build.targets.sdist]
66
72
  include = [
67
73
  "src/synap_git",
@@ -81,7 +87,7 @@ src = ["src", "tests"]
81
87
 
82
88
  [tool.ruff.lint]
83
89
  select = ["A", "B", "C4", "E", "F", "I", "N", "UP", "W", "S", "PT", "ARG", "PTH"]
84
- ignore = ["B008", "E501", "B904", "B007", "F841", "W291", "S101", "S105", "S106", "ARG001", "ARG002", "S110", "S310"]
90
+ ignore = ["B008", "E501", "B904", "B007", "F841", "W291", "S101", "S105", "S106", "ARG001", "ARG002", "S110", "S310", "N806", "S603", "S607", "PTH123"]
85
91
 
86
92
  [tool.ruff.lint.per-file-ignores]
87
93
  "tests/*" = ["S603"]
@@ -104,7 +110,7 @@ module = ["msgpack"]
104
110
  ignore_missing_imports = true
105
111
 
106
112
  [tool.pytest.ini_options]
107
- addopts = "-ra --strict-markers --strict-config"
113
+ addopts = "-ra --strict-markers --strict-config --timeout=60"
108
114
  testpaths = ["tests"]
109
115
  asyncio_mode = "auto"
110
116
  asyncio_default_fixture_loop_scope = "function"
@@ -2,4 +2,4 @@
2
2
 
3
3
  __all__ = ["__version__"]
4
4
 
5
- __version__ = "0.2.0"
5
+ __version__ = "1.1.0"
@@ -10,6 +10,7 @@ from fastapi.middleware.cors import CORSMiddleware
10
10
  from fastapi.responses import HTMLResponse, StreamingResponse
11
11
  from fastapi.staticfiles import StaticFiles
12
12
 
13
+ from synap_git import __version__
13
14
  from synap_git.indexer.engine import SynapRuntime
14
15
 
15
16
 
@@ -17,7 +18,7 @@ def create_app(runtime: SynapRuntime) -> FastAPI:
17
18
  app = FastAPI(
18
19
  title="Synap Context Diagnostics",
19
20
  description="Deterministic structural context infrastructure for AI coding agents.",
20
- version="0.2.0",
21
+ version=__version__,
21
22
  )
22
23
 
23
24
  app.add_middleware(
@@ -36,17 +37,20 @@ def create_app(runtime: SynapRuntime) -> FastAPI:
36
37
  @app.get("/", response_class=HTMLResponse)
37
38
  async def get_index() -> str:
38
39
  index_file = static_dir / "index.html"
39
- if index_file.exists():
40
- return index_file.read_text(encoding="utf-8")
40
+ exists = await asyncio.to_thread(index_file.exists)
41
+ if exists:
42
+ return await asyncio.to_thread(index_file.read_text, encoding="utf-8")
41
43
  return "<h1>Synap Diagnostic UI is ready.</h1>"
42
44
 
43
45
  @app.get("/api/v1/status")
44
46
  async def get_status() -> dict[str, Any]:
45
47
  try:
46
- status = runtime.status()
48
+ status = await asyncio.to_thread(runtime.status)
47
49
  from synap_git.cli.main import _read_daemon_heartbeat
48
50
 
49
- daemon_info = _read_daemon_heartbeat(Path(status.repository_path))
51
+ daemon_info = await asyncio.to_thread(
52
+ _read_daemon_heartbeat, Path(status.repository_path)
53
+ )
50
54
  return {
51
55
  "repository_path": status.repository_path,
52
56
  "branch": status.branch,
@@ -63,7 +67,9 @@ def create_app(runtime: SynapRuntime) -> FastAPI:
63
67
  @app.get("/api/v1/trace/latest")
64
68
  async def get_latest_trace() -> dict[str, Any]:
65
69
  try:
66
- return runtime.trace_store.get_latest()
70
+ if runtime.trace_store:
71
+ return await asyncio.to_thread(runtime.trace_store.get_latest)
72
+ return {}
67
73
  except Exception as exc:
68
74
  raise HTTPException(status_code=500, detail=str(exc))
69
75
 
@@ -73,7 +79,7 @@ def create_app(runtime: SynapRuntime) -> FastAPI:
73
79
 
74
80
  async def event_generator() -> AsyncGenerator[str, None]:
75
81
  while True:
76
- status = runtime.status()
82
+ status = await asyncio.to_thread(runtime.status)
77
83
  yield f"data: {json.dumps(status.__dict__)}\n\n"
78
84
  await asyncio.sleep(2)
79
85
 
@@ -81,31 +87,47 @@ def create_app(runtime: SynapRuntime) -> FastAPI:
81
87
 
82
88
  @app.get("/wiki/{filepath:path}")
83
89
  async def get_wiki_page(filepath: str) -> dict[str, Any]:
84
- wiki_path = runtime.wiki.wiki_dir / f"{filepath}.md"
85
- if wiki_path.exists():
86
- return {"status": "ok", "content": wiki_path.read_text(encoding="utf-8")}
90
+ target = filepath
91
+ if target.endswith(".md"):
92
+ target = target[:-3]
93
+ try:
94
+ await asyncio.to_thread(runtime.wiki.ensure_wiki_page, target)
95
+ except Exception:
96
+ pass
97
+
98
+ wiki_path = runtime.wiki.wiki_dir / f"{target}.md"
99
+ exists = await asyncio.to_thread(wiki_path.exists)
100
+ if exists:
101
+ content = await asyncio.to_thread(wiki_path.read_text, encoding="utf-8")
102
+ return {"status": "ok", "content": content}
87
103
  return {"status": "error", "message": "Wiki not found"}
88
104
 
89
105
  @app.get("/api/v1/memory")
90
106
  async def get_memory_page() -> dict[str, Any]:
91
- approved = runtime.store.get_lessons("approved")
92
- pending = runtime.store.get_lessons("pending")
107
+ approved = await asyncio.to_thread(runtime.store.get_lessons, "approved")
108
+ pending = await asyncio.to_thread(runtime.store.get_lessons, "pending")
93
109
  return {"status": "ok", "approved": approved, "pending": pending}
94
110
 
95
- @app.get("/api/v1/cost")
96
- async def get_cost_page() -> dict[str, Any]:
111
+ def _fetch_calls() -> list[dict[str, Any]]:
97
112
  with runtime.store.connect() as conn:
98
113
  rows = conn.execute("SELECT * FROM llm_calls ORDER BY created_at DESC").fetchall()
99
- calls = [dict(r) for r in rows]
114
+ return [dict(r) for r in rows]
115
+
116
+ @app.get("/api/v1/usage")
117
+ async def get_usage_page() -> dict[str, Any]:
118
+ calls = await asyncio.to_thread(_fetch_calls)
100
119
  return {"status": "ok", "calls": calls}
101
120
 
102
- @app.get("/api/v1/checkpoints")
103
- async def get_checkpoints_page() -> dict[str, Any]:
121
+ def _fetch_checkpoints() -> list[dict[str, Any]]:
104
122
  with runtime.store.connect() as conn:
105
123
  rows = conn.execute(
106
124
  "SELECT * FROM checkpoints ORDER BY created_at DESC LIMIT 20"
107
125
  ).fetchall()
108
- cps = [dict(r) for r in rows]
126
+ return [dict(r) for r in rows]
127
+
128
+ @app.get("/api/v1/checkpoints")
129
+ async def get_checkpoints_page() -> dict[str, Any]:
130
+ cps = await asyncio.to_thread(_fetch_checkpoints)
109
131
  return {"status": "ok", "checkpoints": cps}
110
132
 
111
133
  return app
@@ -202,15 +202,11 @@
202
202
  </div>
203
203
  </div>
204
204
 
205
- <!-- LLM Cost & Usage -->
205
+ <!-- LLM Usage -->
206
206
  <div class="card" style="grid-column: span 2;">
207
- <h2>LLM Call Cost & Usage</h2>
207
+ <h2>LLM Call Usage</h2>
208
208
  <div style="display: grid; grid-template-columns: 1fr 2fr; gap: 1.5rem;">
209
209
  <div class="stats-grid" style="grid-template-columns: 1fr; gap: 0.75rem;">
210
- <div class="stat-box">
211
- <div class="stat-value" id="cost-total-usd">$0.000000</div>
212
- <div class="stat-label">Total Cost (USD)</div>
213
- </div>
214
210
  <div class="stat-box">
215
211
  <div class="stat-value" id="cost-total-calls">0</div>
216
212
  <div class="stat-label">Total LLM Calls</div>
@@ -231,11 +227,10 @@
231
227
  <th>Model</th>
232
228
  <th>Purpose</th>
233
229
  <th>Tokens</th>
234
- <th>Cost</th>
235
230
  </tr>
236
231
  </thead>
237
232
  <tbody id="cost-calls-body">
238
- <tr><td colspan="6" class="empty-state">No calls recorded.</td></tr>
233
+ <tr><td colspan="5" class="empty-state">No calls recorded.</td></tr>
239
234
  </tbody>
240
235
  </table>
241
236
  </div>
@@ -443,44 +438,59 @@
443
438
  pendingBody.innerHTML = `<tr><td colspan="3" class="empty-state">No pending lessons needing approval.</td></tr>`;
444
439
  }
445
440
 
446
- // Fetch Cost & Usage
447
- const costRes = await fetch('/api/v1/cost');
448
- const costData = await costRes.json();
441
+ // Fetch Usage
442
+ const usageRes = await fetch('/api/v1/usage');
443
+ const usageData = await usageRes.json();
449
444
  const costBody = document.getElementById('cost-calls-body');
450
445
 
451
- let totalCost = 0;
452
446
  let totalCalls = 0;
453
447
  let totalTokens = 0;
454
448
 
455
- if (costData.calls && costData.calls.length > 0) {
456
- totalCalls = costData.calls.length;
457
- costBody.innerHTML = costData.calls.slice(0, 15).map(c => {
458
- totalCost += c.cost_usd;
449
+ if (usageData.calls && usageData.calls.length > 0) {
450
+ totalCalls = usageData.calls.length;
451
+ usageData.calls.slice(0, 15).map(c => {
459
452
  const tokens = c.input_tokens + c.output_tokens;
460
453
  totalTokens += tokens;
461
454
 
462
455
  const timeStr = new Date(c.created_at * 1000).toLocaleTimeString();
463
- return `
456
+ const row = `
464
457
  <tr class="row-hover">
465
458
  <td>${timeStr}</td>
466
459
  <td>${c.provider}</td>
467
460
  <td><code>${c.model}</code></td>
468
461
  <td>${c.purpose}</td>
469
462
  <td>${tokens.toLocaleString()}</td>
470
- <td style="color:var(--success); font-family:var(--font-mono); font-size:11px;">$${c.cost_usd.toFixed(6)}</td>
471
463
  </tr>
472
464
  `;
473
- }).join('');
465
+ costBody.insertAdjacentHTML('beforeend', row);
466
+ });
467
+
468
+ // Clear the loading message if it exists
469
+ if (usageData.calls.length > 0 && costBody.querySelector('.empty-state')) {
470
+ costBody.innerHTML = '';
471
+ // Re-render the 15 calls because we just cleared it
472
+ costBody.innerHTML = usageData.calls.slice(0, 15).map(c => {
473
+ const tokens = c.input_tokens + c.output_tokens;
474
+ const timeStr = new Date(c.created_at * 1000).toLocaleTimeString();
475
+ return `
476
+ <tr class="row-hover">
477
+ <td>${timeStr}</td>
478
+ <td>${c.provider}</td>
479
+ <td><code>${c.model}</code></td>
480
+ <td>${c.purpose}</td>
481
+ <td>${tokens.toLocaleString()}</td>
482
+ </tr>
483
+ `;
484
+ }).join('');
485
+ }
474
486
 
475
- costData.calls.slice(15).forEach(c => {
476
- totalCost += c.cost_usd;
487
+ usageData.calls.slice(15).forEach(c => {
477
488
  totalTokens += (c.input_tokens + c.output_tokens);
478
489
  });
479
490
  } else {
480
- costBody.innerHTML = `<tr><td colspan="6" class="empty-state">No LLM calls recorded yet.</td></tr>`;
491
+ costBody.innerHTML = `<tr><td colspan="5" class="empty-state">No LLM calls recorded yet.</td></tr>`;
481
492
  }
482
493
 
483
- document.getElementById('cost-total-usd').textContent = `$${totalCost.toFixed(6)}`;
484
494
  document.getElementById('cost-total-calls').textContent = totalCalls.toLocaleString();
485
495
  document.getElementById('cost-total-tokens').textContent = totalTokens.toLocaleString();
486
496