superlocalmemory 3.8.3 → 3.8.6

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 (125) hide show
  1. package/CHANGELOG.md +76 -0
  2. package/README.md +3 -2
  3. package/package.json +1 -1
  4. package/plugin/.claude-plugin/plugin.json +1 -1
  5. package/plugin/CLAUDE.md +3 -3
  6. package/plugin/agents/slm-governance-advisor.md +1 -1
  7. package/plugin/agents/slm-loop-runner.md +1 -1
  8. package/plugin/agents/slm-memory-advisor.md +1 -1
  9. package/plugin/agents/slm-optimize-advisor.md +1 -1
  10. package/plugin/requirements.txt +1 -1
  11. package/plugin/skills/slm-cache/SKILL.md +1 -1
  12. package/plugin/skills/slm-compress/SKILL.md +1 -1
  13. package/plugin/skills/slm-governance/SKILL.md +1 -1
  14. package/plugin/skills/slm-graph/SKILL.md +1 -1
  15. package/plugin/skills/slm-loop/SKILL.md +1 -1
  16. package/plugin/skills/slm-mesh/SKILL.md +1 -1
  17. package/plugin/skills/slm-profile/SKILL.md +1 -1
  18. package/plugin/skills/slm-recall/SKILL.md +1 -1
  19. package/plugin/skills/slm-remember/SKILL.md +1 -1
  20. package/plugin/skills/slm-scope/SKILL.md +1 -1
  21. package/plugin/skills/slm-session/SKILL.md +1 -1
  22. package/plugin/skills/slm-status/SKILL.md +1 -1
  23. package/plugin-src/rules/AGENTS.md +1 -1
  24. package/plugin-src/skills/slm-cache/SKILL.md +1 -1
  25. package/plugin-src/skills/slm-compress/SKILL.md +1 -1
  26. package/plugin-src/skills/slm-graph/SKILL.md +1 -1
  27. package/plugin-src/skills/slm-recall/SKILL.md +1 -1
  28. package/plugin-src/skills/slm-remember/SKILL.md +1 -1
  29. package/plugin-src/skills/slm-session/SKILL.md +1 -1
  30. package/plugin-src/skills/slm-status/SKILL.md +1 -1
  31. package/pyproject.toml +9 -4
  32. package/src/superlocalmemory/__init__.py +1 -1
  33. package/src/superlocalmemory/access/rbac.py +68 -76
  34. package/src/superlocalmemory/cli/commands.py +158 -404
  35. package/src/superlocalmemory/cli/ingest_cmd.py +11 -1
  36. package/src/superlocalmemory/cli/main.py +30 -0
  37. package/src/superlocalmemory/cli/pending_store.py +39 -14
  38. package/src/superlocalmemory/core/backend_orchestrator.py +93 -0
  39. package/src/superlocalmemory/core/component_registry.py +4 -2
  40. package/src/superlocalmemory/core/config.py +78 -0
  41. package/src/superlocalmemory/core/consolidation_engine.py +79 -73
  42. package/src/superlocalmemory/core/embeddings.py +33 -6
  43. package/src/superlocalmemory/core/engine.py +186 -60
  44. package/src/superlocalmemory/core/engine_ingestion.py +150 -63
  45. package/src/superlocalmemory/core/fact_consolidator.py +148 -30
  46. package/src/superlocalmemory/core/graph_pruner.py +436 -39
  47. package/src/superlocalmemory/core/ingestion_command.py +273 -32
  48. package/src/superlocalmemory/core/maintenance_scheduler.py +61 -1
  49. package/src/superlocalmemory/core/mutations.py +32 -10
  50. package/src/superlocalmemory/core/recall_pipeline.py +111 -74
  51. package/src/superlocalmemory/core/registry.py +5 -1
  52. package/src/superlocalmemory/core/remember_admission.py +152 -0
  53. package/src/superlocalmemory/core/remember_runtime.py +712 -0
  54. package/src/superlocalmemory/core/remote_mode.py +3 -1
  55. package/src/superlocalmemory/core/scale_engine.py +41 -18
  56. package/src/superlocalmemory/core/store_pipeline.py +18 -4
  57. package/src/superlocalmemory/encoding/entity_resolver.py +18 -11
  58. package/src/superlocalmemory/graph/cozo_backend.py +5 -5
  59. package/src/superlocalmemory/hooks/_outcome_common.py +9 -2
  60. package/src/superlocalmemory/hooks/adapter_base.py +58 -44
  61. package/src/superlocalmemory/hooks/ide_connector.py +26 -8
  62. package/src/superlocalmemory/hooks/portable_kit.py +105 -9
  63. package/src/superlocalmemory/hooks/prewarm_auth.py +21 -2
  64. package/src/superlocalmemory/infra/auth_middleware.py +3 -1
  65. package/src/superlocalmemory/infra/cloud_backup.py +26 -27
  66. package/src/superlocalmemory/infra/event_bus.py +250 -88
  67. package/src/superlocalmemory/learning/bandit.py +50 -1
  68. package/src/superlocalmemory/learning/consolidation_cycle.py +33 -16
  69. package/src/superlocalmemory/learning/entity_compiler.py +148 -132
  70. package/src/superlocalmemory/learning/memory_merge.py +97 -82
  71. package/src/superlocalmemory/learning/reward_archive.py +98 -90
  72. package/src/superlocalmemory/learning/reward_boost.py +40 -30
  73. package/src/superlocalmemory/learning/source_quality.py +38 -35
  74. package/src/superlocalmemory/mcp/_daemon_proxy.py +38 -15
  75. package/src/superlocalmemory/mcp/http_transport.py +335 -3
  76. package/src/superlocalmemory/mcp/tools_active.py +4 -41
  77. package/src/superlocalmemory/mcp/tools_core.py +26 -87
  78. package/src/superlocalmemory/mcp/tools_evolution.py +5 -10
  79. package/src/superlocalmemory/optimize/proxy/capture.py +196 -8
  80. package/src/superlocalmemory/retrieval/engine.py +15 -4
  81. package/src/superlocalmemory/retrieval/entity_channel.py +25 -1
  82. package/src/superlocalmemory/retrieval/reranker.py +130 -22
  83. package/src/superlocalmemory/retrieval/spreading_activation.py +20 -12
  84. package/src/superlocalmemory/retrieval/vector_store.py +84 -69
  85. package/src/superlocalmemory/server/loopback.py +85 -0
  86. package/src/superlocalmemory/server/origin.py +9 -4
  87. package/src/superlocalmemory/server/profile_runtime.py +14 -0
  88. package/src/superlocalmemory/server/routes/abstraction.py +2 -4
  89. package/src/superlocalmemory/server/routes/agents.py +3 -5
  90. package/src/superlocalmemory/server/routes/backup.py +6 -2
  91. package/src/superlocalmemory/server/routes/behavioral.py +11 -25
  92. package/src/superlocalmemory/server/routes/brain.py +6 -9
  93. package/src/superlocalmemory/server/routes/compliance.py +20 -23
  94. package/src/superlocalmemory/server/routes/config_api.py +83 -0
  95. package/src/superlocalmemory/server/routes/entity.py +3 -7
  96. package/src/superlocalmemory/server/routes/evolution.py +3 -5
  97. package/src/superlocalmemory/server/routes/helpers.py +57 -25
  98. package/src/superlocalmemory/server/routes/insights.py +2 -4
  99. package/src/superlocalmemory/server/routes/learning.py +2 -5
  100. package/src/superlocalmemory/server/routes/lifecycle.py +2 -4
  101. package/src/superlocalmemory/server/routes/memories.py +119 -98
  102. package/src/superlocalmemory/server/routes/mesh.py +7 -2
  103. package/src/superlocalmemory/server/routes/profiles.py +20 -21
  104. package/src/superlocalmemory/server/routes/rbac.py +0 -1
  105. package/src/superlocalmemory/server/routes/tiers.py +28 -35
  106. package/src/superlocalmemory/server/routes/timeline.py +2 -4
  107. package/src/superlocalmemory/server/routes/v3_api.py +85 -93
  108. package/src/superlocalmemory/server/unified_daemon.py +400 -140
  109. package/src/superlocalmemory/server/write_identity.py +22 -4
  110. package/src/superlocalmemory/storage/admission_codec.py +119 -0
  111. package/src/superlocalmemory/storage/admission_journal.py +728 -0
  112. package/src/superlocalmemory/storage/database.py +168 -19
  113. package/src/superlocalmemory/storage/deferred_writes.py +209 -0
  114. package/src/superlocalmemory/storage/embedding_migrator.py +19 -0
  115. package/src/superlocalmemory/storage/memory_write.py +115 -0
  116. package/src/superlocalmemory/storage/migration_runner.py +44 -0
  117. package/src/superlocalmemory/storage/migrations/M028_fact_entity_associations.py +113 -78
  118. package/src/superlocalmemory/storage/migrations/M031_dead_letter_operations.py +80 -0
  119. package/src/superlocalmemory/storage/migrations/M032_write_coordinator_admission.py +188 -0
  120. package/src/superlocalmemory/storage/read_connection.py +115 -0
  121. package/src/superlocalmemory/storage/write_coordinator.py +756 -0
  122. package/src/superlocalmemory/storage/write_lock.py +88 -0
  123. package/src/superlocalmemory/ui/index.html +1 -1
  124. package/src/superlocalmemory/ui/js/auto-settings.js +14 -1
  125. package/src/superlocalmemory/ui/js/od-settings.js +9 -3
