superlocalmemory 3.8.2 → 3.8.5
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 +57 -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 +1 -1
- package/src/superlocalmemory/__init__.py +1 -1
- package/src/superlocalmemory/access/rbac.py +68 -76
- package/src/superlocalmemory/cli/commands.py +19 -0
- 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/config.py +78 -0
- package/src/superlocalmemory/core/consolidation_engine.py +79 -73
- package/src/superlocalmemory/core/engine.py +92 -11
- 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 +160 -31
- package/src/superlocalmemory/core/maintenance_scheduler.py +61 -1
- package/src/superlocalmemory/core/recall_pipeline.py +3 -0
- package/src/superlocalmemory/core/registry.py +5 -1
- 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/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/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/mcp/http_transport.py +335 -3
- package/src/superlocalmemory/retrieval/engine.py +7 -1
- package/src/superlocalmemory/retrieval/entity_channel.py +25 -1
- package/src/superlocalmemory/retrieval/reranker.py +98 -15
- package/src/superlocalmemory/retrieval/spreading_activation.py +20 -12
- package/src/superlocalmemory/retrieval/vector_store.py +84 -69
- package/src/superlocalmemory/server/loopback.py +91 -0
- package/src/superlocalmemory/server/origin.py +9 -4
- package/src/superlocalmemory/server/routes/backup.py +6 -2
- package/src/superlocalmemory/server/routes/behavioral.py +6 -12
- package/src/superlocalmemory/server/routes/compliance.py +20 -23
- package/src/superlocalmemory/server/routes/config_api.py +83 -0
- package/src/superlocalmemory/server/routes/helpers.py +24 -13
- package/src/superlocalmemory/server/routes/memories.py +139 -91
- 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 +42 -30
- package/src/superlocalmemory/server/routes/v3_api.py +67 -77
- package/src/superlocalmemory/server/unified_daemon.py +283 -39
- package/src/superlocalmemory/server/write_identity.py +22 -4
- package/src/superlocalmemory/storage/database.py +109 -19
- package/src/superlocalmemory/storage/deferred_writes.py +153 -0
- package/src/superlocalmemory/storage/embedding_migrator.py +19 -0
- package/src/superlocalmemory/storage/memory_write.py +119 -0
- package/src/superlocalmemory/storage/migration_runner.py +7 -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/write_lock.py +88 -0
- package/src/superlocalmemory/ui/js/core.js +6 -1
|
@@ -34,6 +34,7 @@ from pathlib import Path
|
|
|
34
34
|
from superlocalmemory.learning.fact_outcome_joins import (
|
|
35
35
|
has_recent_positive_reward,
|
|
36
36
|
)
|
|
37
|
+
from superlocalmemory.storage.write_lock import get_write_lock
|
|
37
38
|
|
|
38
39
|
logger = logging.getLogger(__name__)
|
|
39
40
|
|
|
@@ -80,13 +81,16 @@ def run_reward_gated_archive(
|
|
|
80
81
|
return []
|
|
81
82
|
|
|
82
83
|
archived: list[str] = []
|
|
83
|
-
conn = sqlite3.connect(str(memory_db_path), timeout=10.0)
|
|
84
|
-
conn.row_factory = sqlite3.Row
|
|
85
|
-
conn.execute("PRAGMA busy_timeout=2000")
|
|
86
84
|
|
|
85
|
+
# READ phase — no write lock needed (WAL allows concurrent reads).
|
|
86
|
+
# H-12/H-P-02: compute the archivable set BEFORE acquiring the writer
|
|
87
|
+
# lock so that the lock is held only for the bulk INSERT/UPDATE pass.
|
|
88
|
+
conn_r = sqlite3.connect(str(memory_db_path), timeout=10.0)
|
|
89
|
+
conn_r.row_factory = sqlite3.Row
|
|
90
|
+
conn_r.execute("PRAGMA busy_timeout=2000")
|
|
87
91
|
try:
|
|
88
92
|
placeholders = ",".join("?" for _ in candidate_fact_ids)
|
|
89
|
-
rows =
|
|
93
|
+
rows = conn_r.execute(
|
|
90
94
|
f"SELECT fact_id, content, canonical_entities_json, importance, "
|
|
91
95
|
f" confidence, embedding, created_at "
|
|
92
96
|
f"FROM atomic_facts "
|
|
@@ -95,13 +99,6 @@ def run_reward_gated_archive(
|
|
|
95
99
|
(profile_id, *candidate_fact_ids),
|
|
96
100
|
).fetchall()
|
|
97
101
|
|
|
98
|
-
# H-12/H-P-02: compute the archivable set BEFORE acquiring the
|
|
99
|
-
# writer lock. Previously ``BEGIN IMMEDIATE`` wrapped the full
|
|
100
|
-
# per-candidate reward-lookup loop — holding RESERVED for the
|
|
101
|
-
# entire scan starved concurrent ``record_recall`` writers out
|
|
102
|
-
# with SQLITE_BUSY after their 50 ms busy_timeout. Splitting the
|
|
103
|
-
# read phase keeps the writer lock held only for the bulk
|
|
104
|
-
# INSERT/UPDATE pass.
|
|
105
102
|
to_archive: list[dict] = []
|
|
106
103
|
for row in rows:
|
|
107
104
|
fid = row["fact_id"]
|
|
@@ -111,7 +108,7 @@ def run_reward_gated_archive(
|
|
|
111
108
|
# 2. Recent positive reward skip (criterion 2).
|
|
112
109
|
# H-06 fix — JSON1 equality join via helper.
|
|
113
110
|
if has_recent_positive_reward(
|
|
114
|
-
|
|
111
|
+
conn_r, profile_id, fid,
|
|
115
112
|
min_reward=ARCHIVE_REWARD_THRESHOLD,
|
|
116
113
|
window_days=REWARD_WINDOW_DAYS,
|
|
117
114
|
):
|
|
@@ -125,86 +122,97 @@ def run_reward_gated_archive(
|
|
|
125
122
|
"embedding": row["embedding"],
|
|
126
123
|
"created_at": row["created_at"],
|
|
127
124
|
})
|
|
125
|
+
finally:
|
|
126
|
+
conn_r.close()
|
|
128
127
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
"
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
# replaced with a minimal stub + ``truncated`` reason so the
|
|
165
|
-
# archive row stays within the I4 disk budget while still
|
|
166
|
-
# pointing back to the original ``fact_id`` in atomic_facts.
|
|
167
|
-
payload_str = json.dumps(payload)
|
|
168
|
-
reason = "reward_gated_ebbinghaus"
|
|
169
|
-
if len(payload_str.encode("utf-8")) > PAYLOAD_JSON_MAX_BYTES:
|
|
170
|
-
payload_str = json.dumps({
|
|
128
|
+
if not to_archive:
|
|
129
|
+
return []
|
|
130
|
+
|
|
131
|
+
# WRITE phase — acquire the process-level write lock BEFORE opening the
|
|
132
|
+
# write connection. This serialises BEGIN IMMEDIATE → commit with all
|
|
133
|
+
# other in-process writers (DatabaseManager, VectorStore, adapter sync,
|
|
134
|
+
# memory_merge, reward_boost), eliminating SQLITE_BUSY races at the WAL
|
|
135
|
+
# layer.
|
|
136
|
+
with get_write_lock(memory_db_path):
|
|
137
|
+
conn = sqlite3.connect(str(memory_db_path), timeout=10.0)
|
|
138
|
+
conn.row_factory = sqlite3.Row
|
|
139
|
+
conn.execute("PRAGMA busy_timeout=2000")
|
|
140
|
+
try:
|
|
141
|
+
conn.execute("BEGIN IMMEDIATE")
|
|
142
|
+
# S9-SKEP-07: re-verify reward under RESERVED lock. Between the
|
|
143
|
+
# read-only reward scan above and this BEGIN IMMEDIATE another
|
|
144
|
+
# writer may have inserted a positive reward row. With the writer
|
|
145
|
+
# lock held we know no further inserts are landing; one last
|
|
146
|
+
# check per entry catches everything that raced in.
|
|
147
|
+
verified: list[dict] = []
|
|
148
|
+
for entry in to_archive:
|
|
149
|
+
if has_recent_positive_reward(
|
|
150
|
+
conn, profile_id, entry["fid"],
|
|
151
|
+
min_reward=ARCHIVE_REWARD_THRESHOLD,
|
|
152
|
+
window_days=REWARD_WINDOW_DAYS,
|
|
153
|
+
):
|
|
154
|
+
continue
|
|
155
|
+
verified.append(entry)
|
|
156
|
+
to_archive = verified
|
|
157
|
+
if not to_archive:
|
|
158
|
+
conn.execute("COMMIT")
|
|
159
|
+
return []
|
|
160
|
+
for entry in to_archive:
|
|
161
|
+
fid = entry["fid"]
|
|
162
|
+
payload = {
|
|
171
163
|
"fact_id": fid,
|
|
172
|
-
"
|
|
173
|
-
"
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
"
|
|
178
|
-
|
|
164
|
+
"content": entry["content"],
|
|
165
|
+
"canonical_entities_json": entry["canonical_entities_json"],
|
|
166
|
+
"importance": entry["importance"],
|
|
167
|
+
"confidence": entry["confidence"],
|
|
168
|
+
"embedding": entry["embedding"],
|
|
169
|
+
"created_at": entry["created_at"],
|
|
170
|
+
}
|
|
171
|
+
# SEC-L3 — cap payload_json at 256 KB. Oversize blobs are
|
|
172
|
+
# replaced with a minimal stub + ``truncated`` reason so the
|
|
173
|
+
# archive row stays within the I4 disk budget while still
|
|
174
|
+
# pointing back to the original ``fact_id`` in atomic_facts.
|
|
175
|
+
payload_str = json.dumps(payload)
|
|
176
|
+
reason = "reward_gated_ebbinghaus"
|
|
177
|
+
if len(payload_str.encode("utf-8")) > PAYLOAD_JSON_MAX_BYTES:
|
|
178
|
+
payload_str = json.dumps({
|
|
179
|
+
"fact_id": fid,
|
|
180
|
+
"truncated": True,
|
|
181
|
+
"original_bytes": len(payload_str.encode("utf-8")),
|
|
182
|
+
})
|
|
183
|
+
reason = "reward_gated_ebbinghaus_truncated"
|
|
184
|
+
logger.warning(
|
|
185
|
+
"memory_archive payload >%d bytes for fact_id=%s; "
|
|
186
|
+
"truncated to stub", PAYLOAD_JSON_MAX_BYTES, fid,
|
|
187
|
+
)
|
|
188
|
+
conn.execute(
|
|
189
|
+
"INSERT INTO memory_archive "
|
|
190
|
+
"(archive_id, fact_id, profile_id, payload_json, "
|
|
191
|
+
" archived_at, reason) VALUES (?, ?, ?, ?, ?, ?)",
|
|
192
|
+
(
|
|
193
|
+
str(uuid.uuid4()),
|
|
194
|
+
fid,
|
|
195
|
+
profile_id,
|
|
196
|
+
payload_str,
|
|
197
|
+
_iso_now(),
|
|
198
|
+
reason,
|
|
199
|
+
),
|
|
179
200
|
)
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
fid,
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
)
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
" archive_reason='reward_gated_ebbinghaus' "
|
|
197
|
-
"WHERE fact_id=? "
|
|
198
|
-
" AND (archive_status IS NULL OR archive_status='live')",
|
|
199
|
-
(fid,),
|
|
200
|
-
)
|
|
201
|
-
archived.append(fid)
|
|
202
|
-
|
|
203
|
-
conn.commit()
|
|
204
|
-
except sqlite3.Error as exc:
|
|
205
|
-
conn.rollback()
|
|
206
|
-
logger.warning("run_reward_gated_archive rollback: %s", exc)
|
|
207
|
-
finally:
|
|
208
|
-
conn.close()
|
|
201
|
+
conn.execute(
|
|
202
|
+
"UPDATE atomic_facts "
|
|
203
|
+
"SET archive_status='archived', "
|
|
204
|
+
" archive_reason='reward_gated_ebbinghaus' "
|
|
205
|
+
"WHERE fact_id=? "
|
|
206
|
+
" AND (archive_status IS NULL OR archive_status='live')",
|
|
207
|
+
(fid,),
|
|
208
|
+
)
|
|
209
|
+
archived.append(fid)
|
|
210
|
+
|
|
211
|
+
conn.commit()
|
|
212
|
+
except sqlite3.Error as exc:
|
|
213
|
+
conn.rollback()
|
|
214
|
+
logger.warning("run_reward_gated_archive rollback: %s", exc)
|
|
215
|
+
finally:
|
|
216
|
+
conn.close()
|
|
209
217
|
|
|
210
218
|
return archived
|
|
@@ -23,6 +23,7 @@ from pathlib import Path
|
|
|
23
23
|
from superlocalmemory.learning.fact_outcome_joins import (
|
|
24
24
|
aggregate_reward_for_fact,
|
|
25
25
|
)
|
|
26
|
+
from superlocalmemory.storage.write_lock import get_write_lock
|
|
26
27
|
|
|
27
28
|
|
|
28
29
|
# H-12/H-P-01: single-pass JSON1 aggregation across ALL facts for a profile.
|
|
@@ -123,45 +124,54 @@ def apply_strong_memory_boost(
|
|
|
123
124
|
|
|
124
125
|
Returns number of rows boosted.
|
|
125
126
|
"""
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
boosted = 0
|
|
127
|
+
# READ phase — no write lock needed (WAL allows concurrent reads).
|
|
128
|
+
conn_r = sqlite3.connect(str(memory_db_path), timeout=10.0)
|
|
129
129
|
try:
|
|
130
|
-
rows =
|
|
130
|
+
rows = conn_r.execute(
|
|
131
131
|
"SELECT fact_id FROM atomic_facts WHERE profile_id=? "
|
|
132
132
|
" AND (archive_status IS NULL OR archive_status='live')",
|
|
133
133
|
(profile_id,),
|
|
134
134
|
).fetchall()
|
|
135
135
|
if not rows:
|
|
136
|
+
conn_r.close()
|
|
136
137
|
return 0
|
|
137
|
-
|
|
138
138
|
# H-12/H-P-01: single JSON1 GROUP BY replaces the per-fact loop.
|
|
139
|
-
|
|
140
|
-
# SQLite without JSON1.
|
|
141
|
-
stats = _bulk_fact_reward_stats(conn, profile_id)
|
|
142
|
-
conn.execute("BEGIN IMMEDIATE")
|
|
143
|
-
for (fid,) in rows:
|
|
144
|
-
if stats:
|
|
145
|
-
count, mean = stats.get(fid, _MISS)
|
|
146
|
-
else:
|
|
147
|
-
count, mean = aggregate_reward_for_fact(conn, profile_id, fid)
|
|
148
|
-
if count < STRONG_BOOST_MIN_OUTCOMES:
|
|
149
|
-
continue
|
|
150
|
-
if mean <= STRONG_BOOST_MIN_MEAN:
|
|
151
|
-
continue
|
|
152
|
-
conn.execute(
|
|
153
|
-
"UPDATE atomic_facts "
|
|
154
|
-
"SET retrieval_prior = MIN(COALESCE(retrieval_prior, 0) + ?, ?) "
|
|
155
|
-
"WHERE fact_id=?",
|
|
156
|
-
(STRONG_BOOST_INCREMENT, STRONG_BOOST_CAP, fid),
|
|
157
|
-
)
|
|
158
|
-
boosted += 1
|
|
159
|
-
conn.commit()
|
|
160
|
-
except sqlite3.Error as exc:
|
|
161
|
-
conn.rollback()
|
|
162
|
-
logger.warning("apply_strong_memory_boost rollback: %s", exc)
|
|
139
|
+
stats = _bulk_fact_reward_stats(conn_r, profile_id)
|
|
163
140
|
finally:
|
|
164
|
-
|
|
141
|
+
conn_r.close()
|
|
142
|
+
|
|
143
|
+
# WRITE phase — acquire the process-level write lock BEFORE opening the
|
|
144
|
+
# write connection. This serialises BEGIN IMMEDIATE → commit with all
|
|
145
|
+
# other in-process writers (DatabaseManager, VectorStore, adapter sync),
|
|
146
|
+
# eliminating SQLITE_BUSY races at the SQLite WAL layer.
|
|
147
|
+
boosted = 0
|
|
148
|
+
with get_write_lock(memory_db_path):
|
|
149
|
+
conn = sqlite3.connect(str(memory_db_path), timeout=10.0)
|
|
150
|
+
conn.execute("PRAGMA busy_timeout=2000")
|
|
151
|
+
try:
|
|
152
|
+
conn.execute("BEGIN IMMEDIATE")
|
|
153
|
+
for (fid,) in rows:
|
|
154
|
+
if stats:
|
|
155
|
+
count, mean = stats.get(fid, _MISS)
|
|
156
|
+
else:
|
|
157
|
+
count, mean = aggregate_reward_for_fact(conn, profile_id, fid)
|
|
158
|
+
if count < STRONG_BOOST_MIN_OUTCOMES:
|
|
159
|
+
continue
|
|
160
|
+
if mean <= STRONG_BOOST_MIN_MEAN:
|
|
161
|
+
continue
|
|
162
|
+
conn.execute(
|
|
163
|
+
"UPDATE atomic_facts "
|
|
164
|
+
"SET retrieval_prior = MIN(COALESCE(retrieval_prior, 0) + ?, ?) "
|
|
165
|
+
"WHERE fact_id=?",
|
|
166
|
+
(STRONG_BOOST_INCREMENT, STRONG_BOOST_CAP, fid),
|
|
167
|
+
)
|
|
168
|
+
boosted += 1
|
|
169
|
+
conn.commit()
|
|
170
|
+
except sqlite3.Error as exc:
|
|
171
|
+
conn.rollback()
|
|
172
|
+
logger.warning("apply_strong_memory_boost rollback: %s", exc)
|
|
173
|
+
finally:
|
|
174
|
+
conn.close()
|
|
165
175
|
return boosted
|
|
166
176
|
|
|
167
177
|
|