synap-git 2.1.2__tar.gz → 2.1.4__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.2 → synap_git-2.1.4}/CHANGELOG.md +11 -0
- {synap_git-2.1.2 → synap_git-2.1.4}/PKG-INFO +1 -1
- {synap_git-2.1.2 → synap_git-2.1.4}/src/synap_git/__init__.py +1 -1
- {synap_git-2.1.2 → synap_git-2.1.4}/src/synap_git/cli/main.py +46 -26
- {synap_git-2.1.2 → synap_git-2.1.4}/src/synap_git/indexer/daemon.py +13 -6
- {synap_git-2.1.2 → synap_git-2.1.4}/src/synap_git/indexer/engine.py +65 -43
- {synap_git-2.1.2 → synap_git-2.1.4}/.gitignore +0 -0
- {synap_git-2.1.2 → synap_git-2.1.4}/.synap.example/README.md +0 -0
- {synap_git-2.1.2 → synap_git-2.1.4}/LICENSE.md +0 -0
- {synap_git-2.1.2 → synap_git-2.1.4}/README.md +0 -0
- {synap_git-2.1.2 → synap_git-2.1.4}/pyproject.toml +0 -0
- {synap_git-2.1.2 → synap_git-2.1.4}/src/synap_git/api/__init__.py +0 -0
- {synap_git-2.1.2 → synap_git-2.1.4}/src/synap_git/api/app.py +0 -0
- {synap_git-2.1.2 → synap_git-2.1.4}/src/synap_git/api/static/index.html +0 -0
- {synap_git-2.1.2 → synap_git-2.1.4}/src/synap_git/cli/__init__.py +0 -0
- {synap_git-2.1.2 → synap_git-2.1.4}/src/synap_git/cli/__main__.py +0 -0
- {synap_git-2.1.2 → synap_git-2.1.4}/src/synap_git/config.py +0 -0
- {synap_git-2.1.2 → synap_git-2.1.4}/src/synap_git/diagnostics/__init__.py +0 -0
- {synap_git-2.1.2 → synap_git-2.1.4}/src/synap_git/diagnostics/logger.py +0 -0
- {synap_git-2.1.2 → synap_git-2.1.4}/src/synap_git/diagnostics/tracing.py +0 -0
- {synap_git-2.1.2 → synap_git-2.1.4}/src/synap_git/embeddings/__init__.py +0 -0
- {synap_git-2.1.2 → synap_git-2.1.4}/src/synap_git/git/__init__.py +0 -0
- {synap_git-2.1.2 → synap_git-2.1.4}/src/synap_git/git/state.py +0 -0
- {synap_git-2.1.2 → synap_git-2.1.4}/src/synap_git/indexer/__init__.py +0 -0
- {synap_git-2.1.2 → synap_git-2.1.4}/src/synap_git/indexer/scanner.py +0 -0
- {synap_git-2.1.2 → synap_git-2.1.4}/src/synap_git/indexer/wiki.py +0 -0
- {synap_git-2.1.2 → synap_git-2.1.4}/src/synap_git/mcp/__init__.py +0 -0
- {synap_git-2.1.2 → synap_git-2.1.4}/src/synap_git/mcp/server.py +0 -0
- {synap_git-2.1.2 → synap_git-2.1.4}/src/synap_git/parser/__init__.py +0 -0
- {synap_git-2.1.2 → synap_git-2.1.4}/src/synap_git/parser/registry.py +0 -0
- {synap_git-2.1.2 → synap_git-2.1.4}/src/synap_git/provider/anthropic.py +0 -0
- {synap_git-2.1.2 → synap_git-2.1.4}/src/synap_git/provider/base.py +0 -0
- {synap_git-2.1.2 → synap_git-2.1.4}/src/synap_git/provider/factory.py +0 -0
- {synap_git-2.1.2 → synap_git-2.1.4}/src/synap_git/provider/gemini.py +0 -0
- {synap_git-2.1.2 → synap_git-2.1.4}/src/synap_git/provider/ollama.py +0 -0
- {synap_git-2.1.2 → synap_git-2.1.4}/src/synap_git/provider/openai.py +0 -0
- {synap_git-2.1.2 → synap_git-2.1.4}/src/synap_git/provider/openrouter.py +0 -0
- {synap_git-2.1.2 → synap_git-2.1.4}/src/synap_git/py.typed +0 -0
- {synap_git-2.1.2 → synap_git-2.1.4}/src/synap_git/retrieval/__init__.py +0 -0
- {synap_git-2.1.2 → synap_git-2.1.4}/src/synap_git/retrieval/engine.py +0 -0
- {synap_git-2.1.2 → synap_git-2.1.4}/src/synap_git/retrieval/memory.py +0 -0
- {synap_git-2.1.2 → synap_git-2.1.4}/src/synap_git/storage/__init__.py +0 -0
- {synap_git-2.1.2 → synap_git-2.1.4}/src/synap_git/storage/sqlite.py +0 -0
- {synap_git-2.1.2 → synap_git-2.1.4}/src/synap_git/utils/__init__.py +0 -0
- {synap_git-2.1.2 → synap_git-2.1.4}/src/synap_git/utils/serialization.py +0 -0
|
@@ -5,6 +5,17 @@ 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.4] - 2026-06-11
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- **Graceful Shutdown Hang**: Fixed an edge case where `synap stop` would time out and forcefully terminate due to `concurrent.futures` threads preventing immediate python process exit when handling embeddings. The CLI now safely flushes databases and exits instantly.
|
|
12
|
+
|
|
13
|
+
## [2.1.3] - 2026-06-11
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
- **CLI Interactivity Enhancement**: Wrapped all long-running `synap` CLI commands (`init`, `start`, `stop`, `rollback`, `repair`) with interactive rich status spinners to ensure users never stare at an empty screen.
|
|
17
|
+
- **Incremental Indexing Progress**: Added a rich progress status to the `_incremental_index` background loop to provide visibility into parsing tasks during cache hits.
|
|
18
|
+
|
|
8
19
|
## [2.1.2] - 2026-06-11
|
|
9
20
|
|
|
10
21
|
### Fixed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: synap-git
|
|
3
|
-
Version: 2.1.
|
|
3
|
+
Version: 2.1.4
|
|
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
|
|
@@ -524,7 +524,13 @@ def init(
|
|
|
524
524
|
_auto_protect_synap(settings.repository_path)
|
|
525
525
|
|
|
526
526
|
runtime = SynapRuntime(settings)
|
|
527
|
-
|
|
527
|
+
if not quiet and not json_output:
|
|
528
|
+
with console.status(
|
|
529
|
+
"[yellow]Initializing Synap runtime and scanning repository...[/yellow]", spinner="dots"
|
|
530
|
+
):
|
|
531
|
+
commit = runtime.bootstrap(force=force)
|
|
532
|
+
else:
|
|
533
|
+
commit = runtime.bootstrap(force=force)
|
|
528
534
|
|
|
529
535
|
if json_output:
|
|
530
536
|
_emit({"active_commit": commit, "state": "initialized"}, json_output=True)
|
|
@@ -670,20 +676,22 @@ def start(
|
|
|
670
676
|
console.print(f"[bold red]✗ Failed to start daemon:[/bold red] {e}")
|
|
671
677
|
raise typer.Exit(1)
|
|
672
678
|
|
|
673
|
-
console.print("[yellow]Starting Synap daemon...[/yellow]")
|
|
674
679
|
success = False
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
680
|
+
with console.status(
|
|
681
|
+
"[yellow]Starting Synap daemon and awaiting health check...[/yellow]", spinner="dots"
|
|
682
|
+
):
|
|
683
|
+
for _ in range(15): # wait up to 3 seconds
|
|
684
|
+
time.sleep(0.2)
|
|
685
|
+
if pid_file.exists():
|
|
686
|
+
hb = _read_daemon_heartbeat(abs_path)
|
|
687
|
+
if hb and hb.get("status") in ("healthy", "degraded"):
|
|
688
|
+
success = True
|
|
689
|
+
port = hb.get("port", 9876)
|
|
690
|
+
pid = int(hb.get("pid", 0))
|
|
691
|
+
console.print(f"[green]✓ Synap daemon started (PID {pid})[/green]")
|
|
692
|
+
console.print("[green]✓ Runtime healthy[/green]")
|
|
693
|
+
console.print(f"[green]✓ UI available at http://127.0.0.1:{port}[/green]")
|
|
694
|
+
break
|
|
687
695
|
|
|
688
696
|
if not success:
|
|
689
697
|
console.print(
|
|
@@ -710,6 +718,9 @@ def daemon_run(
|
|
|
710
718
|
daemon = RuntimeDaemon(settings)
|
|
711
719
|
asyncio.run(daemon.start())
|
|
712
720
|
|
|
721
|
+
# Forceful process exit to prevent atexit thread-join blocking
|
|
722
|
+
os._exit(0)
|
|
723
|
+
|
|
713
724
|
|
|
714
725
|
@app.command()
|
|
715
726
|
def stop(
|
|
@@ -766,11 +777,15 @@ def stop(
|
|
|
766
777
|
|
|
767
778
|
# Wait for graceful shutdown (up to 10 seconds)
|
|
768
779
|
success = False
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
780
|
+
with console.status(
|
|
781
|
+
f"[yellow]Waiting for daemon (PID {pid}) to shut down gracefully...[/yellow]",
|
|
782
|
+
spinner="dots",
|
|
783
|
+
):
|
|
784
|
+
for _ in range(50):
|
|
785
|
+
time.sleep(0.2)
|
|
786
|
+
if not _is_process_running(pid):
|
|
787
|
+
success = True
|
|
788
|
+
break
|
|
774
789
|
|
|
775
790
|
# If graceful failed, force kill
|
|
776
791
|
if not success:
|
|
@@ -1234,7 +1249,10 @@ def rollback(
|
|
|
1234
1249
|
|
|
1235
1250
|
structlog.get_logger().error("suppressed_error_caught", error=str(e), exc_info=True)
|
|
1236
1251
|
|
|
1237
|
-
|
|
1252
|
+
with console.status(
|
|
1253
|
+
f"[yellow]Reindexing repository to match {selected_commit}...[/yellow]", spinner="dots"
|
|
1254
|
+
):
|
|
1255
|
+
runtime.bootstrap(force=True)
|
|
1238
1256
|
console.print(f"[green]✓ Rolled back successfully to {selected_commit}[/green]")
|
|
1239
1257
|
|
|
1240
1258
|
|
|
@@ -1294,15 +1312,17 @@ def repair(
|
|
|
1294
1312
|
|
|
1295
1313
|
runtime.initialize_storage()
|
|
1296
1314
|
console.print("[2/3] Reindexing all symbols")
|
|
1297
|
-
|
|
1315
|
+
with console.status("[yellow]Reindexing repository...[/yellow]", spinner="dots"):
|
|
1316
|
+
runtime.bootstrap(force=True)
|
|
1298
1317
|
|
|
1299
1318
|
console.print("[3/3] Regenerating wiki from last known state")
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1319
|
+
with console.status("[yellow]Regenerating wiki documentation...[/yellow]", spinner="dots"):
|
|
1320
|
+
try:
|
|
1321
|
+
runtime.wiki.generate_project_wiki()
|
|
1322
|
+
except Exception as e:
|
|
1323
|
+
import structlog
|
|
1304
1324
|
|
|
1305
|
-
|
|
1325
|
+
structlog.get_logger().error("suppressed_error_caught", error=str(e), exc_info=True)
|
|
1306
1326
|
|
|
1307
1327
|
status_info = runtime.status()
|
|
1308
1328
|
console.print(f"\n[green]✓ Recovery complete. {status_info.files} files restored.[/green]")
|
|
@@ -100,14 +100,10 @@ class RuntimeDaemon:
|
|
|
100
100
|
self._running = False
|
|
101
101
|
self.logger.info("daemon_shutting_down")
|
|
102
102
|
self._ui_server.should_exit = True
|
|
103
|
-
|
|
104
|
-
from synap_git.config import RuntimeProfile
|
|
105
|
-
|
|
106
|
-
if self.settings.profile == RuntimeProfile.TEST:
|
|
107
|
-
self._ui_server.force_exit = True
|
|
103
|
+
self._ui_server.force_exit = True # Always force exit since daemon is terminating
|
|
108
104
|
|
|
109
105
|
# Allow uvicorn and wiki worker to shut down gracefully
|
|
110
|
-
timeout =
|
|
106
|
+
timeout = 0.5
|
|
111
107
|
self.logger.info("daemon_waiting_for_tasks", timeout=timeout)
|
|
112
108
|
|
|
113
109
|
tasks = [server_task, wiki_worker_task]
|
|
@@ -132,6 +128,17 @@ class RuntimeDaemon:
|
|
|
132
128
|
self.logger.info("daemon_stop_requested")
|
|
133
129
|
self._stop_event.set()
|
|
134
130
|
|
|
131
|
+
# Force immediate cleanup and exit to avoid thread-join blocking
|
|
132
|
+
try:
|
|
133
|
+
self.runtime.shutdown()
|
|
134
|
+
self._delete_heartbeat()
|
|
135
|
+
except Exception:
|
|
136
|
+
pass
|
|
137
|
+
|
|
138
|
+
import os
|
|
139
|
+
|
|
140
|
+
os._exit(0)
|
|
141
|
+
|
|
135
142
|
def health(self) -> DaemonHealth:
|
|
136
143
|
status = self.runtime.status()
|
|
137
144
|
return DaemonHealth(
|
|
@@ -601,55 +601,77 @@ class SynapRuntime:
|
|
|
601
601
|
# Process additions and modifications
|
|
602
602
|
registry = CodeParserRegistry()
|
|
603
603
|
parsed_results = []
|
|
604
|
-
for rel_path in filtered_added_or_modified:
|
|
605
|
-
self.logger.info("reindexing_file", path=rel_path)
|
|
606
|
-
p = self.settings.repository_path / rel_path
|
|
607
604
|
|
|
608
|
-
|
|
609
|
-
content = p.read_text(encoding="utf-8", errors="replace")
|
|
610
|
-
except Exception as e:
|
|
611
|
-
self.logger.warning("failed_to_read_file", path=rel_path, error=str(e))
|
|
612
|
-
continue
|
|
605
|
+
import contextlib
|
|
613
606
|
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
content_hash = hashlib.sha256(content.encode("utf-8")).hexdigest()
|
|
617
|
-
file_id_hash = hashlib.sha256((rel_path + content_hash).encode("utf-8")).hexdigest()
|
|
618
|
-
|
|
619
|
-
parse_result = registry.parse(p, relative_path=rel_path, text=content)
|
|
620
|
-
symbols_list = [
|
|
621
|
-
{
|
|
622
|
-
"symbol_id": sym.stable_id,
|
|
623
|
-
"name": sym.name,
|
|
624
|
-
"kind": sym.kind,
|
|
625
|
-
"start_line": sym.start_line,
|
|
626
|
-
"end_line": sym.end_line,
|
|
627
|
-
"ast_hash": sym.ast_hash,
|
|
628
|
-
"metadata": sym.metadata,
|
|
629
|
-
}
|
|
630
|
-
for sym in parse_result.symbols
|
|
631
|
-
]
|
|
632
|
-
|
|
633
|
-
self.store.upsert_file_and_symbols(
|
|
634
|
-
file_id=file_id_hash,
|
|
635
|
-
path=rel_path,
|
|
636
|
-
git_oid=git_oids.get(rel_path, ""),
|
|
637
|
-
content_hash=content_hash,
|
|
638
|
-
language=parse_result.language,
|
|
639
|
-
symbols=symbols_list,
|
|
640
|
-
)
|
|
641
|
-
self._schedule_embedding(file_id_hash, symbols_list, content_hash)
|
|
607
|
+
from rich.console import Console
|
|
642
608
|
|
|
643
|
-
|
|
644
|
-
|
|
609
|
+
console = Console(stderr=True)
|
|
610
|
+
show_progress = (
|
|
611
|
+
self.settings.logging_mode.name == "HUMAN" and len(filtered_added_or_modified) > 0
|
|
612
|
+
)
|
|
645
613
|
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
614
|
+
with (
|
|
615
|
+
console.status(
|
|
616
|
+
"[yellow]Parsing incrementally changed files...[/yellow]", spinner="dots"
|
|
617
|
+
)
|
|
618
|
+
if show_progress
|
|
619
|
+
else contextlib.nullcontext()
|
|
620
|
+
) as status_ctx:
|
|
621
|
+
for idx, rel_path in enumerate(filtered_added_or_modified, 1):
|
|
622
|
+
if show_progress and hasattr(status_ctx, "update"):
|
|
623
|
+
status_ctx.update( # type: ignore[union-attr]
|
|
624
|
+
f"[yellow]Parsing incrementally changed files ([white]{idx}/{len(filtered_added_or_modified)}[/white])...[/yellow]"
|
|
625
|
+
)
|
|
626
|
+
|
|
627
|
+
self.logger.info("reindexing_file", path=rel_path)
|
|
628
|
+
p = self.settings.repository_path / rel_path
|
|
629
|
+
|
|
630
|
+
try:
|
|
631
|
+
content = p.read_text(encoding="utf-8", errors="replace")
|
|
632
|
+
except Exception as e:
|
|
633
|
+
self.logger.warning("failed_to_read_file", path=rel_path, error=str(e))
|
|
634
|
+
continue
|
|
635
|
+
|
|
636
|
+
import hashlib
|
|
637
|
+
|
|
638
|
+
content_hash = hashlib.sha256(content.encode("utf-8")).hexdigest()
|
|
639
|
+
file_id_hash = hashlib.sha256((rel_path + content_hash).encode("utf-8")).hexdigest()
|
|
640
|
+
|
|
641
|
+
parse_result = registry.parse(p, relative_path=rel_path, text=content)
|
|
642
|
+
symbols_list = [
|
|
643
|
+
{
|
|
644
|
+
"symbol_id": sym.stable_id,
|
|
645
|
+
"name": sym.name,
|
|
646
|
+
"kind": sym.kind,
|
|
647
|
+
"start_line": sym.start_line,
|
|
648
|
+
"end_line": sym.end_line,
|
|
649
|
+
"ast_hash": sym.ast_hash,
|
|
650
|
+
"metadata": sym.metadata,
|
|
651
|
+
}
|
|
652
|
+
for sym in parse_result.symbols
|
|
653
|
+
]
|
|
654
|
+
|
|
655
|
+
self.store.upsert_file_and_symbols(
|
|
656
|
+
file_id=file_id_hash,
|
|
657
|
+
path=rel_path,
|
|
658
|
+
git_oid=git_oids.get(rel_path, ""),
|
|
659
|
+
content_hash=content_hash,
|
|
660
|
+
language=parse_result.language,
|
|
661
|
+
symbols=symbols_list,
|
|
650
662
|
)
|
|
663
|
+
self._schedule_embedding(file_id_hash, symbols_list, content_hash)
|
|
664
|
+
|
|
665
|
+
self.store.set_wiki_status(rel_path, None, "stale")
|
|
666
|
+
self.store.enqueue_wiki(rel_path)
|
|
667
|
+
|
|
668
|
+
with self.store.connect() as conn:
|
|
669
|
+
conn.execute(
|
|
670
|
+
"DELETE FROM edges WHERE source_symbol IN (SELECT symbol_id FROM symbols WHERE file_id = ?)",
|
|
671
|
+
(file_id_hash,),
|
|
672
|
+
)
|
|
651
673
|
|
|
652
|
-
|
|
674
|
+
parsed_results.append((file_id_hash, rel_path, list(parse_result.imports)))
|
|
653
675
|
|
|
654
676
|
# Re-resolve edges for changed files
|
|
655
677
|
if parsed_results:
|
|
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
|