@@ -11,7 +11,7 @@ import logging
11
11
  import os
12
12
  from fastapi import APIRouter, HTTPException, Request
13
13
  from fastapi.responses import JSONResponse
14
- from superlocalmemory.server.routes.helpers import SLM_VERSION
14
+ from superlocalmemory.server.routes.helpers import SLM_VERSION, get_read_connection
15
15
  from superlocalmemory.server.route_mutations import authorize_route_mutation
16
16
 
17
17
  logger = logging.getLogger(__name__)
@@ -101,13 +101,12 @@ async def dashboard(request: Request):
101
101
  active_profile = get_profile_runtime(request.app.state).snapshot.profile_id
102
102
 
103
103
  # Read stats directly from SQLite (dashboard doesn't load engine)
104
- import sqlite3
105
104
  memory_count = 0
106
105
  fact_count = 0
107
106
  db_path = config.base_dir / "memory.db"
108
107
  if db_path.exists():
109
108
  try:
110
- conn = sqlite3.connect(str(db_path))
109
+ conn = get_read_connection(db_path)
111
110
  cursor = conn.cursor()
112
111
  try:
113
112
  cursor.execute(
@@ -358,7 +357,10 @@ async def get_embedding_config(request: Request):
358
357
  """Return current embedding configuration."""
359
358
  try:
360
359
  from superlocalmemory.core.config import SLMConfig
361
- config = SLMConfig.load()
360
+ # The daemon may already be running a freshly hot-swapped config while
361
+ # a profile update is still being persisted. The dashboard must show
362
+ # that live truth, never silently replace it with disk defaults.
363
+ config = getattr(request.app.state, "config", None) or SLMConfig.load()
362
364
  emb = config.embedding
363
365
  return {
364
366
  "provider": emb.provider,
@@ -382,7 +384,7 @@ async def set_embedding_config(request: Request):
382
384
  try:
383
385
  body = await request.json()
384
386
  from superlocalmemory.core.config import SLMConfig, EmbeddingConfig
385
- config = SLMConfig.load()
387
+ config = getattr(request.app.state, "config", None) or SLMConfig.load()
386
388
 
387
389
  new_provider = body.get("provider", config.embedding.provider)
388
390
  new_model = body.get("model_name", config.embedding.model_name)
@@ -596,7 +598,9 @@ def _validate_provider_url(url: str, client_host: str) -> str | None:
596
598
  host = p.hostname or ""
597
599
  if host.lower() in ("169.254.169.254", "metadata.google.internal", "metadata"):
598
600
  return "Cloud metadata endpoints are not allowed"
599
- if client_host in ("127.0.0.1", "::1", "localhost"):
601
+ from superlocalmemory.server.loopback import is_loopback as _is_loopback_host
602
+
603
+ if _is_loopback_host(client_host):
600
604
  return None # local dashboard may target its own local/LAN endpoints
601
605
  # SLM_REMOTE residue (#40): an allowlisted LAN dashboard is trusted exactly
602
606
  # like the loopback one and may probe its own LAN LLM endpoint. This does
@@ -1420,7 +1424,7 @@ async def get_associations(
1420
1424
  if not DB_PATH.exists():
1421
1425
  return {"edges": [], "total": 0}
1422
1426
 
1423
- conn = sqlite3.connect(str(DB_PATH))
1427
+ conn = get_read_connection(DB_PATH)
1424
1428
  conn.row_factory = sqlite3.Row
1425
1429
 
1426
1430
  # Build query with optional type filter (parameterized)
@@ -1493,7 +1497,7 @@ async def get_association_stats(request: Request, profile: str = ""):
1493
1497
  "top_connected_facts": [],
1494
1498
  }
1495
1499
 
1496
- conn = sqlite3.connect(str(DB_PATH))
1500
+ conn = get_read_connection(DB_PATH)
1497
1501
  conn.row_factory = sqlite3.Row
1498
1502
 
1499
1503
  # Total edges
@@ -1605,7 +1609,7 @@ async def get_consolidation_status(request: Request, profile: str = ""):
1605
1609
  if not DB_PATH.exists():
1606
1610
  return result
1607
1611
 
1608
- conn = sqlite3.connect(str(DB_PATH))
1612
+ conn = get_read_connection(DB_PATH)
1609
1613
  conn.row_factory = sqlite3.Row
1610
1614
 
1611
1615
  # Last consolidation log entry
@@ -1757,7 +1761,7 @@ async def get_core_memory(request: Request, profile: str = ""):
1757
1761
  if not DB_PATH.exists():
1758
1762
  return {"blocks": [], "total_chars": 0, "char_limit": 2000}
1759
1763
 
1760
- conn = sqlite3.connect(str(DB_PATH))
1764
+ conn = get_read_connection(DB_PATH)
1761
1765
  conn.row_factory = sqlite3.Row
1762
1766
 
1763
1767
  rows = conn.execute(
@@ -1808,7 +1812,7 @@ async def update_core_memory_block(block_id: str, request: Request):
1808
1812
  )
1809
1813
 
1810
1814
  from superlocalmemory.server.routes.helpers import DB_PATH, get_active_profile
1811
- import sqlite3
1815
+ from superlocalmemory.storage.memory_write import memory_write
1812
1816
  from datetime import datetime, timezone
1813
1817
 
1814
1818
  if not DB_PATH.exists():
@@ -1827,46 +1831,38 @@ async def update_core_memory_block(block_id: str, request: Request):
1827
1831
  content_preview=str(content),
1828
1832
  )
1829
1833
 
1830
- conn = sqlite3.connect(str(DB_PATH))
1831
- conn.row_factory = sqlite3.Row
1832
-
1833
- # Verify block exists
1834
- existing = conn.execute(
1835
- "SELECT block_id, profile_id, block_type, version "
1836
- "FROM core_memory_blocks WHERE block_id = ? AND profile_id = ?",
1837
- (block_id, pid),
1838
- ).fetchone()
1839
-
1840
- if not existing:
1841
- conn.close()
1842
- return JSONResponse(
1843
- {"error": f"Block {block_id} not found"},
1844
- status_code=404,
1845
- )
1846
-
1847
- existing_dict = dict(existing)
1848
- new_version = existing_dict["version"] + 1
1849
1834
  now = datetime.now(timezone.utc).isoformat()
1835
+ # memory_write: process write lock + busy_timeout.
1836
+ # SELECT + UPDATE + read-back are atomic inside the same connection.
1837
+ with memory_write(DB_PATH) as conn:
1838
+ # Verify block exists
1839
+ existing = conn.execute(
1840
+ "SELECT block_id, profile_id, block_type, version "
1841
+ "FROM core_memory_blocks WHERE block_id = ? AND profile_id = ?",
1842
+ (block_id, pid),
1843
+ ).fetchone()
1850
1844
 
1851
- conn.execute(
1852
- "UPDATE core_memory_blocks SET content = ?, char_count = ?, "
1853
- "version = ?, compiled_by = 'manual', updated_at = ? "
1854
- "WHERE block_id = ? AND profile_id = ?",
1855
- (content, len(content), new_version, now, block_id, pid),
1856
- )
1857
- conn.commit()
1845
+ if not existing:
1846
+ raise HTTPException(status_code=404, detail=f"Block {block_id} not found")
1858
1847
 
1859
- # Read back updated block
1860
- updated = conn.execute(
1861
- "SELECT block_id, block_type, content, char_count, version, "
1862
- "compiled_by, updated_at FROM core_memory_blocks "
1863
- "WHERE block_id = ? AND profile_id = ?",
1864
- (block_id, pid),
1865
- ).fetchone()
1866
- conn.close()
1848
+ new_version = dict(existing)["version"] + 1
1849
+ conn.execute(
1850
+ "UPDATE core_memory_blocks SET content = ?, char_count = ?, "
1851
+ "version = ?, compiled_by = 'manual', updated_at = ? "
1852
+ "WHERE block_id = ? AND profile_id = ?",
1853
+ (content, len(content), new_version, now, block_id, pid),
1854
+ )
1855
+ # Read back updated block while connection is still open.
1856
+ updated = conn.execute(
1857
+ "SELECT block_id, block_type, content, char_count, version, "
1858
+ "compiled_by, updated_at FROM core_memory_blocks "
1859
+ "WHERE block_id = ? AND profile_id = ?",
1860
+ (block_id, pid),
1861
+ ).fetchone()
1862
+ updated_dict = dict(updated) if updated else {"block_id": block_id, "updated": True}
1867
1863
 
1868
1864
  authorization.complete()
1869
- return dict(updated) if updated else {"block_id": block_id, "updated": True}
1865
+ return updated_dict
1870
1866
  except HTTPException:
1871
1867
  raise
1872
1868
  except Exception as e:
@@ -1907,7 +1903,7 @@ async def get_vector_store_status(request: Request, profile: str = ""):
1907
1903
  # Count vectors in embedding_metadata
1908
1904
  if DB_PATH.exists():
1909
1905
  try:
1910
- conn = sqlite3.connect(str(DB_PATH))
1906
+ conn = get_read_connection(DB_PATH)
1911
1907
  count = conn.execute(
1912
1908
  "SELECT COUNT(*) FROM embedding_metadata WHERE profile_id = ?",
1913
1909
  (pid,),
@@ -1951,7 +1947,7 @@ async def forgetting_stats(request: Request, profile: str = ""):
1951
1947
  if not DB_PATH.exists():
1952
1948
  return {"total": total, "zones": zones}
1953
1949
 
1954
- conn = _sqlite3.connect(str(DB_PATH))
1950
+ conn = get_read_connection(DB_PATH)
1955
1951
  conn.row_factory = _sqlite3.Row
1956
1952
 
1957
1953
  try:
@@ -1990,7 +1986,7 @@ async def run_forgetting(request: Request):
1990
1986
  profile = body.get("profile", "")
1991
1987
 
1992
1988
  from superlocalmemory.server.routes.helpers import get_active_profile, DB_PATH
1993
- import sqlite3 as _sqlite3
1989
+ from superlocalmemory.storage.memory_write import memory_write as _memory_write
1994
1990
  pid = _resolve_mutation_profile(profile)
1995
1991
  _require_manage_for_profile(request, pid)
1996
1992
 
@@ -2003,59 +1999,55 @@ async def run_forgetting(request: Request):
2003
1999
  source_agent_id="http-forgetting-run",
2004
2000
  profile_id=pid,
2005
2001
  )
2006
- conn = _sqlite3.connect(str(DB_PATH))
2007
- conn.row_factory = _sqlite3.Row
2008
2002
 
2003
+ # memory_write: process write lock + busy_timeout — all UPDATEs atomic.
2009
2004
  updated = 0
2010
2005
  try:
2011
- # Apply Ebbinghaus decay: reduce retention for facts not accessed recently
2012
- # Formula: retention *= exp(-0.1) for each cycle (simplified batch decay)
2013
- conn.execute(
2014
- "UPDATE fact_retention "
2015
- "SET retention_score = MAX(0.0, retention_score * 0.9), "
2016
- " last_computed_at = datetime('now') "
2017
- "WHERE profile_id = ? "
2018
- "AND lifecycle_zone NOT IN ('archive', 'forgotten')",
2019
- (pid,),
2020
- )
2021
- updated = conn.total_changes
2022
-
2023
- # Transition zones based on new retention scores
2024
- zone_thresholds = [
2025
- ("forgotten", 0.05),
2026
- ("archive", 0.15),
2027
- ("cold", 0.35),
2028
- ("warm", 0.65),
2029
- ]
2030
- for zone, threshold in zone_thresholds:
2006
+ with _memory_write(DB_PATH) as conn:
2007
+ # Apply Ebbinghaus decay: reduce retention for facts not accessed recently
2008
+ # Formula: retention *= exp(-0.1) for each cycle (simplified batch decay)
2031
2009
  conn.execute(
2032
2010
  "UPDATE fact_retention "
2033
- "SET lifecycle_zone = ? "
2011
+ "SET retention_score = MAX(0.0, retention_score * 0.9), "
2012
+ " last_computed_at = datetime('now') "
2034
2013
  "WHERE profile_id = ? "
2035
- "AND retention_score < ? "
2036
2014
  "AND lifecycle_zone NOT IN ('archive', 'forgotten')",
2037
- (zone, pid, threshold),
2015
+ (pid,),
2038
2016
  )
2017
+ updated = conn.total_changes
2018
+
2019
+ # Transition zones based on new retention scores
2020
+ zone_thresholds = [
2021
+ ("forgotten", 0.05),
2022
+ ("archive", 0.15),
2023
+ ("cold", 0.35),
2024
+ ("warm", 0.65),
2025
+ ]
2026
+ for zone, threshold in zone_thresholds:
2027
+ conn.execute(
2028
+ "UPDATE fact_retention "
2029
+ "SET lifecycle_zone = ? "
2030
+ "WHERE profile_id = ? "
2031
+ "AND retention_score < ? "
2032
+ "AND lifecycle_zone NOT IN ('archive', 'forgotten')",
2033
+ (zone, pid, threshold),
2034
+ )
2039
2035
 
2040
- # Ensure high-retention facts are active
2041
- conn.execute(
2042
- "UPDATE fact_retention "
2043
- "SET lifecycle_zone = 'active' "
2044
- "WHERE profile_id = ? AND retention_score >= 0.65 "
2045
- "AND lifecycle_zone NOT IN ('archive', 'forgotten')",
2046
- (pid,),
2047
- )
2048
-
2049
- from superlocalmemory.core.lifecycle_state import reconcile_profile_lifecycle
2050
- reconcile_profile_lifecycle(conn, pid)
2036
+ # Ensure high-retention facts are active
2037
+ conn.execute(
2038
+ "UPDATE fact_retention "
2039
+ "SET lifecycle_zone = 'active' "
2040
+ "WHERE profile_id = ? AND retention_score >= 0.65 "
2041
+ "AND lifecycle_zone NOT IN ('archive', 'forgotten')",
2042
+ (pid,),
2043
+ )
2051
2044
 
2052
- conn.commit()
2045
+ from superlocalmemory.core.lifecycle_state import reconcile_profile_lifecycle
2046
+ reconcile_profile_lifecycle(conn, pid)
2053
2047
  except Exception as exc:
2054
2048
  logger.exception("run_forgetting decay failed")
2055
- conn.close()
2056
2049
  return {"success": False, "error": "internal error"}
2057
2050
 
2058
- conn.close()
2059
2051
  authorization.complete()
2060
2052
  return {"success": True, "facts_decayed": updated, "profile": pid}
2061
2053
  except HTTPException:
@@ -2081,7 +2073,7 @@ async def quantization_stats(request: Request, profile: str = ""):
2081
2073
  if not DB_PATH.exists():
2082
2074
  return {"total": total, "tiers": tiers, "compression_ratio": compression_ratio}
2083
2075
 
2084
- conn = _sqlite3.connect(str(DB_PATH))
2076
+ conn = get_read_connection(DB_PATH)
2085
2077
  conn.row_factory = _sqlite3.Row
2086
2078
 
2087
2079
  try:
@@ -2140,7 +2132,7 @@ async def ccq_blocks(request: Request, profile: str = "", limit: int = 50):
2140
2132
  if not DB_PATH.exists():
2141
2133
  return {"blocks": [], "total": 0}
2142
2134
 
2143
- conn = _sqlite3.connect(str(DB_PATH))
2135
+ conn = get_read_connection(DB_PATH)
2144
2136
  conn.row_factory = _sqlite3.Row
2145
2137
 
2146
2138
  blocks = []
@@ -2200,7 +2192,7 @@ async def get_soft_prompts(request: Request, profile: str = ""):
2200
2192
  if not DB_PATH.exists():
2201
2193
  return {"prompts": [], "total": 0, "total_tokens": 0}
2202
2194
 
2203
- conn = _sqlite3.connect(str(DB_PATH))
2195
+ conn = get_read_connection(DB_PATH)
2204
2196
  conn.row_factory = _sqlite3.Row
2205
2197
 
2206
2198
  prompts = []
@@ -2308,7 +2300,7 @@ async def get_graph_communities(request: Request, profile: str = ""):
2308
2300
  if not DB_PATH.exists():
2309
2301
  return {"communities": [], "total": 0}
2310
2302
 
2311
- conn = sqlite3.connect(str(DB_PATH))
2303
+ conn = get_read_connection(DB_PATH)
2312
2304
  conn.row_factory = sqlite3.Row
2313
2305
 
2314
2306
  # Get community member counts and average pagerank
@@ -2475,7 +2467,7 @@ async def v33_overview(request: Request, profile: str = ""):
2475
2467
  if not DB_PATH.exists():
2476
2468
  return overview
2477
2469
 
2478
- conn = _sqlite3.connect(str(DB_PATH))
2470
+ conn = get_read_connection(DB_PATH)
2479
2471
  conn.row_factory = _sqlite3.Row
2480
2472
 
2481
2473
  # Forgetting stats