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
@@ -32,6 +32,10 @@ import sqlite3
32
32
  import uuid
33
33
  from datetime import datetime, timezone
34
34
  from pathlib import Path
35
+ from typing import TYPE_CHECKING, Union
36
+
37
+ if TYPE_CHECKING:
38
+ from superlocalmemory.storage.database import DatabaseManager
35
39
 
36
40
  logger = logging.getLogger("superlocalmemory.fact_consolidator")
37
41
 
@@ -41,7 +45,7 @@ _MAX_CONSOLIDATED_CHARS = 2000
41
45
 
42
46
 
43
47
  def consolidate_facts(
44
- db_path: str | Path,
48
+ db_or_path: "Union[DatabaseManager, str, Path]",
45
49
  profile_id: str = "default",
46
50
  max_clusters: int = 20,
47
51
  dry_run: bool = False,
@@ -49,6 +53,20 @@ def consolidate_facts(
49
53
  ) -> dict:
50
54
  """Find and consolidate clusters of related facts.
51
55
 
56
+ Concurrency fix (v3.8.4 — implements TODO from Fix-A comment):
57
+ When a DatabaseManager is provided the consolidation now uses per-cluster
58
+ short write transactions instead of one long raw_connection() that held
59
+ the write lock across Ollama / Cloud-LLM calls for every cluster after
60
+ the first. The fix:
61
+
62
+ 1. Discover clusters with a short memory_read() (no write lock).
63
+ 2. For each cluster: fetch fact content (memory_read()), generate the
64
+ summary OUTSIDE any lock (Ollama / Cloud LLM may take 30 s), then
65
+ do the SAVEPOINT write inside a short memory_write() block.
66
+
67
+ SAVEPOINT atomicity per cluster is preserved: _consolidate_cluster still
68
+ uses SAVEPOINT / RELEASE / ROLLBACK TO internally.
69
+
52
70
  Mode behavior:
53
71
  - Mode A: Extractive only (no LLM). Always available.
54
72
  - Mode B: Ollama LLM summarization. Falls back to extractive if Ollama down.
@@ -56,7 +74,10 @@ def consolidate_facts(
56
74
 
57
75
  Returns stats: consolidated, clusters_found, facts_archived, errors.
58
76
  """
59
- stats = {
77
+ from superlocalmemory.storage.database import DatabaseManager
78
+ from superlocalmemory.storage.memory_write import memory_read, memory_write
79
+
80
+ stats: dict = {
60
81
  "clusters_found": 0,
61
82
  "consolidated": 0,
62
83
  "facts_archived": 0,
@@ -71,7 +92,79 @@ def consolidate_facts(
71
92
  mode_str = getattr(mode, 'value', str(mode)).lower()
72
93
  stats["mode"] = mode_str
73
94
 
74
- conn = sqlite3.connect(str(db_path))
95
+ if isinstance(db_or_path, DatabaseManager):
96
+ db_path = db_or_path.db_path
97
+ try:
98
+ # Step 1: discover clusters — short read, no write lock.
99
+ with memory_read(db_path) as rconn:
100
+ rconn.row_factory = sqlite3.Row
101
+ clusters = _find_consolidation_clusters(rconn, profile_id, max_clusters)
102
+ stats["clusters_found"] = len(clusters)
103
+
104
+ for entity_id, entity_name, fact_ids in clusters:
105
+ try:
106
+ # Step 2: load fact content for this cluster (read, no write lock).
107
+ placeholders = ",".join("?" * len(fact_ids))
108
+ with memory_read(db_path) as rconn:
109
+ rconn.row_factory = sqlite3.Row
110
+ facts = rconn.execute(
111
+ f"SELECT fact_id, content, confidence, created_at, "
112
+ f"canonical_entities_json, scope, shared_with "
113
+ f"FROM atomic_facts "
114
+ f"WHERE fact_id IN ({placeholders}) ORDER BY created_at",
115
+ fact_ids,
116
+ ).fetchall()
117
+ facts = [dict(f) for f in facts]
118
+
119
+ if len(facts) < _MIN_CLUSTER_SIZE:
120
+ continue
121
+
122
+ # Step 3: generate summary OUTSIDE any write lock.
123
+ # Ollama (Mode B) or Cloud LLM (Mode C) may take 30 s here.
124
+ summary = _generate_summary(entity_name, facts, config)
125
+ if not summary:
126
+ continue
127
+
128
+ if dry_run:
129
+ stats["consolidated"] += 1
130
+ stats["facts_archived"] += len(fact_ids)
131
+ continue
132
+
133
+ # Step 4: short per-cluster write — hold lock only for SQL.
134
+ with memory_write(db_path) as conn:
135
+ conn.row_factory = sqlite3.Row
136
+ result = _consolidate_cluster(
137
+ conn, profile_id, entity_id, entity_name,
138
+ fact_ids, dry_run=False, config=None,
139
+ _presummary=summary,
140
+ )
141
+ if result:
142
+ stats["consolidated"] += 1
143
+ stats["facts_archived"] += len(fact_ids)
144
+ except Exception as exc:
145
+ logger.warning(
146
+ "Consolidation failed for %s: %s",
147
+ entity_name, exc, exc_info=True,
148
+ )
149
+ stats["errors"] += 1
150
+
151
+ if stats["consolidated"] > 0:
152
+ logger.info(
153
+ "Fact consolidation: %d clusters merged, %d facts archived",
154
+ stats["consolidated"], stats["facts_archived"],
155
+ )
156
+ except Exception as exc:
157
+ logger.error("Fact consolidation failed: %s", exc, exc_info=True)
158
+ stats["errors"] += 1
159
+ stats["error_detail"] = str(exc)
160
+ return stats
161
+
162
+ # Backward-compat: str | Path — open own connection.
163
+ logger.warning(
164
+ "consolidate_facts: passing a db_path is deprecated — pass a "
165
+ "DatabaseManager instead (Fix A backward-compat shim active)"
166
+ )
167
+ conn = sqlite3.connect(str(db_or_path))
75
168
  wal_mode = conn.execute("PRAGMA journal_mode=WAL").fetchone()
76
169
  if wal_mode and wal_mode[0] != "wal":
77
170
  logger.warning("WAL mode not active, got: %s", wal_mode[0])
@@ -79,33 +172,9 @@ def consolidate_facts(
79
172
  conn.row_factory = sqlite3.Row
80
173
 
81
174
  try:
82
- clusters = _find_consolidation_clusters(conn, profile_id, max_clusters)
83
- stats["clusters_found"] = len(clusters)
84
-
85
- for entity_id, entity_name, fact_ids in clusters:
86
- try:
87
- result = _consolidate_cluster(
88
- conn, profile_id, entity_id, entity_name,
89
- fact_ids, dry_run, config,
90
- )
91
- if result:
92
- stats["consolidated"] += 1
93
- stats["facts_archived"] += len(fact_ids)
94
- except Exception as exc:
95
- logger.warning(
96
- "Consolidation failed for %s: %s",
97
- entity_name, exc, exc_info=True,
98
- )
99
- stats["errors"] += 1
100
-
175
+ _run_consolidation(conn, profile_id, max_clusters, dry_run, config, stats)
101
176
  if not dry_run:
102
177
  conn.commit()
103
-
104
- if stats["consolidated"] > 0:
105
- logger.info(
106
- "Fact consolidation: %d clusters merged, %d facts archived",
107
- stats["consolidated"], stats["facts_archived"],
108
- )
109
178
  except Exception as exc:
110
179
  logger.error("Fact consolidation failed: %s", exc, exc_info=True)
111
180
  stats["errors"] += 1
@@ -116,6 +185,41 @@ def consolidate_facts(
116
185
  return stats
117
186
 
118
187
 
188
+ def _run_consolidation(
189
+ conn: sqlite3.Connection,
190
+ profile_id: str,
191
+ max_clusters: int,
192
+ dry_run: bool,
193
+ config: object | None,
194
+ stats: dict,
195
+ ) -> None:
196
+ """Core consolidation logic — connection-agnostic inner function."""
197
+ clusters = _find_consolidation_clusters(conn, profile_id, max_clusters)
198
+ stats["clusters_found"] = len(clusters)
199
+
200
+ for entity_id, entity_name, fact_ids in clusters:
201
+ try:
202
+ result = _consolidate_cluster(
203
+ conn, profile_id, entity_id, entity_name,
204
+ fact_ids, dry_run, config,
205
+ )
206
+ if result:
207
+ stats["consolidated"] += 1
208
+ stats["facts_archived"] += len(fact_ids)
209
+ except Exception as exc:
210
+ logger.warning(
211
+ "Consolidation failed for %s: %s",
212
+ entity_name, exc, exc_info=True,
213
+ )
214
+ stats["errors"] += 1
215
+
216
+ if stats["consolidated"] > 0:
217
+ logger.info(
218
+ "Fact consolidation: %d clusters merged, %d facts archived",
219
+ stats["consolidated"], stats["facts_archived"],
220
+ )
221
+
222
+
119
223
  def _find_consolidation_clusters(
120
224
  conn: sqlite3.Connection,
121
225
  profile_id: str,
@@ -179,15 +283,25 @@ def _consolidate_cluster(
179
283
  fact_ids: list[str],
180
284
  dry_run: bool,
181
285
  config: object | None = None,
286
+ *,
287
+ _presummary: str | None = None,
182
288
  ) -> dict | None:
183
289
  """Merge a cluster of facts into one consolidated fact.
184
290
 
185
291
  All writes are wrapped in a SAVEPOINT for atomicity — if any step fails,
186
292
  the entire cluster consolidation is rolled back.
293
+
294
+ _presummary (v3.8.4): when the caller supplies a pre-computed summary
295
+ (generated OUTSIDE the write lock), skip the _generate_summary() call.
296
+ This is the short-lock path used by the DatabaseManager branch of
297
+ consolidate_facts(). The str | Path backward-compat path still calls
298
+ _generate_summary() inline (legacy behaviour, no regression).
187
299
  """
188
300
  c = conn.cursor()
189
301
 
190
- # Load fact contents including canonical_entities_json
302
+ # Load fact contents including canonical_entities_json.
303
+ # Even when _presummary is provided we still re-read from the DB inside
304
+ # the write lock so the SAVEPOINT has a fresh, authoritative facts list.
191
305
  placeholders = ",".join("?" * len(fact_ids))
192
306
  facts = c.execute(
193
307
  f"SELECT fact_id, content, confidence, created_at, canonical_entities_json, "
@@ -200,7 +314,11 @@ def _consolidate_cluster(
200
314
  if len(facts) < _MIN_CLUSTER_SIZE:
201
315
  return None
202
316
 
203
- summary = _generate_summary(entity_name, facts, config)
317
+ if _presummary is not None:
318
+ summary = _presummary
319
+ else:
320
+ # Legacy path (str | Path caller) — may call Ollama with write lock held.
321
+ summary = _generate_summary(entity_name, facts, config)
204
322
  if not summary:
205
323
  return None
206
324