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.
Files changed (45) hide show
  1. {synap_git-1.1.1 → synap_git-1.2.2}/.gitignore +3 -0
  2. {synap_git-1.1.1 → synap_git-1.2.2}/PKG-INFO +1 -1
  3. {synap_git-1.1.1 → synap_git-1.2.2}/src/synap_git/__init__.py +1 -1
  4. {synap_git-1.1.1 → synap_git-1.2.2}/src/synap_git/cli/main.py +117 -70
  5. {synap_git-1.1.1 → synap_git-1.2.2}/src/synap_git/indexer/daemon.py +80 -27
  6. {synap_git-1.1.1 → synap_git-1.2.2}/src/synap_git/indexer/engine.py +17 -7
  7. {synap_git-1.1.1 → synap_git-1.2.2}/src/synap_git/storage/sqlite.py +2 -2
  8. {synap_git-1.1.1 → synap_git-1.2.2}/.synap.example/README.md +0 -0
  9. {synap_git-1.1.1 → synap_git-1.2.2}/CHANGELOG.md +0 -0
  10. {synap_git-1.1.1 → synap_git-1.2.2}/LICENSE.md +0 -0
  11. {synap_git-1.1.1 → synap_git-1.2.2}/README.md +0 -0
  12. {synap_git-1.1.1 → synap_git-1.2.2}/pyproject.toml +0 -0
  13. {synap_git-1.1.1 → synap_git-1.2.2}/src/synap_git/api/__init__.py +0 -0
  14. {synap_git-1.1.1 → synap_git-1.2.2}/src/synap_git/api/app.py +0 -0
  15. {synap_git-1.1.1 → synap_git-1.2.2}/src/synap_git/api/static/index.html +0 -0
  16. {synap_git-1.1.1 → synap_git-1.2.2}/src/synap_git/cli/__init__.py +0 -0
  17. {synap_git-1.1.1 → synap_git-1.2.2}/src/synap_git/cli/__main__.py +0 -0
  18. {synap_git-1.1.1 → synap_git-1.2.2}/src/synap_git/config.py +0 -0
  19. {synap_git-1.1.1 → synap_git-1.2.2}/src/synap_git/diagnostics/__init__.py +0 -0
  20. {synap_git-1.1.1 → synap_git-1.2.2}/src/synap_git/diagnostics/logger.py +0 -0
  21. {synap_git-1.1.1 → synap_git-1.2.2}/src/synap_git/diagnostics/tracing.py +0 -0
  22. {synap_git-1.1.1 → synap_git-1.2.2}/src/synap_git/embeddings/__init__.py +0 -0
  23. {synap_git-1.1.1 → synap_git-1.2.2}/src/synap_git/git/__init__.py +0 -0
  24. {synap_git-1.1.1 → synap_git-1.2.2}/src/synap_git/git/state.py +0 -0
  25. {synap_git-1.1.1 → synap_git-1.2.2}/src/synap_git/indexer/__init__.py +0 -0
  26. {synap_git-1.1.1 → synap_git-1.2.2}/src/synap_git/indexer/scanner.py +0 -0
  27. {synap_git-1.1.1 → synap_git-1.2.2}/src/synap_git/indexer/wiki.py +0 -0
  28. {synap_git-1.1.1 → synap_git-1.2.2}/src/synap_git/mcp/__init__.py +0 -0
  29. {synap_git-1.1.1 → synap_git-1.2.2}/src/synap_git/mcp/server.py +0 -0
  30. {synap_git-1.1.1 → synap_git-1.2.2}/src/synap_git/parser/__init__.py +0 -0
  31. {synap_git-1.1.1 → synap_git-1.2.2}/src/synap_git/parser/registry.py +0 -0
  32. {synap_git-1.1.1 → synap_git-1.2.2}/src/synap_git/provider/anthropic.py +0 -0
  33. {synap_git-1.1.1 → synap_git-1.2.2}/src/synap_git/provider/base.py +0 -0
  34. {synap_git-1.1.1 → synap_git-1.2.2}/src/synap_git/provider/factory.py +0 -0
  35. {synap_git-1.1.1 → synap_git-1.2.2}/src/synap_git/provider/gemini.py +0 -0
  36. {synap_git-1.1.1 → synap_git-1.2.2}/src/synap_git/provider/ollama.py +0 -0
  37. {synap_git-1.1.1 → synap_git-1.2.2}/src/synap_git/provider/openai.py +0 -0
  38. {synap_git-1.1.1 → synap_git-1.2.2}/src/synap_git/provider/openrouter.py +0 -0
  39. {synap_git-1.1.1 → synap_git-1.2.2}/src/synap_git/py.typed +0 -0
  40. {synap_git-1.1.1 → synap_git-1.2.2}/src/synap_git/retrieval/__init__.py +0 -0
  41. {synap_git-1.1.1 → synap_git-1.2.2}/src/synap_git/retrieval/engine.py +0 -0
  42. {synap_git-1.1.1 → synap_git-1.2.2}/src/synap_git/retrieval/memory.py +0 -0
  43. {synap_git-1.1.1 → synap_git-1.2.2}/src/synap_git/storage/__init__.py +0 -0
  44. {synap_git-1.1.1 → synap_git-1.2.2}/src/synap_git/utils/__init__.py +0 -0
  45. {synap_git-1.1.1 → synap_git-1.2.2}/src/synap_git/utils/serialization.py +0 -0
