synap-git 2.1.7__tar.gz → 2.2.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.1.7 → synap_git-2.2.0}/.gitignore +3 -0
- {synap_git-2.1.7 → synap_git-2.2.0}/CHANGELOG.md +16 -0
- {synap_git-2.1.7 → synap_git-2.2.0}/PKG-INFO +3 -2
- {synap_git-2.1.7 → synap_git-2.2.0}/pyproject.toml +1 -0
- {synap_git-2.1.7 → synap_git-2.2.0}/src/synap_git/__init__.py +1 -1
- {synap_git-2.1.7 → synap_git-2.2.0}/src/synap_git/cli/main.py +61 -25
- {synap_git-2.1.7 → synap_git-2.2.0}/src/synap_git/indexer/daemon.py +101 -44
- {synap_git-2.1.7 → synap_git-2.2.0}/src/synap_git/indexer/engine.py +1 -0
- {synap_git-2.1.7 → synap_git-2.2.0}/src/synap_git/parser/registry.py +46 -0
- {synap_git-2.1.7 → synap_git-2.2.0}/src/synap_git/provider/anthropic.py +6 -5
- {synap_git-2.1.7 → synap_git-2.2.0}/src/synap_git/provider/openrouter.py +6 -2
- {synap_git-2.1.7 → synap_git-2.2.0}/src/synap_git/retrieval/engine.py +9 -4
- {synap_git-2.1.7 → synap_git-2.2.0}/src/synap_git/storage/sqlite.py +14 -0
- {synap_git-2.1.7 → synap_git-2.2.0}/.synap.example/README.md +0 -0
- {synap_git-2.1.7 → synap_git-2.2.0}/LICENSE.md +0 -0
- {synap_git-2.1.7 → synap_git-2.2.0}/README.md +0 -0
- {synap_git-2.1.7 → synap_git-2.2.0}/src/synap_git/api/__init__.py +0 -0
- {synap_git-2.1.7 → synap_git-2.2.0}/src/synap_git/api/app.py +0 -0
- {synap_git-2.1.7 → synap_git-2.2.0}/src/synap_git/api/static/index.html +0 -0
- {synap_git-2.1.7 → synap_git-2.2.0}/src/synap_git/cli/__init__.py +0 -0
- {synap_git-2.1.7 → synap_git-2.2.0}/src/synap_git/cli/__main__.py +0 -0
- {synap_git-2.1.7 → synap_git-2.2.0}/src/synap_git/cli/checkpoint.py +0 -0
- {synap_git-2.1.7 → synap_git-2.2.0}/src/synap_git/cli/lessons.py +0 -0
- {synap_git-2.1.7 → synap_git-2.2.0}/src/synap_git/cli/memory.py +0 -0
- {synap_git-2.1.7 → synap_git-2.2.0}/src/synap_git/cli/usage.py +0 -0
- {synap_git-2.1.7 → synap_git-2.2.0}/src/synap_git/cli/wiki.py +0 -0
- {synap_git-2.1.7 → synap_git-2.2.0}/src/synap_git/config.py +0 -0
- {synap_git-2.1.7 → synap_git-2.2.0}/src/synap_git/diagnostics/__init__.py +0 -0
- {synap_git-2.1.7 → synap_git-2.2.0}/src/synap_git/diagnostics/logger.py +0 -0
- {synap_git-2.1.7 → synap_git-2.2.0}/src/synap_git/diagnostics/tracing.py +0 -0
- {synap_git-2.1.7 → synap_git-2.2.0}/src/synap_git/embeddings/__init__.py +0 -0
- {synap_git-2.1.7 → synap_git-2.2.0}/src/synap_git/git/__init__.py +0 -0
- {synap_git-2.1.7 → synap_git-2.2.0}/src/synap_git/git/state.py +0 -0
- {synap_git-2.1.7 → synap_git-2.2.0}/src/synap_git/indexer/__init__.py +0 -0
- {synap_git-2.1.7 → synap_git-2.2.0}/src/synap_git/indexer/scanner.py +0 -0
- {synap_git-2.1.7 → synap_git-2.2.0}/src/synap_git/indexer/wiki.py +0 -0
- {synap_git-2.1.7 → synap_git-2.2.0}/src/synap_git/mcp/__init__.py +0 -0
- {synap_git-2.1.7 → synap_git-2.2.0}/src/synap_git/mcp/server.py +0 -0
- {synap_git-2.1.7 → synap_git-2.2.0}/src/synap_git/parser/__init__.py +0 -0
- {synap_git-2.1.7 → synap_git-2.2.0}/src/synap_git/provider/__init__.py +0 -0
- {synap_git-2.1.7 → synap_git-2.2.0}/src/synap_git/provider/base.py +0 -0
- {synap_git-2.1.7 → synap_git-2.2.0}/src/synap_git/provider/factory.py +0 -0
- {synap_git-2.1.7 → synap_git-2.2.0}/src/synap_git/provider/gemini.py +0 -0
- {synap_git-2.1.7 → synap_git-2.2.0}/src/synap_git/provider/ollama.py +0 -0
- {synap_git-2.1.7 → synap_git-2.2.0}/src/synap_git/provider/openai.py +0 -0
- {synap_git-2.1.7 → synap_git-2.2.0}/src/synap_git/py.typed +0 -0
- {synap_git-2.1.7 → synap_git-2.2.0}/src/synap_git/retrieval/__init__.py +0 -0
- {synap_git-2.1.7 → synap_git-2.2.0}/src/synap_git/retrieval/memory.py +0 -0
- {synap_git-2.1.7 → synap_git-2.2.0}/src/synap_git/security/__init__.py +0 -0
- {synap_git-2.1.7 → synap_git-2.2.0}/src/synap_git/security/redactor.py +0 -0
- {synap_git-2.1.7 → synap_git-2.2.0}/src/synap_git/security/sanitizer.py +0 -0
- {synap_git-2.1.7 → synap_git-2.2.0}/src/synap_git/security/validator.py +0 -0
- {synap_git-2.1.7 → synap_git-2.2.0}/src/synap_git/storage/__init__.py +0 -0
- {synap_git-2.1.7 → synap_git-2.2.0}/src/synap_git/utils/__init__.py +0 -0
- {synap_git-2.1.7 → synap_git-2.2.0}/src/synap_git/utils/serialization.py +0 -0
- {synap_git-2.1.7 → synap_git-2.2.0}/website/README.md +0 -0
|
@@ -5,6 +5,22 @@ 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.2.0] - 2026-08-03
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **AST Symbol Parser Expansion (AUD-02)**: Implemented Tree-sitter AST symbol extraction handlers for C#, PHP, Kotlin, Scala, and C.
|
|
12
|
+
- **Event-Driven File Watcher (AUD-03)**: Replaced continuous 2s subprocess polling with `watchdog` filesystem event tracking, reducing idle CPU usage to near-zero.
|
|
13
|
+
- **Daemon REST API Diagnostic Checks (AUD-06)**: Added daemon port availability and REST API endpoint status checks to `synap doctor`.
|
|
14
|
+
- **Wiki Task Requeue Subcommand (AUD-08)**: Added `synap wiki retry` CLI subcommand and `retry_failed_wiki_queue` storage method to requeue permanently failed wiki tasks back to pending state.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
- **CLI `synap start` Control Flow (AUD-01)**: Fixed `synap start` so it exits cleanly when a daemon is already running unless `--foreground` is explicitly passed.
|
|
18
|
+
- **SQLite WAL Performance Rationale (AUD-07)**: Documented `PRAGMA synchronous=NORMAL` in WAL mode as an intentional architectural tradeoff balancing write throughput and self-healing index recovery.
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
- **Graceful Embedding Provider Fallback (AUD-04)**: Configured Anthropic and OpenRouter providers to log a warning and return empty embedding vectors instead of raising unhandled `NotImplementedError`.
|
|
22
|
+
- **Symbol Preservation on File Renames (AUD-05)**: Added `-M` rename detection to `git diff-tree` in incremental indexing to preserve file symbol history and edges across renames.
|
|
23
|
+
|
|
8
24
|
## [2.1.7] - 2026-07-29
|
|
9
25
|
|
|
10
26
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: synap-git
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.2.0
|
|
4
4
|
Summary: Persistent structural context infrastructure for AI coding agents.
|
|
5
5
|
Project-URL: Homepage, https://github.com/saahilpal/synapse
|
|
6
6
|
Project-URL: Repository, https://github.com/saahilpal/synapse
|
|
@@ -37,6 +37,7 @@ Requires-Dist: tree-sitter-languages==1.10.2
|
|
|
37
37
|
Requires-Dist: tree-sitter==0.20.4
|
|
38
38
|
Requires-Dist: typer==0.15.1
|
|
39
39
|
Requires-Dist: uvicorn==0.34.0
|
|
40
|
+
Requires-Dist: watchdog>=6.0.0
|
|
40
41
|
Provides-Extra: dev
|
|
41
42
|
Requires-Dist: bandit==1.8.3; extra == 'dev'
|
|
42
43
|
Requires-Dist: mypy==1.15.0; extra == 'dev'
|
|
@@ -652,6 +652,12 @@ def _detect_install_method() -> str:
|
|
|
652
652
|
@app.command()
|
|
653
653
|
def start(
|
|
654
654
|
path: Annotated[str, typer.Argument(help="Repository path to watch.")] = ".",
|
|
655
|
+
foreground: Annotated[
|
|
656
|
+
bool,
|
|
657
|
+
typer.Option(
|
|
658
|
+
"--foreground", help="Run FastMCP stdio server in foreground after starting daemon."
|
|
659
|
+
),
|
|
660
|
+
] = False,
|
|
655
661
|
) -> None:
|
|
656
662
|
"""Start the Synap daemon in the background."""
|
|
657
663
|
_verify_is_git(path)
|
|
@@ -676,6 +682,8 @@ def start(
|
|
|
676
682
|
if hb and "port" in hb:
|
|
677
683
|
console.print(f"[green]✓ UI available at http://127.0.0.1:{hb['port']}[/green]")
|
|
678
684
|
is_running = True
|
|
685
|
+
if not foreground:
|
|
686
|
+
return
|
|
679
687
|
else:
|
|
680
688
|
pid_file.unlink()
|
|
681
689
|
except Exception as e:
|
|
@@ -739,7 +747,10 @@ def start(
|
|
|
739
747
|
)
|
|
740
748
|
raise typer.Exit(1)
|
|
741
749
|
|
|
742
|
-
|
|
750
|
+
if not foreground:
|
|
751
|
+
return
|
|
752
|
+
|
|
753
|
+
# 3. Start MCP Server in the foreground if --foreground flag passed
|
|
743
754
|
settings = _settings(path, json_output=True)
|
|
744
755
|
runtime = SynapRuntime(settings)
|
|
745
756
|
server = SynapMCPServer(runtime)
|
|
@@ -1496,40 +1507,52 @@ def doctor(
|
|
|
1496
1507
|
task_prov, completed=1, description=f"[red]✗ Unexpected error: {e}[/red]"
|
|
1497
1508
|
)
|
|
1498
1509
|
|
|
1499
|
-
task_daemon = progress.add_task("Checking Daemon Status...", total=1)
|
|
1510
|
+
task_daemon = progress.add_task("Checking Daemon & REST API Status...", total=1)
|
|
1500
1511
|
try:
|
|
1512
|
+
import socket
|
|
1513
|
+
|
|
1514
|
+
import httpx
|
|
1515
|
+
|
|
1501
1516
|
daemon_info = _read_daemon_heartbeat(settings.repository_path)
|
|
1502
|
-
if daemon_info
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1517
|
+
port = daemon_info.get("port", 9876) if daemon_info else 9876
|
|
1518
|
+
|
|
1519
|
+
# Test port availability
|
|
1520
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
1521
|
+
s.settimeout(0.5)
|
|
1522
|
+
port_in_use = s.connect_ex(("127.0.0.1", port)) == 0
|
|
1523
|
+
|
|
1524
|
+
if daemon_info and port_in_use:
|
|
1525
|
+
try:
|
|
1526
|
+
r = httpx.get(f"http://127.0.0.1:{port}/api/status", timeout=2.0)
|
|
1527
|
+
if r.status_code == 200:
|
|
1528
|
+
progress.update(
|
|
1529
|
+
task_daemon,
|
|
1530
|
+
completed=1,
|
|
1531
|
+
description=f"[green]✓ Daemon active & REST API healthy (PID {daemon_info['pid']}, port {port})[/green]",
|
|
1532
|
+
)
|
|
1533
|
+
else:
|
|
1534
|
+
progress.update(
|
|
1535
|
+
task_daemon,
|
|
1536
|
+
completed=1,
|
|
1537
|
+
description=f"[yellow]⚠ Daemon running on port {port} but REST API returned HTTP {r.status_code}[/yellow]",
|
|
1538
|
+
)
|
|
1539
|
+
except Exception as ex:
|
|
1523
1540
|
progress.update(
|
|
1524
1541
|
task_daemon,
|
|
1525
1542
|
completed=1,
|
|
1526
|
-
description=f"[
|
|
1543
|
+
description=f"[yellow]⚠ Daemon running on port {port} but API check failed: {ex}[/yellow]",
|
|
1527
1544
|
)
|
|
1545
|
+
elif daemon_info:
|
|
1546
|
+
progress.update(
|
|
1547
|
+
task_daemon,
|
|
1548
|
+
completed=1,
|
|
1549
|
+
description=f"[yellow]⚠ Heartbeat found for PID {daemon_info['pid']} but port {port} is not listening[/yellow]",
|
|
1550
|
+
)
|
|
1528
1551
|
else:
|
|
1529
1552
|
progress.update(
|
|
1530
1553
|
task_daemon,
|
|
1531
1554
|
completed=1,
|
|
1532
|
-
description="[yellow]⚠ Daemon inactive (
|
|
1555
|
+
description=f"[yellow]⚠ Daemon inactive (port {port} available). Start using `synap start`[/yellow]",
|
|
1533
1556
|
)
|
|
1534
1557
|
except Exception as e:
|
|
1535
1558
|
progress.update(
|
|
@@ -2173,3 +2196,16 @@ def wiki_show(
|
|
|
2173
2196
|
|
|
2174
2197
|
content = wiki_path.read_text(encoding="utf-8")
|
|
2175
2198
|
console.print(Markdown(content))
|
|
2199
|
+
|
|
2200
|
+
|
|
2201
|
+
@wiki_app.command("retry")
|
|
2202
|
+
def wiki_retry(
|
|
2203
|
+
path: Annotated[str, typer.Argument(help="Repository path.")] = ".",
|
|
2204
|
+
) -> None:
|
|
2205
|
+
"""Requeue all permanently failed wiki tasks back to pending state."""
|
|
2206
|
+
runtime = SynapRuntime(_settings(path))
|
|
2207
|
+
count = runtime.store.retry_failed_wiki_queue()
|
|
2208
|
+
if count > 0:
|
|
2209
|
+
console.print(f"[green]✓ Requeued {count} failed wiki tasks to pending state.[/green]")
|
|
2210
|
+
else:
|
|
2211
|
+
console.print("[yellow]No failed wiki tasks found in queue.[/yellow]")
|
|
@@ -35,6 +35,8 @@ class RuntimeDaemon:
|
|
|
35
35
|
self._last_metrics_time: float = 0.0
|
|
36
36
|
self._last_cpu_time: float = 0.0
|
|
37
37
|
self._last_prune_time: float = 0.0
|
|
38
|
+
self._fs_changed = False
|
|
39
|
+
self._observer: Any = None
|
|
38
40
|
|
|
39
41
|
async def start(self) -> None:
|
|
40
42
|
self.runtime.initialize_storage()
|
|
@@ -91,6 +93,44 @@ class RuntimeDaemon:
|
|
|
91
93
|
self._write_heartbeat(status="healthy")
|
|
92
94
|
self._install_signal_handlers()
|
|
93
95
|
|
|
96
|
+
# Start watchdog file observer for event-driven git indexing
|
|
97
|
+
self._fs_changed = False
|
|
98
|
+
self._observer = None
|
|
99
|
+
try:
|
|
100
|
+
from watchdog.events import FileSystemEventHandler
|
|
101
|
+
from watchdog.observers import Observer
|
|
102
|
+
|
|
103
|
+
daemon_ref = self
|
|
104
|
+
|
|
105
|
+
class _WatcherHandler(FileSystemEventHandler):
|
|
106
|
+
def on_any_event(self, event: Any) -> None:
|
|
107
|
+
path_str = str(getattr(event, "src_path", ""))
|
|
108
|
+
if any(
|
|
109
|
+
ignored in path_str
|
|
110
|
+
for ignored in (
|
|
111
|
+
".synap",
|
|
112
|
+
".git",
|
|
113
|
+
".venv",
|
|
114
|
+
"__pycache__",
|
|
115
|
+
"node_modules",
|
|
116
|
+
".next",
|
|
117
|
+
"dist",
|
|
118
|
+
"build",
|
|
119
|
+
)
|
|
120
|
+
):
|
|
121
|
+
return
|
|
122
|
+
daemon_ref._fs_changed = True
|
|
123
|
+
|
|
124
|
+
self._observer = Observer()
|
|
125
|
+
self._observer.schedule(
|
|
126
|
+
_WatcherHandler(), str(self.settings.repository_path), recursive=True
|
|
127
|
+
)
|
|
128
|
+
self._observer.start()
|
|
129
|
+
self.logger.info("watchdog_observer_started")
|
|
130
|
+
except Exception as e:
|
|
131
|
+
self.logger.warning("watchdog_observer_init_failed", error=str(e))
|
|
132
|
+
self._observer = None
|
|
133
|
+
|
|
94
134
|
# Launch Uvicorn and Wiki Worker in background
|
|
95
135
|
server_task = asyncio.create_task(self._ui_server.serve())
|
|
96
136
|
wiki_worker_task = asyncio.create_task(self._wiki_worker_loop())
|
|
@@ -98,6 +138,13 @@ class RuntimeDaemon:
|
|
|
98
138
|
try:
|
|
99
139
|
await self._poll_git_loop()
|
|
100
140
|
finally:
|
|
141
|
+
if self._observer:
|
|
142
|
+
try:
|
|
143
|
+
self._observer.stop()
|
|
144
|
+
self._observer.join(timeout=1.0)
|
|
145
|
+
except Exception:
|
|
146
|
+
pass
|
|
147
|
+
|
|
101
148
|
self._running = False
|
|
102
149
|
self.logger.info("daemon_shutting_down")
|
|
103
150
|
self._ui_server.should_exit = True
|
|
@@ -258,60 +305,70 @@ class RuntimeDaemon:
|
|
|
258
305
|
structlog.get_logger().error("suppressed_error_caught", error=str(e))
|
|
259
306
|
|
|
260
307
|
async def _poll_git_loop(self) -> None:
|
|
308
|
+
backstop_interval = 30.0
|
|
309
|
+
poll_step = 0.5
|
|
310
|
+
elapsed = backstop_interval # Force initial check on loop start
|
|
311
|
+
|
|
261
312
|
while not self._stop_event.is_set():
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
if now - self._last_prune_time > 3600:
|
|
266
|
-
self._last_prune_time = now
|
|
267
|
-
count = await asyncio.to_thread(self.runtime.store.prune_expired_lessons)
|
|
268
|
-
if count > 0:
|
|
269
|
-
self.logger.info("lessons_pruned", count=count)
|
|
270
|
-
print(f"\n[Synapse] Pruned {count} expired lessons.")
|
|
271
|
-
|
|
272
|
-
state = self.git.state()
|
|
273
|
-
change = self.git.classify(self._last_git_state, state)
|
|
274
|
-
self._last_git_state = state
|
|
275
|
-
|
|
276
|
-
if change.kind == "commit":
|
|
277
|
-
self.logger.info("git_commit_detected")
|
|
278
|
-
await asyncio.to_thread(self.runtime.handle_commit, state)
|
|
279
|
-
elif change.kind in ("branch", "checkout"):
|
|
280
|
-
self.logger.info("git_branch_switch_detected")
|
|
281
|
-
await asyncio.to_thread(self.runtime.handle_branch_switch, state)
|
|
282
|
-
elif change.kind == "merge":
|
|
283
|
-
self.logger.info("git_merge_detected")
|
|
284
|
-
await asyncio.to_thread(self.runtime.handle_merge, state)
|
|
285
|
-
elif change.kind == "revert":
|
|
286
|
-
self.logger.info("git_revert_detected")
|
|
287
|
-
await asyncio.to_thread(self.runtime.handle_revert, self._last_git_state, state)
|
|
288
|
-
elif change.kind not in ("initial", "unchanged"):
|
|
289
|
-
self.logger.info("git_change_detected", kind=change.kind)
|
|
290
|
-
await asyncio.to_thread(self.runtime.index_repository, git_state=state)
|
|
291
|
-
|
|
292
|
-
self._write_heartbeat(status="healthy")
|
|
293
|
-
except Exception as e:
|
|
294
|
-
self.logger.error("daemon_loop_error", error=str(e))
|
|
295
|
-
self._recovery_attempts += 1
|
|
296
|
-
self._write_heartbeat(status="degraded", last_error=str(e))
|
|
313
|
+
if self._fs_changed or elapsed >= backstop_interval:
|
|
314
|
+
self._fs_changed = False
|
|
315
|
+
elapsed = 0.0
|
|
297
316
|
|
|
298
|
-
# Database recovery if corrupted
|
|
299
317
|
try:
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
318
|
+
# Periodic pruning (once per hour)
|
|
319
|
+
now = time.time()
|
|
320
|
+
if now - self._last_prune_time > 3600:
|
|
321
|
+
self._last_prune_time = now
|
|
322
|
+
count = await asyncio.to_thread(self.runtime.store.prune_expired_lessons)
|
|
323
|
+
if count > 0:
|
|
324
|
+
self.logger.info("lessons_pruned", count=count)
|
|
325
|
+
print(f"\n[Synapse] Pruned {count} expired lessons.")
|
|
326
|
+
|
|
327
|
+
state = self.git.state()
|
|
328
|
+
change = self.git.classify(self._last_git_state, state)
|
|
329
|
+
self._last_git_state = state
|
|
330
|
+
|
|
331
|
+
if change.kind == "commit":
|
|
332
|
+
self.logger.info("git_commit_detected")
|
|
333
|
+
await asyncio.to_thread(self.runtime.handle_commit, state)
|
|
334
|
+
elif change.kind in ("branch", "checkout"):
|
|
335
|
+
self.logger.info("git_branch_switch_detected")
|
|
336
|
+
await asyncio.to_thread(self.runtime.handle_branch_switch, state)
|
|
337
|
+
elif change.kind == "merge":
|
|
338
|
+
self.logger.info("git_merge_detected")
|
|
339
|
+
await asyncio.to_thread(self.runtime.handle_merge, state)
|
|
340
|
+
elif change.kind == "revert":
|
|
341
|
+
self.logger.info("git_revert_detected")
|
|
342
|
+
await asyncio.to_thread(
|
|
343
|
+
self.runtime.handle_revert, self._last_git_state, state
|
|
344
|
+
)
|
|
345
|
+
elif change.kind not in ("initial", "unchanged"):
|
|
346
|
+
self.logger.info("git_change_detected", kind=change.kind)
|
|
347
|
+
await asyncio.to_thread(self.runtime.index_repository, git_state=state)
|
|
348
|
+
|
|
349
|
+
self._write_heartbeat(status="healthy")
|
|
350
|
+
except Exception as e:
|
|
351
|
+
self.logger.error("daemon_loop_error", error=str(e))
|
|
352
|
+
self._recovery_attempts += 1
|
|
353
|
+
self._write_heartbeat(status="degraded", last_error=str(e))
|
|
354
|
+
|
|
355
|
+
# Database recovery if corrupted
|
|
356
|
+
try:
|
|
357
|
+
is_wiped = await asyncio.to_thread(self.runtime.store.recover_if_corrupted)
|
|
358
|
+
if is_wiped:
|
|
359
|
+
self.logger.warning("daemon_rebuilding_corrupted_db")
|
|
360
|
+
await asyncio.to_thread(self.runtime.bootstrap, force=True)
|
|
361
|
+
except Exception as ex:
|
|
362
|
+
self.logger.error("daemon_self_healing_failed", error=str(ex))
|
|
306
363
|
|
|
307
364
|
try:
|
|
308
365
|
await asyncio.wait_for(
|
|
309
366
|
self._stop_event.wait(),
|
|
310
|
-
timeout=
|
|
367
|
+
timeout=poll_step,
|
|
311
368
|
)
|
|
312
369
|
break # Stop event was set, exit loop
|
|
313
370
|
except TimeoutError:
|
|
314
|
-
|
|
371
|
+
elapsed += poll_step
|
|
315
372
|
|
|
316
373
|
def _install_signal_handlers(self) -> None:
|
|
317
374
|
loop = asyncio.get_running_loop()
|
|
@@ -217,6 +217,52 @@ class CodeParserRegistry:
|
|
|
217
217
|
if kind in ("class", "module", "method"):
|
|
218
218
|
name = extract_text(node.child_by_field_name("name"))
|
|
219
219
|
|
|
220
|
+
elif lang_name == "c_sharp":
|
|
221
|
+
if kind in (
|
|
222
|
+
"class_declaration",
|
|
223
|
+
"method_declaration",
|
|
224
|
+
"interface_declaration",
|
|
225
|
+
"struct_declaration",
|
|
226
|
+
"enum_declaration",
|
|
227
|
+
):
|
|
228
|
+
name = extract_text(node.child_by_field_name("name"))
|
|
229
|
+
|
|
230
|
+
elif lang_name == "php":
|
|
231
|
+
if kind in (
|
|
232
|
+
"class_declaration",
|
|
233
|
+
"function_definition",
|
|
234
|
+
"method_declaration",
|
|
235
|
+
"interface_declaration",
|
|
236
|
+
"trait_declaration",
|
|
237
|
+
):
|
|
238
|
+
name = extract_text(node.child_by_field_name("name"))
|
|
239
|
+
|
|
240
|
+
elif lang_name == "kotlin":
|
|
241
|
+
if kind in (
|
|
242
|
+
"class_declaration",
|
|
243
|
+
"function_declaration",
|
|
244
|
+
"object_declaration",
|
|
245
|
+
"interface_declaration",
|
|
246
|
+
):
|
|
247
|
+
name = extract_text(node.child_by_field_name("name"))
|
|
248
|
+
|
|
249
|
+
elif lang_name == "scala":
|
|
250
|
+
if kind in (
|
|
251
|
+
"class_definition",
|
|
252
|
+
"object_definition",
|
|
253
|
+
"function_definition",
|
|
254
|
+
"trait_definition",
|
|
255
|
+
):
|
|
256
|
+
name = extract_text(node.child_by_field_name("name"))
|
|
257
|
+
|
|
258
|
+
elif lang_name == "c":
|
|
259
|
+
if kind in ("function_definition", "struct_specifier", "enum_specifier"):
|
|
260
|
+
name = extract_text(node.child_by_field_name("name"))
|
|
261
|
+
if not name:
|
|
262
|
+
declarator = node.child_by_field_name("declarator")
|
|
263
|
+
if declarator:
|
|
264
|
+
name = extract_text(declarator.child_by_field_name("declarator"))
|
|
265
|
+
|
|
220
266
|
if name:
|
|
221
267
|
# Compute hash of the node's subtree for change detection
|
|
222
268
|
ast_content = extract_text(node) or ""
|
|
@@ -123,12 +123,13 @@ class AnthropicProvider(LLMProvider):
|
|
|
123
123
|
*,
|
|
124
124
|
model: str | None = None,
|
|
125
125
|
) -> list[float]:
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
126
|
+
import structlog
|
|
127
|
+
|
|
128
|
+
structlog.get_logger().warning(
|
|
129
|
+
"anthropic_embedding_not_supported_degrading_to_fts",
|
|
130
|
+
provider="anthropic",
|
|
131
131
|
)
|
|
132
|
+
return []
|
|
132
133
|
|
|
133
134
|
def count_tokens(self, text: str) -> int:
|
|
134
135
|
import tiktoken
|
|
@@ -120,6 +120,10 @@ class OpenRouterProvider(OpenAIProvider):
|
|
|
120
120
|
*,
|
|
121
121
|
model: str | None = None,
|
|
122
122
|
) -> list[float]:
|
|
123
|
-
|
|
124
|
-
|
|
123
|
+
import structlog
|
|
124
|
+
|
|
125
|
+
structlog.get_logger().warning(
|
|
126
|
+
"openrouter_embedding_not_supported_degrading_to_fts",
|
|
127
|
+
provider="openrouter",
|
|
125
128
|
)
|
|
129
|
+
return []
|
|
@@ -122,10 +122,15 @@ class HybridRetrievalEngine:
|
|
|
122
122
|
if self.embed_provider:
|
|
123
123
|
try:
|
|
124
124
|
query_vector = self.embed_provider.embed(query)
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
125
|
+
if query_vector:
|
|
126
|
+
symbols = self.store.get_similar_symbols(query_vector, limit=50)
|
|
127
|
+
for rank, sym in enumerate(symbols):
|
|
128
|
+
sid = sym["symbol_id"]
|
|
129
|
+
semantic_candidates[sid] = {
|
|
130
|
+
**sym,
|
|
131
|
+
"reason": "semantic",
|
|
132
|
+
"semantic_rank": rank,
|
|
133
|
+
}
|
|
129
134
|
except Exception as e:
|
|
130
135
|
import structlog
|
|
131
136
|
|
|
@@ -44,6 +44,11 @@ class SynapStore:
|
|
|
44
44
|
def initialize(self) -> None:
|
|
45
45
|
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
46
46
|
with self.connect() as conn:
|
|
47
|
+
# ARCHITECTURAL DECISION (AUD-07):
|
|
48
|
+
# PRAGMA synchronous=NORMAL in WAL mode is an intentional tradeoff:
|
|
49
|
+
# It avoids fsync() on every transaction commit, providing a 5-10x throughput boost for bulk AST indexing.
|
|
50
|
+
# In the rare event of an OS power-loss crash during a write, SQLite WAL recovery or Synapse's automatic
|
|
51
|
+
# self-healing (`recover_if_corrupted()`) safely reconstructs the deterministic local index.
|
|
47
52
|
conn.execute("PRAGMA journal_mode=WAL")
|
|
48
53
|
conn.execute("PRAGMA synchronous=NORMAL")
|
|
49
54
|
conn.execute("PRAGMA foreign_keys=ON")
|
|
@@ -854,6 +859,15 @@ class SynapStore:
|
|
|
854
859
|
with self.connect() as conn:
|
|
855
860
|
conn.execute("DELETE FROM wiki_queue")
|
|
856
861
|
|
|
862
|
+
def retry_failed_wiki_queue(self) -> int:
|
|
863
|
+
now = int(datetime.now(UTC).timestamp())
|
|
864
|
+
with self.connect() as conn:
|
|
865
|
+
cursor = conn.execute(
|
|
866
|
+
"UPDATE wiki_queue SET status = 'pending', attempts = 0, updated_at = ? WHERE status = 'failed'",
|
|
867
|
+
(now,),
|
|
868
|
+
)
|
|
869
|
+
return cursor.rowcount
|
|
870
|
+
|
|
857
871
|
def get_llm_calls(self) -> list[dict[str, Any]]:
|
|
858
872
|
with self.connect() as conn:
|
|
859
873
|
rows = conn.execute("SELECT * FROM llm_calls ORDER BY created_at DESC").fetchall()
|
|
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
|
|
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
|