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.
- package/CHANGELOG.md +76 -0
- package/README.md +3 -2
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/CLAUDE.md +3 -3
- package/plugin/agents/slm-governance-advisor.md +1 -1
- package/plugin/agents/slm-loop-runner.md +1 -1
- package/plugin/agents/slm-memory-advisor.md +1 -1
- package/plugin/agents/slm-optimize-advisor.md +1 -1
- package/plugin/requirements.txt +1 -1
- package/plugin/skills/slm-cache/SKILL.md +1 -1
- package/plugin/skills/slm-compress/SKILL.md +1 -1
- package/plugin/skills/slm-governance/SKILL.md +1 -1
- package/plugin/skills/slm-graph/SKILL.md +1 -1
- package/plugin/skills/slm-loop/SKILL.md +1 -1
- package/plugin/skills/slm-mesh/SKILL.md +1 -1
- package/plugin/skills/slm-profile/SKILL.md +1 -1
- package/plugin/skills/slm-recall/SKILL.md +1 -1
- package/plugin/skills/slm-remember/SKILL.md +1 -1
- package/plugin/skills/slm-scope/SKILL.md +1 -1
- package/plugin/skills/slm-session/SKILL.md +1 -1
- package/plugin/skills/slm-status/SKILL.md +1 -1
- package/plugin-src/rules/AGENTS.md +1 -1
- package/plugin-src/skills/slm-cache/SKILL.md +1 -1
- package/plugin-src/skills/slm-compress/SKILL.md +1 -1
- package/plugin-src/skills/slm-graph/SKILL.md +1 -1
- package/plugin-src/skills/slm-recall/SKILL.md +1 -1
- package/plugin-src/skills/slm-remember/SKILL.md +1 -1
- package/plugin-src/skills/slm-session/SKILL.md +1 -1
- package/plugin-src/skills/slm-status/SKILL.md +1 -1
- package/pyproject.toml +9 -4
- package/src/superlocalmemory/__init__.py +1 -1
- package/src/superlocalmemory/access/rbac.py +68 -76
- package/src/superlocalmemory/cli/commands.py +158 -404
- package/src/superlocalmemory/cli/ingest_cmd.py +11 -1
- package/src/superlocalmemory/cli/main.py +30 -0
- package/src/superlocalmemory/cli/pending_store.py +39 -14
- package/src/superlocalmemory/core/backend_orchestrator.py +93 -0
- package/src/superlocalmemory/core/component_registry.py +4 -2
- package/src/superlocalmemory/core/config.py +78 -0
- package/src/superlocalmemory/core/consolidation_engine.py +79 -73
- package/src/superlocalmemory/core/embeddings.py +33 -6
- package/src/superlocalmemory/core/engine.py +186 -60
- package/src/superlocalmemory/core/engine_ingestion.py +150 -63
- package/src/superlocalmemory/core/fact_consolidator.py +148 -30
- package/src/superlocalmemory/core/graph_pruner.py +436 -39
- package/src/superlocalmemory/core/ingestion_command.py +273 -32
- package/src/superlocalmemory/core/maintenance_scheduler.py +61 -1
- package/src/superlocalmemory/core/mutations.py +32 -10
- package/src/superlocalmemory/core/recall_pipeline.py +111 -74
- package/src/superlocalmemory/core/registry.py +5 -1
- package/src/superlocalmemory/core/remember_admission.py +152 -0
- package/src/superlocalmemory/core/remember_runtime.py +712 -0
- package/src/superlocalmemory/core/remote_mode.py +3 -1
- package/src/superlocalmemory/core/scale_engine.py +41 -18
- package/src/superlocalmemory/core/store_pipeline.py +18 -4
- package/src/superlocalmemory/encoding/entity_resolver.py +18 -11
- package/src/superlocalmemory/graph/cozo_backend.py +5 -5
- package/src/superlocalmemory/hooks/_outcome_common.py +9 -2
- package/src/superlocalmemory/hooks/adapter_base.py +58 -44
- package/src/superlocalmemory/hooks/ide_connector.py +26 -8
- package/src/superlocalmemory/hooks/portable_kit.py +105 -9
- package/src/superlocalmemory/hooks/prewarm_auth.py +21 -2
- package/src/superlocalmemory/infra/auth_middleware.py +3 -1
- package/src/superlocalmemory/infra/cloud_backup.py +26 -27
- package/src/superlocalmemory/infra/event_bus.py +250 -88
- package/src/superlocalmemory/learning/bandit.py +50 -1
- package/src/superlocalmemory/learning/consolidation_cycle.py +33 -16
- package/src/superlocalmemory/learning/entity_compiler.py +148 -132
- package/src/superlocalmemory/learning/memory_merge.py +97 -82
- package/src/superlocalmemory/learning/reward_archive.py +98 -90
- package/src/superlocalmemory/learning/reward_boost.py +40 -30
- package/src/superlocalmemory/learning/source_quality.py +38 -35
- package/src/superlocalmemory/mcp/_daemon_proxy.py +38 -15
- package/src/superlocalmemory/mcp/http_transport.py +335 -3
- package/src/superlocalmemory/mcp/tools_active.py +4 -41
- package/src/superlocalmemory/mcp/tools_core.py +26 -87
- package/src/superlocalmemory/mcp/tools_evolution.py +5 -10
- package/src/superlocalmemory/optimize/proxy/capture.py +196 -8
- package/src/superlocalmemory/retrieval/engine.py +15 -4
- package/src/superlocalmemory/retrieval/entity_channel.py +25 -1
- package/src/superlocalmemory/retrieval/reranker.py +130 -22
- package/src/superlocalmemory/retrieval/spreading_activation.py +20 -12
- package/src/superlocalmemory/retrieval/vector_store.py +84 -69
- package/src/superlocalmemory/server/loopback.py +85 -0
- package/src/superlocalmemory/server/origin.py +9 -4
- package/src/superlocalmemory/server/profile_runtime.py +14 -0
- package/src/superlocalmemory/server/routes/abstraction.py +2 -4
- package/src/superlocalmemory/server/routes/agents.py +3 -5
- package/src/superlocalmemory/server/routes/backup.py +6 -2
- package/src/superlocalmemory/server/routes/behavioral.py +11 -25
- package/src/superlocalmemory/server/routes/brain.py +6 -9
- package/src/superlocalmemory/server/routes/compliance.py +20 -23
- package/src/superlocalmemory/server/routes/config_api.py +83 -0
- package/src/superlocalmemory/server/routes/entity.py +3 -7
- package/src/superlocalmemory/server/routes/evolution.py +3 -5
- package/src/superlocalmemory/server/routes/helpers.py +57 -25
- package/src/superlocalmemory/server/routes/insights.py +2 -4
- package/src/superlocalmemory/server/routes/learning.py +2 -5
- package/src/superlocalmemory/server/routes/lifecycle.py +2 -4
- package/src/superlocalmemory/server/routes/memories.py +119 -98
- package/src/superlocalmemory/server/routes/mesh.py +7 -2
- package/src/superlocalmemory/server/routes/profiles.py +20 -21
- package/src/superlocalmemory/server/routes/rbac.py +0 -1
- package/src/superlocalmemory/server/routes/tiers.py +28 -35
- package/src/superlocalmemory/server/routes/timeline.py +2 -4
- package/src/superlocalmemory/server/routes/v3_api.py +85 -93
- package/src/superlocalmemory/server/unified_daemon.py +400 -140
- package/src/superlocalmemory/server/write_identity.py +22 -4
- package/src/superlocalmemory/storage/admission_codec.py +119 -0
- package/src/superlocalmemory/storage/admission_journal.py +728 -0
- package/src/superlocalmemory/storage/database.py +168 -19
- package/src/superlocalmemory/storage/deferred_writes.py +209 -0
- package/src/superlocalmemory/storage/embedding_migrator.py +19 -0
- package/src/superlocalmemory/storage/memory_write.py +115 -0
- package/src/superlocalmemory/storage/migration_runner.py +44 -0
- package/src/superlocalmemory/storage/migrations/M028_fact_entity_associations.py +113 -78
- package/src/superlocalmemory/storage/migrations/M031_dead_letter_operations.py +80 -0
- package/src/superlocalmemory/storage/migrations/M032_write_coordinator_admission.py +188 -0
- package/src/superlocalmemory/storage/read_connection.py +115 -0
- package/src/superlocalmemory/storage/write_coordinator.py +756 -0
- package/src/superlocalmemory/storage/write_lock.py +88 -0
- package/src/superlocalmemory/ui/index.html +1 -1
- package/src/superlocalmemory/ui/js/auto-settings.js +14 -1
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|