@@ -22,3 +22,6 @@ build/
22
22
  .env
23
23
  .env.*
24
24
  .env.local
25
+ .synapse/
26
+ .synapse/*-wal
27
+ .synapse/*-shm
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: synap-git
3
- Version: 1.1.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
@@ -2,4 +2,4 @@
2
2
 
3
3
  __all__ = ["__version__"]
4
4
 
5
- __version__ = "1.1.1"
5
+ __version__ = "1.2.2"
@@ -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(".synap/\n")
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
- if ".synap/" not in lines and ".synap" not in lines:
491
- if content and not content.endswith("\n"):
492
- content += "\n"
493
- content += ".synap/\n"
494
- gitignore_path.write_text(content)
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
- lines = f.readlines()
910
- for line in lines[-50:]:
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
- if current_version == latest_version and install_method != "editable":
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
- subprocess.run([sys.executable, "-m", "pip", "install", "-e", "."], check=True)
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
- subprocess.run(cmd, check=True)
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
- def _simulate_mcp_call(tool_name: str, *args: Any, **kwargs: Any) -> bool:
1449
- start = time.monotonic()
1450
- try:
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
- if tool_name == "search":
1478
- # Check trace payload exists
1479
- assert "trace" in data, "Trace payload missing in search data"
1480
-
1481
- details = f"keys: {list(data.keys())}"
1482
- table.add_row(tool_name, "[green]PASS[/green]", f"{latency:.1f}", details)
1483
- return True
1484
- except Exception as e:
1485
- latency = (time.monotonic() - start) * 1000
1486
- table.add_row(tool_name, "[red]FAIL[/red]", f"{latency:.1f}", str(e))
1487
- return False
1488
-
1489
- results = [
1490
- _simulate_mcp_call("get_status"),
1491
- _simulate_mcp_call("verify_system"),
1492
- _simulate_mcp_call("search", "User"),
1493
- _simulate_mcp_call("create_checkpoint", "Testing MCP", ["test.py"], "Next", "None"),
1494
- _simulate_mcp_call("restore_checkpoint", "latest"),
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[str, typer.Option(help="What the agent is currently doing.")] = "",
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 if it's running
110
- if self._ui_server.started:
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 asyncio.wait_for(
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.set()
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
- await asyncio.to_thread(self.runtime.wiki.ensure_wiki_page, file_path)
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
- self.logger.info("wiki_worker_completed_task", path=file_path)
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
- # Log visibly for daemon output
360
- print(
361
- f"\n[Synapse] Wiki generation permanently failed for '{file_path}' after 3 attempts."
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(".synap/\n", encoding="utf-8")
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
- if ".synap/" not in lines and ".synap" not in lines:
99
- if content and not content.endswith("\n"):
100
- content += "\n"
101
- content += ".synap/\n"
102
- gitignore_path.write_text(content, encoding="utf-8")
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
- self.store.set_wiki_status(rel_path, None, "stale")
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=NORMAL")
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=NORMAL")
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