synap-git 1.1.1__tar.gz → 1.2.2__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-1.1.1 → synap_git-1.2.2}/.gitignore +3 -0
- {synap_git-1.1.1 → synap_git-1.2.2}/PKG-INFO +1 -1
- {synap_git-1.1.1 → synap_git-1.2.2}/src/synap_git/__init__.py +1 -1
- {synap_git-1.1.1 → synap_git-1.2.2}/src/synap_git/cli/main.py +117 -70
- {synap_git-1.1.1 → synap_git-1.2.2}/src/synap_git/indexer/daemon.py +80 -27
- {synap_git-1.1.1 → synap_git-1.2.2}/src/synap_git/indexer/engine.py +17 -7
- {synap_git-1.1.1 → synap_git-1.2.2}/src/synap_git/storage/sqlite.py +2 -2
- {synap_git-1.1.1 → synap_git-1.2.2}/.synap.example/README.md +0 -0
- {synap_git-1.1.1 → synap_git-1.2.2}/CHANGELOG.md +0 -0
- {synap_git-1.1.1 → synap_git-1.2.2}/LICENSE.md +0 -0
- {synap_git-1.1.1 → synap_git-1.2.2}/README.md +0 -0
- {synap_git-1.1.1 → synap_git-1.2.2}/pyproject.toml +0 -0
- {synap_git-1.1.1 → synap_git-1.2.2}/src/synap_git/api/__init__.py +0 -0
- {synap_git-1.1.1 → synap_git-1.2.2}/src/synap_git/api/app.py +0 -0
- {synap_git-1.1.1 → synap_git-1.2.2}/src/synap_git/api/static/index.html +0 -0
- {synap_git-1.1.1 → synap_git-1.2.2}/src/synap_git/cli/__init__.py +0 -0
- {synap_git-1.1.1 → synap_git-1.2.2}/src/synap_git/cli/__main__.py +0 -0
- {synap_git-1.1.1 → synap_git-1.2.2}/src/synap_git/config.py +0 -0
- {synap_git-1.1.1 → synap_git-1.2.2}/src/synap_git/diagnostics/__init__.py +0 -0
- {synap_git-1.1.1 → synap_git-1.2.2}/src/synap_git/diagnostics/logger.py +0 -0
- {synap_git-1.1.1 → synap_git-1.2.2}/src/synap_git/diagnostics/tracing.py +0 -0
- {synap_git-1.1.1 → synap_git-1.2.2}/src/synap_git/embeddings/__init__.py +0 -0
- {synap_git-1.1.1 → synap_git-1.2.2}/src/synap_git/git/__init__.py +0 -0
- {synap_git-1.1.1 → synap_git-1.2.2}/src/synap_git/git/state.py +0 -0
- {synap_git-1.1.1 → synap_git-1.2.2}/src/synap_git/indexer/__init__.py +0 -0
- {synap_git-1.1.1 → synap_git-1.2.2}/src/synap_git/indexer/scanner.py +0 -0
- {synap_git-1.1.1 → synap_git-1.2.2}/src/synap_git/indexer/wiki.py +0 -0
- {synap_git-1.1.1 → synap_git-1.2.2}/src/synap_git/mcp/__init__.py +0 -0
- {synap_git-1.1.1 → synap_git-1.2.2}/src/synap_git/mcp/server.py +0 -0
- {synap_git-1.1.1 → synap_git-1.2.2}/src/synap_git/parser/__init__.py +0 -0
- {synap_git-1.1.1 → synap_git-1.2.2}/src/synap_git/parser/registry.py +0 -0
- {synap_git-1.1.1 → synap_git-1.2.2}/src/synap_git/provider/anthropic.py +0 -0
- {synap_git-1.1.1 → synap_git-1.2.2}/src/synap_git/provider/base.py +0 -0
- {synap_git-1.1.1 → synap_git-1.2.2}/src/synap_git/provider/factory.py +0 -0
- {synap_git-1.1.1 → synap_git-1.2.2}/src/synap_git/provider/gemini.py +0 -0
- {synap_git-1.1.1 → synap_git-1.2.2}/src/synap_git/provider/ollama.py +0 -0
- {synap_git-1.1.1 → synap_git-1.2.2}/src/synap_git/provider/openai.py +0 -0
- {synap_git-1.1.1 → synap_git-1.2.2}/src/synap_git/provider/openrouter.py +0 -0
- {synap_git-1.1.1 → synap_git-1.2.2}/src/synap_git/py.typed +0 -0
- {synap_git-1.1.1 → synap_git-1.2.2}/src/synap_git/retrieval/__init__.py +0 -0
- {synap_git-1.1.1 → synap_git-1.2.2}/src/synap_git/retrieval/engine.py +0 -0
- {synap_git-1.1.1 → synap_git-1.2.2}/src/synap_git/retrieval/memory.py +0 -0
- {synap_git-1.1.1 → synap_git-1.2.2}/src/synap_git/storage/__init__.py +0 -0
- {synap_git-1.1.1 → synap_git-1.2.2}/src/synap_git/utils/__init__.py +0 -0
- {synap_git-1.1.1 → synap_git-1.2.2}/src/synap_git/utils/serialization.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: synap-git
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.2.2
|
|
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
|
|
@@ -120,22 +120,22 @@ def _jsonable(value: Any) -> Any:
|
|
|
120
120
|
return value
|
|
121
121
|
|
|
122
122
|
|
|
123
|
-
mcp_app = typer.Typer(help="Model Context Protocol (MCP) server commands.")
|
|
123
|
+
mcp_app = typer.Typer(help="Model Context Protocol (MCP) server commands.", no_args_is_help=True)
|
|
124
124
|
app.add_typer(mcp_app, name="mcp")
|
|
125
125
|
|
|
126
|
-
memory_app = typer.Typer(help="Manage L3 Agent Memory.")
|
|
126
|
+
memory_app = typer.Typer(help="Manage L3 Agent Memory.", no_args_is_help=True)
|
|
127
127
|
app.add_typer(memory_app, name="memory")
|
|
128
128
|
|
|
129
|
-
lessons_app = typer.Typer(help="Manage Agent Lessons.")
|
|
129
|
+
lessons_app = typer.Typer(help="Manage Agent Lessons.", no_args_is_help=True)
|
|
130
130
|
app.add_typer(lessons_app, name="lessons")
|
|
131
131
|
|
|
132
|
-
checkpoint_app = typer.Typer(help="Manage Context Checkpoints.")
|
|
132
|
+
checkpoint_app = typer.Typer(help="Manage Context Checkpoints.", no_args_is_help=True)
|
|
133
133
|
app.add_typer(checkpoint_app, name="checkpoint")
|
|
134
134
|
|
|
135
|
-
wiki_app = typer.Typer(help="Manage L2 Wiki Documentation.")
|
|
135
|
+
wiki_app = typer.Typer(help="Manage L2 Wiki Documentation.", no_args_is_help=True)
|
|
136
136
|
app.add_typer(wiki_app, name="wiki")
|
|
137
137
|
|
|
138
|
-
usage_app = typer.Typer(help="View AI Usage Tracking.")
|
|
138
|
+
usage_app = typer.Typer(help="View AI Usage Tracking.", no_args_is_help=True)
|
|
139
139
|
app.add_typer(usage_app, name="usage")
|
|
140
140
|
|
|
141
141
|
|
|
@@ -481,17 +481,26 @@ ollama_url = "{ollama_url}"
|
|
|
481
481
|
|
|
482
482
|
def _auto_protect_synap(repository_path: Path) -> None:
|
|
483
483
|
gitignore_path = repository_path / ".gitignore"
|
|
484
|
+
patterns = [".synap/", ".synapse/", ".synapse/*-wal", ".synapse/*-shm"]
|
|
485
|
+
|
|
484
486
|
if not gitignore_path.exists():
|
|
485
|
-
gitignore_path.write_text(".
|
|
487
|
+
gitignore_path.write_text("\n".join(patterns) + "\n")
|
|
486
488
|
return
|
|
487
489
|
|
|
488
490
|
content = gitignore_path.read_text()
|
|
489
|
-
lines = content.splitlines()
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
491
|
+
lines = [line.strip() for line in content.splitlines()]
|
|
492
|
+
|
|
493
|
+
new_content = content
|
|
494
|
+
added = False
|
|
495
|
+
for p in patterns:
|
|
496
|
+
if p.strip() not in lines and p.strip().rstrip("/") not in lines:
|
|
497
|
+
if not new_content.endswith("\n"):
|
|
498
|
+
new_content += "\n"
|
|
499
|
+
new_content += p + "\n"
|
|
500
|
+
added = True
|
|
501
|
+
|
|
502
|
+
if added:
|
|
503
|
+
gitignore_path.write_text(new_content)
|
|
495
504
|
|
|
496
505
|
|
|
497
506
|
@app.command()
|
|
@@ -889,6 +898,7 @@ def logs(
|
|
|
889
898
|
tail: Annotated[
|
|
890
899
|
bool, typer.Option("--tail", "-t", help="Stream new log entries in real-time.")
|
|
891
900
|
] = False,
|
|
901
|
+
lines: Annotated[int, typer.Option("--lines", "-n", help="Number of last lines to show.")] = 50,
|
|
892
902
|
debug: Annotated[
|
|
893
903
|
bool, typer.Option("--debug", "-d", help="Show verbose debug and trace logs.")
|
|
894
904
|
] = False,
|
|
@@ -906,8 +916,8 @@ def logs(
|
|
|
906
916
|
try:
|
|
907
917
|
with open(log_file, encoding="utf-8") as f:
|
|
908
918
|
if not tail:
|
|
909
|
-
|
|
910
|
-
for line in
|
|
919
|
+
all_lines = f.readlines()
|
|
920
|
+
for line in all_lines[-lines:]:
|
|
911
921
|
if not debug and '"level": "debug"' in line.lower():
|
|
912
922
|
continue
|
|
913
923
|
console.print(line.strip())
|
|
@@ -960,7 +970,24 @@ def update() -> None:
|
|
|
960
970
|
|
|
961
971
|
console.print(f"Latest version on PyPI: [bold]{latest_version}[/bold]")
|
|
962
972
|
|
|
963
|
-
|
|
973
|
+
def is_newer(v_remote: str, v_local: str) -> bool:
|
|
974
|
+
try:
|
|
975
|
+
# Attempt to use packaging if available
|
|
976
|
+
from packaging.version import Version
|
|
977
|
+
|
|
978
|
+
return Version(v_remote) > Version(v_local)
|
|
979
|
+
except (ImportError, Exception):
|
|
980
|
+
# Fallback to simple semver tuple comparison
|
|
981
|
+
try:
|
|
982
|
+
|
|
983
|
+
def _to_tuple(v: str) -> tuple[int, ...]:
|
|
984
|
+
return tuple(int(x) for x in v.split(".") if x.isdigit())
|
|
985
|
+
|
|
986
|
+
return _to_tuple(v_remote) > _to_tuple(v_local)
|
|
987
|
+
except Exception:
|
|
988
|
+
return v_remote != v_local
|
|
989
|
+
|
|
990
|
+
if not is_newer(latest_version, current_version) and install_method != "editable":
|
|
964
991
|
console.print("[green]✓ Synap is already up to date.[/green]")
|
|
965
992
|
return
|
|
966
993
|
|
|
@@ -968,7 +995,11 @@ def update() -> None:
|
|
|
968
995
|
console.print("\n[yellow]Editable installation detected. Updating via git pull...[/yellow]")
|
|
969
996
|
try:
|
|
970
997
|
subprocess.run(["git", "pull", "origin", "main"], check=True)
|
|
971
|
-
|
|
998
|
+
# Try pip, then uv
|
|
999
|
+
try:
|
|
1000
|
+
subprocess.run([sys.executable, "-m", "pip", "install", "-e", "."], check=True)
|
|
1001
|
+
except Exception:
|
|
1002
|
+
subprocess.run(["uv", "pip", "install", "-e", "."], check=True)
|
|
972
1003
|
console.print(
|
|
973
1004
|
"[green]✓ Update successful (Git repository pulled and re-installed)[/green]"
|
|
974
1005
|
)
|
|
@@ -993,7 +1024,17 @@ def update() -> None:
|
|
|
993
1024
|
f"\n[yellow]Upgrading Synap to {latest_version} using: {' '.join(cmd)}...[/yellow]"
|
|
994
1025
|
)
|
|
995
1026
|
try:
|
|
996
|
-
|
|
1027
|
+
try:
|
|
1028
|
+
subprocess.run(cmd, check=True)
|
|
1029
|
+
except Exception as e:
|
|
1030
|
+
if install_method == "venv" or install_method == "pip":
|
|
1031
|
+
# Fallback to uv if pip fails/is missing
|
|
1032
|
+
console.print(
|
|
1033
|
+
"[yellow]Pip failed or missing. Attempting upgrade via uv...[/yellow]"
|
|
1034
|
+
)
|
|
1035
|
+
subprocess.run(["uv", "pip", "install", "--upgrade", "synap-git"], check=True)
|
|
1036
|
+
else:
|
|
1037
|
+
raise e
|
|
997
1038
|
console.print("[green]✓ Update successful[/green]")
|
|
998
1039
|
except Exception as e:
|
|
999
1040
|
console.print(f"[bold red]✗ Upgrade failed:[/bold red] {e}")
|
|
@@ -1445,54 +1486,62 @@ def mcp_verify(
|
|
|
1445
1486
|
table.add_column("Latency (ms)")
|
|
1446
1487
|
table.add_column("Details")
|
|
1447
1488
|
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
import uuid
|
|
1452
|
-
|
|
1453
|
-
status = facade.runtime.status()
|
|
1454
|
-
dirty = status.is_dirty
|
|
1455
|
-
warnings = ["Working tree is dirty. Index may be stale."] if dirty else []
|
|
1456
|
-
|
|
1457
|
-
method = getattr(facade, tool_name)
|
|
1458
|
-
data = method(*args, **kwargs)
|
|
1459
|
-
|
|
1460
|
-
response = {
|
|
1461
|
-
"ok": True,
|
|
1462
|
-
"data": data,
|
|
1463
|
-
"warnings": warnings,
|
|
1464
|
-
"trace_id": str(uuid.uuid4()),
|
|
1465
|
-
"dirty_tree": dirty,
|
|
1466
|
-
}
|
|
1467
|
-
|
|
1468
|
-
# Verify strict schema
|
|
1469
|
-
assert "ok" in response
|
|
1470
|
-
assert "data" in response
|
|
1471
|
-
assert "warnings" in response
|
|
1472
|
-
assert "trace_id" in response
|
|
1473
|
-
assert "dirty_tree" in response
|
|
1474
|
-
|
|
1475
|
-
latency = (time.monotonic() - start) * 1000
|
|
1489
|
+
with console.status(
|
|
1490
|
+
"[bold yellow]Verifying MCP transport and tools...[/bold yellow]"
|
|
1491
|
+
) as status:
|
|
1476
1492
|
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1493
|
+
def _simulate_mcp_call(tool_name: str, *args: Any, **kwargs: Any) -> bool:
|
|
1494
|
+
status.update(
|
|
1495
|
+
f"[bold yellow]Verifying stage: [white]{tool_name}[/white]...[/bold yellow]"
|
|
1496
|
+
)
|
|
1497
|
+
start = time.monotonic()
|
|
1498
|
+
try:
|
|
1499
|
+
import uuid
|
|
1500
|
+
|
|
1501
|
+
status_info = facade.runtime.status()
|
|
1502
|
+
dirty = status_info.is_dirty
|
|
1503
|
+
warnings = ["Working tree is dirty. Index may be stale."] if dirty else []
|
|
1504
|
+
|
|
1505
|
+
method = getattr(facade, tool_name)
|
|
1506
|
+
data = method(*args, **kwargs)
|
|
1507
|
+
|
|
1508
|
+
response = {
|
|
1509
|
+
"ok": True,
|
|
1510
|
+
"data": data,
|
|
1511
|
+
"warnings": warnings,
|
|
1512
|
+
"trace_id": str(uuid.uuid4()),
|
|
1513
|
+
"dirty_tree": dirty,
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
# Verify strict schema
|
|
1517
|
+
assert "ok" in response
|
|
1518
|
+
assert "data" in response
|
|
1519
|
+
assert "warnings" in response
|
|
1520
|
+
assert "trace_id" in response
|
|
1521
|
+
assert "dirty_tree" in response
|
|
1522
|
+
|
|
1523
|
+
latency = (time.monotonic() - start) * 1000
|
|
1524
|
+
|
|
1525
|
+
if tool_name == "search":
|
|
1526
|
+
# Check trace payload exists
|
|
1527
|
+
assert "trace" in data, "Trace payload missing in search data"
|
|
1528
|
+
|
|
1529
|
+
details = f"keys: {list(data.keys())}"
|
|
1530
|
+
table.add_row(tool_name, "[green]PASS[/green]", f"{latency:.1f}", details)
|
|
1531
|
+
time.sleep(0.1) # Visual breathing room
|
|
1532
|
+
return True
|
|
1533
|
+
except Exception as e:
|
|
1534
|
+
latency = (time.monotonic() - start) * 1000
|
|
1535
|
+
table.add_row(tool_name, "[red]FAIL[/red]", f"{latency:.1f}", str(e))
|
|
1536
|
+
return False
|
|
1537
|
+
|
|
1538
|
+
results = [
|
|
1539
|
+
_simulate_mcp_call("get_status"),
|
|
1540
|
+
_simulate_mcp_call("verify_system"),
|
|
1541
|
+
_simulate_mcp_call("search", "User"),
|
|
1542
|
+
_simulate_mcp_call("create_checkpoint", "Testing MCP", ["test.py"], "Next", "None"),
|
|
1543
|
+
_simulate_mcp_call("restore_checkpoint", "latest"),
|
|
1544
|
+
]
|
|
1496
1545
|
|
|
1497
1546
|
console.print(table)
|
|
1498
1547
|
|
|
@@ -1775,16 +1824,14 @@ def lessons_reject(
|
|
|
1775
1824
|
@checkpoint_app.command("create")
|
|
1776
1825
|
def checkpoint_create(
|
|
1777
1826
|
path: Annotated[str, typer.Argument(help="Repository path.")] = ".",
|
|
1778
|
-
doing: Annotated[
|
|
1827
|
+
doing: Annotated[
|
|
1828
|
+
str, typer.Option(help="What the agent is currently doing.")
|
|
1829
|
+
] = "Manual snapshot",
|
|
1779
1830
|
files: Annotated[str, typer.Option(help="Comma-separated list of changed files.")] = "",
|
|
1780
1831
|
next_step: Annotated[str, typer.Option(help="The next step to be taken.")] = "",
|
|
1781
1832
|
blockers: Annotated[str, typer.Option(help="Current blockers or obstacles.")] = "",
|
|
1782
1833
|
) -> None:
|
|
1783
1834
|
"""Create a new context checkpoint."""
|
|
1784
|
-
if not doing:
|
|
1785
|
-
console.print("[red]✗ The --doing option is required.[/red]")
|
|
1786
|
-
raise typer.Exit(1)
|
|
1787
|
-
|
|
1788
1835
|
runtime = SynapRuntime(_settings(path))
|
|
1789
1836
|
import uuid
|
|
1790
1837
|
|
|
@@ -106,26 +106,20 @@ class RuntimeDaemon:
|
|
|
106
106
|
if self.settings.profile == RuntimeProfile.TEST:
|
|
107
107
|
self._ui_server.force_exit = True
|
|
108
108
|
|
|
109
|
-
# Allow uvicorn to shut down gracefully
|
|
110
|
-
|
|
109
|
+
# Allow uvicorn and wiki worker to shut down gracefully
|
|
110
|
+
timeout = self.settings.shutdown_timeout_seconds
|
|
111
|
+
self.logger.info("daemon_waiting_for_tasks", timeout=timeout)
|
|
112
|
+
|
|
113
|
+
tasks = [server_task, wiki_worker_task]
|
|
114
|
+
done, pending = await asyncio.wait(
|
|
115
|
+
tasks, timeout=timeout, return_when=asyncio.ALL_COMPLETED
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
for task in pending:
|
|
119
|
+
self.logger.warning("daemon_force_cancelling_task", task=task.get_coro())
|
|
120
|
+
task.cancel()
|
|
111
121
|
try:
|
|
112
|
-
await
|
|
113
|
-
server_task, timeout=self.settings.shutdown_timeout_seconds
|
|
114
|
-
)
|
|
115
|
-
except (TimeoutError, asyncio.CancelledError):
|
|
116
|
-
self.logger.warning("daemon_uvicorn_graceful_shutdown_failed")
|
|
117
|
-
|
|
118
|
-
if not server_task.done():
|
|
119
|
-
server_task.cancel()
|
|
120
|
-
try:
|
|
121
|
-
await server_task
|
|
122
|
-
except (asyncio.CancelledError, Exception):
|
|
123
|
-
pass
|
|
124
|
-
|
|
125
|
-
if not wiki_worker_task.done():
|
|
126
|
-
wiki_worker_task.cancel()
|
|
127
|
-
try:
|
|
128
|
-
await wiki_worker_task
|
|
122
|
+
await task
|
|
129
123
|
except (asyncio.CancelledError, Exception):
|
|
130
124
|
pass
|
|
131
125
|
|
|
@@ -133,7 +127,9 @@ class RuntimeDaemon:
|
|
|
133
127
|
self.logger.info("daemon_stopped")
|
|
134
128
|
|
|
135
129
|
def stop(self) -> None:
|
|
136
|
-
self._stop_event.
|
|
130
|
+
if not self._stop_event.is_set():
|
|
131
|
+
self.logger.info("daemon_stop_requested")
|
|
132
|
+
self._stop_event.set()
|
|
137
133
|
|
|
138
134
|
def health(self) -> DaemonHealth:
|
|
139
135
|
status = self.runtime.status()
|
|
@@ -325,8 +321,23 @@ class RuntimeDaemon:
|
|
|
325
321
|
|
|
326
322
|
async def _wiki_worker_loop(self) -> None:
|
|
327
323
|
self.logger.info("wiki_worker_started")
|
|
324
|
+
from rich.console import Console
|
|
325
|
+
|
|
326
|
+
from synap_git.config import LoggingMode
|
|
327
|
+
|
|
328
|
+
console = Console()
|
|
329
|
+
|
|
328
330
|
while not self._stop_event.is_set():
|
|
329
331
|
try:
|
|
332
|
+
# Check queue depth for progress context
|
|
333
|
+
try:
|
|
334
|
+
with self.runtime.store.connect() as conn:
|
|
335
|
+
total_pending = conn.execute(
|
|
336
|
+
"SELECT COUNT(*) FROM wiki_queue WHERE status = 'pending'"
|
|
337
|
+
).fetchone()[0]
|
|
338
|
+
except Exception:
|
|
339
|
+
total_pending = 0
|
|
340
|
+
|
|
330
341
|
task = await asyncio.to_thread(self.runtime.store.dequeue_wiki)
|
|
331
342
|
if task:
|
|
332
343
|
task_id = task["task_id"]
|
|
@@ -336,17 +347,59 @@ class RuntimeDaemon:
|
|
|
336
347
|
self.logger.info("wiki_worker_processing_task", path=file_path)
|
|
337
348
|
|
|
338
349
|
if attempts > 0:
|
|
339
|
-
await asyncio.sleep(2**attempts)
|
|
350
|
+
await asyncio.sleep(min(30, 2**attempts))
|
|
340
351
|
|
|
352
|
+
start_time = time.time()
|
|
341
353
|
try:
|
|
342
|
-
|
|
354
|
+
if self.settings.logging_mode == LoggingMode.HUMAN:
|
|
355
|
+
with console.status(
|
|
356
|
+
f"[bold cyan][Wiki][/bold cyan] Generating: [white]{file_path}[/white] (0.0s) ({total_pending} remaining)"
|
|
357
|
+
) as status:
|
|
358
|
+
# Update status periodically with elapsed time
|
|
359
|
+
async def _update_status(
|
|
360
|
+
st: float = start_time,
|
|
361
|
+
fp: str = file_path,
|
|
362
|
+
tp: int = total_pending,
|
|
363
|
+
) -> None:
|
|
364
|
+
try:
|
|
365
|
+
while True:
|
|
366
|
+
await asyncio.sleep(0.1)
|
|
367
|
+
elapsed = time.time() - st
|
|
368
|
+
status.update(
|
|
369
|
+
f"[bold cyan][Wiki][/bold cyan] Generating: [white]{fp}[/white] [dim]({elapsed:.1f}s)[/dim] ({tp} remaining)"
|
|
370
|
+
)
|
|
371
|
+
except asyncio.CancelledError:
|
|
372
|
+
pass
|
|
373
|
+
|
|
374
|
+
status_task = asyncio.create_task(_update_status())
|
|
375
|
+
try:
|
|
376
|
+
await asyncio.to_thread(
|
|
377
|
+
self.runtime.wiki.ensure_wiki_page, file_path
|
|
378
|
+
)
|
|
379
|
+
finally:
|
|
380
|
+
status_task.cancel()
|
|
381
|
+
try:
|
|
382
|
+
await status_task
|
|
383
|
+
except asyncio.CancelledError:
|
|
384
|
+
pass
|
|
385
|
+
else:
|
|
386
|
+
await asyncio.to_thread(self.runtime.wiki.ensure_wiki_page, file_path)
|
|
387
|
+
|
|
343
388
|
await asyncio.to_thread(
|
|
344
389
|
self.runtime.store.update_wiki_queue_status,
|
|
345
390
|
task_id,
|
|
346
391
|
"completed",
|
|
347
392
|
attempts + 1,
|
|
348
393
|
)
|
|
349
|
-
|
|
394
|
+
elapsed = time.time() - start_time
|
|
395
|
+
self.logger.info(
|
|
396
|
+
"wiki_worker_completed_task", path=file_path, elapsed=elapsed
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
if self.settings.logging_mode == LoggingMode.HUMAN:
|
|
400
|
+
print(
|
|
401
|
+
f"[bold green]✓[/bold green] Wiki generated: [white]{file_path}[/white] [dim]({elapsed:.1f}s)[/dim]"
|
|
402
|
+
)
|
|
350
403
|
except Exception as ex:
|
|
351
404
|
self.logger.error("wiki_worker_task_failed", path=file_path, error=str(ex))
|
|
352
405
|
if attempts + 1 >= 3:
|
|
@@ -356,10 +409,10 @@ class RuntimeDaemon:
|
|
|
356
409
|
"failed",
|
|
357
410
|
attempts + 1,
|
|
358
411
|
)
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
412
|
+
if self.settings.logging_mode == LoggingMode.HUMAN:
|
|
413
|
+
print(
|
|
414
|
+
f"[bold red]✗[/bold red] Wiki failed: [white]{file_path}[/white] [dim]({str(ex)})[/dim]"
|
|
415
|
+
)
|
|
363
416
|
else:
|
|
364
417
|
await asyncio.to_thread(
|
|
365
418
|
self.runtime.store.update_wiki_queue_status,
|
|
@@ -88,18 +88,26 @@ class SynapRuntime:
|
|
|
88
88
|
|
|
89
89
|
def _auto_protect_synap(self) -> None:
|
|
90
90
|
gitignore_path = self.settings.repository_path / ".gitignore"
|
|
91
|
+
patterns = [".synap/", ".synapse/", ".synapse/*-wal", ".synapse/*-shm"]
|
|
91
92
|
try:
|
|
92
93
|
if not gitignore_path.exists():
|
|
93
|
-
gitignore_path.write_text(".
|
|
94
|
+
gitignore_path.write_text("\n".join(patterns) + "\n", encoding="utf-8")
|
|
94
95
|
return
|
|
95
96
|
|
|
96
97
|
content = gitignore_path.read_text(encoding="utf-8")
|
|
97
98
|
lines = [line.strip() for line in content.splitlines()]
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
99
|
+
|
|
100
|
+
new_content = content
|
|
101
|
+
added = False
|
|
102
|
+
for p in patterns:
|
|
103
|
+
if p.strip() not in lines and p.strip().rstrip("/") not in lines:
|
|
104
|
+
if not new_content.endswith("\n"):
|
|
105
|
+
new_content += "\n"
|
|
106
|
+
new_content += p + "\n"
|
|
107
|
+
added = True
|
|
108
|
+
|
|
109
|
+
if added:
|
|
110
|
+
gitignore_path.write_text(new_content, encoding="utf-8")
|
|
103
111
|
except Exception as e:
|
|
104
112
|
self.logger.warning("failed_to_auto_protect_synap", error=str(e))
|
|
105
113
|
|
|
@@ -472,7 +480,9 @@ class SynapRuntime:
|
|
|
472
480
|
if file_row:
|
|
473
481
|
file_id = file_row["file_id"]
|
|
474
482
|
conn.execute("DELETE FROM files WHERE file_id = ?", (file_id,))
|
|
475
|
-
|
|
483
|
+
|
|
484
|
+
for rel_path in deleted:
|
|
485
|
+
self.store.set_wiki_status(rel_path, None, "stale")
|
|
476
486
|
|
|
477
487
|
# Get blob OIDs for changed files
|
|
478
488
|
git_oids = {}
|
|
@@ -31,7 +31,7 @@ class SynapStore:
|
|
|
31
31
|
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
32
32
|
with self.connect() as conn:
|
|
33
33
|
conn.execute("PRAGMA journal_mode=WAL")
|
|
34
|
-
conn.execute("PRAGMA synchronous=
|
|
34
|
+
conn.execute("PRAGMA synchronous=FULL")
|
|
35
35
|
conn.execute("PRAGMA foreign_keys=ON")
|
|
36
36
|
|
|
37
37
|
# Get current user version
|
|
@@ -231,7 +231,7 @@ class SynapStore:
|
|
|
231
231
|
conn.row_factory = sqlite3.Row
|
|
232
232
|
try:
|
|
233
233
|
conn.execute("PRAGMA foreign_keys=ON")
|
|
234
|
-
conn.execute("PRAGMA synchronous=
|
|
234
|
+
conn.execute("PRAGMA synchronous=FULL")
|
|
235
235
|
yield conn
|
|
236
236
|
conn.commit()
|
|
237
237
|
except Exception:
|
|
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
|