superlocalmemory 4.0.8 → 4.0.10
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 +221 -1
- package/README.md +6 -6
- 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/scripts/ensure-venv.sh +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 +3 -3
- package/plugin-src/rules/AGENTS.md +1 -1
- package/plugin-src/skills/slm-status/SKILL.md +2 -2
- package/pyproject.toml +1 -1
- package/scripts/postinstall.js +4 -0
- package/src/superlocalmemory/__init__.py +1 -1
- package/src/superlocalmemory/cli/_lazy_init.py +1 -1
- package/src/superlocalmemory/cli/commands.py +164 -11
- package/src/superlocalmemory/cli/db_migrate.py +0 -2
- package/src/superlocalmemory/cli/gdpr_io.py +1 -1
- package/src/superlocalmemory/cli/main.py +7 -7
- package/src/superlocalmemory/cli/service_installer.py +2 -1
- package/src/superlocalmemory/cli/setup_wizard.py +1 -1
- package/src/superlocalmemory/code_graph/bridge/maintenance.py +8 -0
- package/src/superlocalmemory/core/config.py +41 -7
- package/src/superlocalmemory/core/context_cache.py +0 -2
- package/src/superlocalmemory/core/engine.py +371 -63
- package/src/superlocalmemory/core/evidence_bundle.py +3 -1
- package/src/superlocalmemory/core/fact_consolidator.py +316 -125
- package/src/superlocalmemory/core/install_detector.py +131 -0
- package/src/superlocalmemory/core/maintenance.py +44 -6
- package/src/superlocalmemory/core/memory_health.py +266 -0
- package/src/superlocalmemory/core/operation_policy_registry.py +1 -1
- package/src/superlocalmemory/core/operation_request.py +1 -1
- package/src/superlocalmemory/core/ops_remediation.py +2 -2
- package/src/superlocalmemory/core/progressive_abstraction.py +1 -1
- package/src/superlocalmemory/core/security_primitives.py +3 -6
- package/src/superlocalmemory/core/store_pipeline.py +172 -29
- package/src/superlocalmemory/core/topic_signature.py +0 -2
- package/src/superlocalmemory/core/transactions/concrete_owners.py +15 -8
- package/src/superlocalmemory/dynamics/eap_scheduler.py +17 -6
- package/src/superlocalmemory/encoding/cognitive_consolidator.py +15 -1
- package/src/superlocalmemory/encoding/graph_builder.py +2 -2
- package/src/superlocalmemory/encoding/scene_builder.py +8 -2
- package/src/superlocalmemory/hooks/adapter_base.py +0 -2
- package/src/superlocalmemory/hooks/context_payload.py +0 -2
- package/src/superlocalmemory/hooks/portable_kit.py +8 -8
- package/src/superlocalmemory/hooks/post_tool_async_hook.py +0 -2
- package/src/superlocalmemory/hooks/prewarm_auth.py +0 -2
- package/src/superlocalmemory/hooks/user_prompt_hook.py +0 -2
- package/src/superlocalmemory/infra/backup.py +44 -8
- package/src/superlocalmemory/integrations/bounded_loops_mcp.py +24 -7
- package/src/superlocalmemory/learning/arm_catalog.py +0 -2
- package/src/superlocalmemory/learning/bandit.py +0 -2
- package/src/superlocalmemory/learning/bandit_cache.py +0 -2
- package/src/superlocalmemory/learning/dedup_hnsw.py +11 -11
- package/src/superlocalmemory/learning/ensemble.py +0 -2
- package/src/superlocalmemory/learning/labeler.py +0 -2
- package/src/superlocalmemory/learning/legacy_migration.py +0 -2
- package/src/superlocalmemory/learning/model_cache.py +0 -2
- package/src/superlocalmemory/learning/ranker.py +0 -2
- package/src/superlocalmemory/learning/reward_archive.py +6 -1
- package/src/superlocalmemory/learning/reward_proxy.py +0 -2
- package/src/superlocalmemory/learning/signal_worker.py +0 -2
- package/src/superlocalmemory/math/fisher.py +1 -1
- package/src/superlocalmemory/math/hopfield.py +4 -1
- package/src/superlocalmemory/math/langevin.py +1 -1
- package/src/superlocalmemory/math/sheaf.py +7 -3
- package/src/superlocalmemory/mcp/cli_fallback.py +1 -1
- package/src/superlocalmemory/mcp/profiles.py +1 -1
- package/src/superlocalmemory/mcp/server.py +2 -2
- package/src/superlocalmemory/mcp/session_binding.py +92 -0
- package/src/superlocalmemory/mcp/tools_active.py +56 -0
- package/src/superlocalmemory/mcp/tools_core.py +41 -40
- package/src/superlocalmemory/mcp/tools_ops.py +2 -2
- package/src/superlocalmemory/optimize/cache/manager.py +2 -2
- package/src/superlocalmemory/optimize/compress/ccr.py +1 -1
- package/src/superlocalmemory/optimize/compress/router.py +1 -1
- package/src/superlocalmemory/optimize/proxy/_helpers.py +2 -2
- package/src/superlocalmemory/optimize/proxy/server.py +1 -1
- package/src/superlocalmemory/optimize/proxy/vertex_surface.py +2 -2
- package/src/superlocalmemory/optimize/storage/db.py +2 -2
- package/src/superlocalmemory/retrieval/agentic.py +1 -1
- package/src/superlocalmemory/retrieval/ann_index.py +9 -2
- package/src/superlocalmemory/retrieval/bm25_channel.py +6 -10
- package/src/superlocalmemory/retrieval/bridge_discovery.py +2 -2
- package/src/superlocalmemory/retrieval/engine.py +272 -43
- package/src/superlocalmemory/retrieval/entity_channel.py +8 -2
- package/src/superlocalmemory/retrieval/hopfield_channel.py +8 -2
- package/src/superlocalmemory/retrieval/profile_channel.py +1 -1
- package/src/superlocalmemory/retrieval/quantization_aware_search.py +1 -1
- package/src/superlocalmemory/retrieval/remote_reranker.py +2 -2
- package/src/superlocalmemory/retrieval/reranker.py +3 -3
- package/src/superlocalmemory/retrieval/scope_policy.py +22 -1
- package/src/superlocalmemory/retrieval/semantic_channel.py +3 -3
- package/src/superlocalmemory/retrieval/spreading_activation.py +8 -8
- package/src/superlocalmemory/retrieval/strategy.py +94 -0
- package/src/superlocalmemory/retrieval/temporal_channel.py +179 -10
- package/src/superlocalmemory/retrieval/temporal_validity_filter.py +1 -1
- package/src/superlocalmemory/retrieval/vector_store.py +151 -10
- package/src/superlocalmemory/server/api.py +6 -1
- package/src/superlocalmemory/server/asset_versions.py +171 -0
- package/src/superlocalmemory/server/recall_serializer.py +10 -0
- package/src/superlocalmemory/server/routes/abstraction.py +201 -0
- package/src/superlocalmemory/server/routes/brain.py +2 -2
- package/src/superlocalmemory/server/routes/data_io.py +29 -1
- package/src/superlocalmemory/server/routes/entity.py +13 -1
- package/src/superlocalmemory/server/routes/mesh.py +1 -1
- package/src/superlocalmemory/server/routes/prewarm.py +4 -4
- package/src/superlocalmemory/server/routes/v3_api.py +2 -2
- package/src/superlocalmemory/server/ui.py +8 -1
- package/src/superlocalmemory/server/unified_daemon.py +570 -16
- package/src/superlocalmemory/storage/_migration_internals.py +4 -0
- package/src/superlocalmemory/storage/_schema_version.py +46 -3
- package/src/superlocalmemory/storage/backup.py +531 -0
- package/src/superlocalmemory/storage/database.py +139 -34
- package/src/superlocalmemory/storage/embedding_codec.py +129 -0
- package/src/superlocalmemory/storage/embedding_migrator.py +5 -3
- package/src/superlocalmemory/storage/migration_runner.py +153 -2
- package/src/superlocalmemory/storage/migrations/M043_quarantine_display_summaries.py +488 -0
- package/src/superlocalmemory/storage/migrations/__init__.py +1 -1
- package/src/superlocalmemory/storage/migrations.py +15 -1
- package/src/superlocalmemory/storage/models.py +7 -0
- package/src/superlocalmemory/storage/quantized_store.py +4 -2
- package/src/superlocalmemory/storage/schema.py +98 -0
- package/src/superlocalmemory/summaries/base.py +1 -1
- package/src/superlocalmemory/summaries/non_answer.py +223 -0
- package/src/superlocalmemory/summaries/session_summary.py +1 -1
- package/src/superlocalmemory/ui/css/legacy-dashboard.css +1 -1
- package/src/superlocalmemory/ui/css/neural-glass.css +1 -1
- package/src/superlocalmemory/ui/index.html +1 -1
- package/src/superlocalmemory/ui/js/core.js +1 -1
- package/src/superlocalmemory/ui/js/od-brain.js +1 -1
- package/src/superlocalmemory/ui/js/od-memories.js +190 -1
- package/src/superlocalmemory/ui/js/od-ops-health.js +1 -1
|
@@ -118,6 +118,54 @@ def _record_correction_candidate(
|
|
|
118
118
|
logger.warning("Correction candidate not recorded for %s: %s", successor_fact_id, exc)
|
|
119
119
|
|
|
120
120
|
|
|
121
|
+
#: Entity id pattern for the per-profile placeholder a dated fact with no
|
|
122
|
+
#: resolved entity points its temporal event at. Per profile, not shared, so
|
|
123
|
+
#: profile deletion cascades it away with everything else.
|
|
124
|
+
_UNRESOLVED_ENTITY_PREFIX = "__slm_unresolved__:"
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _ensure_unresolved_entity(db: Any, profile_id: str) -> str | None:
|
|
128
|
+
"""Return the placeholder entity id for ``profile_id``, creating it once.
|
|
129
|
+
|
|
130
|
+
``temporal_events.entity_id`` is NOT NULL with a foreign key to
|
|
131
|
+
``canonical_entities``, so an event for a fact with no recognised entity
|
|
132
|
+
needs something real to reference. This is that something.
|
|
133
|
+
|
|
134
|
+
It is deliberately inert as an entity:
|
|
135
|
+
|
|
136
|
+
* Its ``canonical_name`` is empty, and the only query that resolves an
|
|
137
|
+
entity by name (``retrieval/temporal_channel.py``, the "events for
|
|
138
|
+
entity X" lookup) is driven by names extracted from the user's
|
|
139
|
+
question, which are never empty.
|
|
140
|
+
* Nothing derives entity links from ``temporal_events``. The entity
|
|
141
|
+
channel builds its map from ``atomic_facts.canonical_entities_json``,
|
|
142
|
+
which this does not touch — so the placeholder cannot become the kind
|
|
143
|
+
of entity that links a quarter of the store and flattens entity
|
|
144
|
+
proximity. That failure mode is real (on the author's store 'State'
|
|
145
|
+
links 1,388 facts) and this is specifically shaped not to add to it.
|
|
146
|
+
* ``fact_count`` stays 0. It is a hook for a foreign key, not a concept.
|
|
147
|
+
|
|
148
|
+
Returns None if the row cannot be created, in which case the caller skips
|
|
149
|
+
the temporal event rather than failing the write — a missing temporal row
|
|
150
|
+
is a degraded memory, an aborted remember is a lost one.
|
|
151
|
+
"""
|
|
152
|
+
entity_id = f"{_UNRESOLVED_ENTITY_PREFIX}{profile_id}"
|
|
153
|
+
try:
|
|
154
|
+
db.execute(
|
|
155
|
+
"INSERT OR IGNORE INTO canonical_entities "
|
|
156
|
+
"(entity_id, profile_id, canonical_name, entity_type, fact_count) "
|
|
157
|
+
"VALUES (?, ?, '', 'unresolved', 0)",
|
|
158
|
+
(entity_id, profile_id),
|
|
159
|
+
)
|
|
160
|
+
return entity_id
|
|
161
|
+
except Exception as exc: # noqa: BLE001 — never fail a write over this
|
|
162
|
+
logger.debug(
|
|
163
|
+
"temporal placeholder entity unavailable for %s: %s",
|
|
164
|
+
profile_id, exc,
|
|
165
|
+
)
|
|
166
|
+
return None
|
|
167
|
+
|
|
168
|
+
|
|
121
169
|
def _record_fact_entity_association(
|
|
122
170
|
db: DatabaseManager,
|
|
123
171
|
*,
|
|
@@ -311,8 +359,13 @@ def enrich_fact(
|
|
|
311
359
|
# Vector dual-write helper (P1-2 / embeddings-vector-01)
|
|
312
360
|
# ---------------------------------------------------------------------------
|
|
313
361
|
|
|
314
|
-
def _upsert_fact_vectors(fact, profile_id, ann_index, vector_store, embedder=None):
|
|
315
|
-
"""Dual-write a fact's embedding to the
|
|
362
|
+
def _upsert_fact_vectors(fact, profile_id, ann_index, vector_store, embedder=None) -> bool:
|
|
363
|
+
"""Dual-write a fact's embedding to the sqlite-vec projection and the ANN index.
|
|
364
|
+
|
|
365
|
+
Write order mirrors _attach_vector: the sqlite-vec projection is attempted
|
|
366
|
+
first because that is what a meaning-based search reads. The ANN index is
|
|
367
|
+
updated only when the projection accepts the vector. Returns True when the
|
|
368
|
+
fact is now findable by meaning (the projection accepted the vector).
|
|
316
369
|
|
|
317
370
|
Embeds on-demand when the fact has no embedding (e.g. consolidated
|
|
318
371
|
summary facts created without one), so UPDATE/SUPERSEDE and consolidated
|
|
@@ -325,18 +378,44 @@ def _upsert_fact_vectors(fact, profile_id, ann_index, vector_store, embedder=Non
|
|
|
325
378
|
except Exception as _emb_exc: # pragma: no cover - defensive
|
|
326
379
|
_reraise_materialization_deferral(_emb_exc)
|
|
327
380
|
logger.debug("on-demand embed failed for %s: %s", fact.fact_id, _emb_exc)
|
|
328
|
-
return
|
|
381
|
+
return False
|
|
329
382
|
if not getattr(fact, "embedding", None):
|
|
330
|
-
return
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
#
|
|
383
|
+
return False
|
|
384
|
+
|
|
385
|
+
projected = False
|
|
386
|
+
# Projection first: what a meaning-based search reads.
|
|
334
387
|
if vector_store and getattr(vector_store, "available", False):
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
388
|
+
try:
|
|
389
|
+
projected = bool(vector_store.upsert(
|
|
390
|
+
fact_id=fact.fact_id,
|
|
391
|
+
profile_id=profile_id,
|
|
392
|
+
embedding=fact.embedding,
|
|
393
|
+
))
|
|
394
|
+
if not projected:
|
|
395
|
+
logger.warning(
|
|
396
|
+
"vector projection refused for %s — stored but not "
|
|
397
|
+
"findable by meaning", fact.fact_id[:12],
|
|
398
|
+
)
|
|
399
|
+
except Exception as _vs_exc:
|
|
400
|
+
logger.warning(
|
|
401
|
+
"vector projection failed for %s (%s: %s) — stored but not "
|
|
402
|
+
"findable by meaning",
|
|
403
|
+
fact.fact_id[:12], type(_vs_exc).__name__, _vs_exc,
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
# ANN index only when the projection accepted the vector so both
|
|
407
|
+
# representations stay consistent. Writing ANN without a vector-store
|
|
408
|
+
# entry creates a ghost that disappears on restart (ANN is rebuilt from
|
|
409
|
+
# the vector store at startup).
|
|
410
|
+
if projected and ann_index:
|
|
411
|
+
try:
|
|
412
|
+
ann_index.add(fact.fact_id, fact.embedding)
|
|
413
|
+
except Exception:
|
|
414
|
+
logger.warning(
|
|
415
|
+
"in-memory index rejected %s", fact.fact_id[:12], exc_info=True,
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
return projected
|
|
340
419
|
|
|
341
420
|
|
|
342
421
|
class _TombstoneReadError(Exception):
|
|
@@ -741,6 +820,15 @@ def run_store(
|
|
|
741
820
|
materialization_progress["relational_started"] = True
|
|
742
821
|
|
|
743
822
|
is_queryable_promotion = fact.fact_id in queryable_ids
|
|
823
|
+
# When a promoted fact is being materialised the embedding column is
|
|
824
|
+
# written in a separate step AFTER the projection attempt so that the
|
|
825
|
+
# two representations stay in the correct order: projection first,
|
|
826
|
+
# canonical column second (mirroring _attach_vector's invariant).
|
|
827
|
+
# Writing the column before the projection and then failing the
|
|
828
|
+
# projection leaves the fact with embedding IS NOT NULL but no entry
|
|
829
|
+
# in fact_embeddings — invisible to meaning-search and invisible to
|
|
830
|
+
# every repair pass that selects WHERE embedding IS NULL.
|
|
831
|
+
_deferred_canonical_embedding = None # set below if promotion path taken
|
|
744
832
|
if is_queryable_promotion:
|
|
745
833
|
db.update_fact(fact.fact_id, {
|
|
746
834
|
"content": fact.content,
|
|
@@ -757,15 +845,22 @@ def run_store(
|
|
|
757
845
|
"access_count": fact.access_count,
|
|
758
846
|
"source_turn_ids_json": fact.source_turn_ids,
|
|
759
847
|
"session_id": fact.session_id,
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
848
|
+
# embedding / fisher fields omitted here — written after
|
|
849
|
+
# the projection attempt below so canonical is never ahead
|
|
850
|
+
# of the projection.
|
|
763
851
|
"lifecycle": fact.lifecycle,
|
|
764
852
|
"langevin_position": fact.langevin_position,
|
|
765
853
|
"emotional_valence": fact.emotional_valence,
|
|
766
854
|
"emotional_arousal": fact.emotional_arousal,
|
|
767
855
|
"signal_type": fact.signal_type,
|
|
768
856
|
})
|
|
857
|
+
if fact.embedding:
|
|
858
|
+
_deferred_canonical_embedding = (
|
|
859
|
+
fact.fact_id,
|
|
860
|
+
fact.embedding,
|
|
861
|
+
fact.fisher_mean,
|
|
862
|
+
fact.fisher_variance,
|
|
863
|
+
)
|
|
769
864
|
if consolidator:
|
|
770
865
|
try:
|
|
771
866
|
action = consolidator.consolidate(
|
|
@@ -793,6 +888,9 @@ def run_store(
|
|
|
793
888
|
target_id = action.existing_fact_id
|
|
794
889
|
if is_queryable_promotion and target_id:
|
|
795
890
|
db.delete_fact(fact.fact_id)
|
|
891
|
+
# The original promoted fact was deleted; its deferred
|
|
892
|
+
# canonical embedding write must not happen.
|
|
893
|
+
_deferred_canonical_embedding = None
|
|
796
894
|
existing_fact = db.get_fact(target_id) if target_id else None
|
|
797
895
|
if existing_fact is None:
|
|
798
896
|
continue
|
|
@@ -838,9 +936,31 @@ def run_store(
|
|
|
838
936
|
if materialization_progress is not None:
|
|
839
937
|
materialization_progress["fact_ids"] = tuple(stored_ids)
|
|
840
938
|
|
|
841
|
-
#
|
|
842
|
-
#
|
|
939
|
+
# Projection first (vector store then ANN index, on-demand embed for
|
|
940
|
+
# consolidated facts that arrived without one).
|
|
843
941
|
_upsert_fact_vectors(fact, profile_id, ann_index, vector_store, embedder)
|
|
942
|
+
|
|
943
|
+
# Canonical embedding write: deferred to here from the queryable-
|
|
944
|
+
# promotion block above so the projection is always attempted first.
|
|
945
|
+
# The column is written regardless of whether the projection accepted
|
|
946
|
+
# the vector (the vector is real data; withholding it would force
|
|
947
|
+
# every repair pass to re-compute a model call that the same
|
|
948
|
+
# installation condition would refuse again anyway).
|
|
949
|
+
if _deferred_canonical_embedding is not None:
|
|
950
|
+
_dc_fid, _dc_emb, _dc_fmean, _dc_fvar = _deferred_canonical_embedding
|
|
951
|
+
try:
|
|
952
|
+
db.update_fact(_dc_fid, {
|
|
953
|
+
"embedding": _dc_emb,
|
|
954
|
+
"fisher_mean": _dc_fmean,
|
|
955
|
+
"fisher_variance": _dc_fvar,
|
|
956
|
+
})
|
|
957
|
+
except Exception as _dc_exc:
|
|
958
|
+
logger.warning(
|
|
959
|
+
"canonical embedding write failed for %s: %s",
|
|
960
|
+
_dc_fid[:12], _dc_exc,
|
|
961
|
+
)
|
|
962
|
+
_deferred_canonical_embedding = None
|
|
963
|
+
|
|
844
964
|
# Phase 2: Generate contextual description (after consolidator, before graph_builder)
|
|
845
965
|
if context_generator:
|
|
846
966
|
try:
|
|
@@ -959,12 +1079,39 @@ def run_store(
|
|
|
959
1079
|
if scene_builder:
|
|
960
1080
|
scene_builder.assign_to_scene(fact, profile_id)
|
|
961
1081
|
|
|
962
|
-
# Populate temporal_events for temporal retrieval
|
|
1082
|
+
# Populate temporal_events for temporal retrieval.
|
|
1083
|
+
#
|
|
1084
|
+
# A DATE IS A DATE WHETHER OR NOT AN ENTITY WAS RESOLVED. This used to
|
|
1085
|
+
# read `if fact.canonical_entities and has_dates`, so a fact carrying a
|
|
1086
|
+
# perfectly good date but no recognised entity got no temporal row at
|
|
1087
|
+
# all. Measured on the author's store: 967 of 3,894 genuine facts have
|
|
1088
|
+
# no canonical entity (24.8%), and 958 of those have no temporal event
|
|
1089
|
+
# either — a quarter of the store that the temporal channel could only
|
|
1090
|
+
# reach through its created_at recency fallback, i.e. by being recent
|
|
1091
|
+
# rather than by being about the right time.
|
|
1092
|
+
#
|
|
1093
|
+
# The date-window query does not need the entity: it joins
|
|
1094
|
+
# temporal_events to atomic_facts and nothing else
|
|
1095
|
+
# (retrieval/temporal_channel.py). Only the "events for entity X"
|
|
1096
|
+
# lookup joins canonical_entities, and that lookup is correctly
|
|
1097
|
+
# uninterested in a fact with no entity.
|
|
1098
|
+
#
|
|
1099
|
+
# temporal_events.entity_id is NOT NULL with an FK to
|
|
1100
|
+
# canonical_entities, so an entity-less event needs a row to point at.
|
|
1101
|
+
# It gets a per-profile sentinel rather than a shared one, and the
|
|
1102
|
+
# sentinel is invisible to ranking: the entity channel builds its map
|
|
1103
|
+
# from atomic_facts.canonical_entities_json, which is untouched here, so
|
|
1104
|
+
# this cannot create the kind of entity that links a quarter of the
|
|
1105
|
+
# store and destroys entity proximity.
|
|
963
1106
|
has_dates = (fact.observation_date or fact.referenced_date
|
|
964
1107
|
or fact.interval_start)
|
|
965
|
-
if
|
|
1108
|
+
if has_dates:
|
|
966
1109
|
from superlocalmemory.storage.models import TemporalEvent
|
|
967
|
-
|
|
1110
|
+
entity_ids = list(fact.canonical_entities)
|
|
1111
|
+
if not entity_ids:
|
|
1112
|
+
sentinel = _ensure_unresolved_entity(db, profile_id)
|
|
1113
|
+
entity_ids = [sentinel] if sentinel else []
|
|
1114
|
+
for eid in entity_ids:
|
|
968
1115
|
event = TemporalEvent(
|
|
969
1116
|
event_id=_ingestion_effect_id(
|
|
970
1117
|
ingestion_operation_id,
|
|
@@ -1139,15 +1286,11 @@ def run_store_fact_direct(
|
|
|
1139
1286
|
)
|
|
1140
1287
|
fact.canonical_entities = list(canonical.values())
|
|
1141
1288
|
db.store_fact(fact)
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
#
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
fact_id=fact.fact_id,
|
|
1148
|
-
profile_id=profile_id,
|
|
1149
|
-
embedding=fact.embedding,
|
|
1150
|
-
)
|
|
1289
|
+
# Projection first (vector store), ANN only on success — matching
|
|
1290
|
+
# _attach_vector's invariant. The return value (projected) is not used
|
|
1291
|
+
# here because run_store_fact_direct is a fire-and-forget path with no
|
|
1292
|
+
# receipt to update; the caller is responsible for any enrichment status.
|
|
1293
|
+
_upsert_fact_vectors(fact, profile_id, ann_index, vector_store)
|
|
1151
1294
|
if graph_builder:
|
|
1152
1295
|
graph_builder.build_edges(fact, profile_id)
|
|
1153
1296
|
# The graph projection must run after GraphBuilder: syncing immediately
|
|
@@ -15,6 +15,7 @@ from superlocalmemory.core.transactions.owners import (
|
|
|
15
15
|
OwnerResult,
|
|
16
16
|
)
|
|
17
17
|
from superlocalmemory.core.transactions.service import MemoryTransactionService
|
|
18
|
+
from superlocalmemory.storage.embedding_codec import decode_embedding
|
|
18
19
|
|
|
19
20
|
REQUIRED_ADMISSION_OWNERS: tuple[str, ...] = ("bm25", "temporal", "vector")
|
|
20
21
|
|
|
@@ -438,10 +439,16 @@ class VectorOwner(_FactScopedOwner):
|
|
|
438
439
|
for row in rows:
|
|
439
440
|
record = dict(row)
|
|
440
441
|
fid = record.get("fact_id")
|
|
441
|
-
|
|
442
|
-
if fid is None or not
|
|
442
|
+
raw_embedding = record.get("embedding")
|
|
443
|
+
if fid is None or not raw_embedding:
|
|
443
444
|
continue
|
|
444
|
-
|
|
445
|
+
# Decode so TEXT and BLOB rows produce the same fingerprint for the
|
|
446
|
+
# same embedding values. A ValueError here (corrupt buffer) must
|
|
447
|
+
# propagate — a bad fingerprint is worse than no fingerprint.
|
|
448
|
+
decoded = decode_embedding(raw_embedding, fact_id=fid)
|
|
449
|
+
if decoded is None:
|
|
450
|
+
continue
|
|
451
|
+
result[fid] = _fingerprint("vector", str(decoded))
|
|
445
452
|
return result
|
|
446
453
|
|
|
447
454
|
def _table_present(
|
|
@@ -496,11 +503,11 @@ class VectorOwner(_FactScopedOwner):
|
|
|
496
503
|
raw = dict(rows[0]).get("embedding")
|
|
497
504
|
if not raw:
|
|
498
505
|
return False
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
if not
|
|
506
|
+
# decode_embedding accepts both TEXT (JSON) and BLOB (float32 binary);
|
|
507
|
+
# ValueError for corrupt data propagates — a heal that silently
|
|
508
|
+
# returns False on corruption is indistinguishable from "not present".
|
|
509
|
+
embedding = decode_embedding(raw, fact_id=fact_id)
|
|
510
|
+
if not embedding:
|
|
504
511
|
return False
|
|
505
512
|
ok = self._vector_store.upsert(
|
|
506
513
|
fact_id=fact_id, profile_id=context.profile_id, embedding=embedding,
|
|
@@ -29,12 +29,14 @@ from __future__ import annotations
|
|
|
29
29
|
|
|
30
30
|
import json
|
|
31
31
|
import logging
|
|
32
|
+
import sqlite3
|
|
32
33
|
from typing import TYPE_CHECKING
|
|
33
34
|
|
|
34
35
|
import numpy as np
|
|
35
36
|
from numpy.typing import NDArray
|
|
36
37
|
|
|
37
38
|
from superlocalmemory.core.config import QuantizationConfig
|
|
39
|
+
from superlocalmemory.storage.embedding_codec import decode_embedding
|
|
38
40
|
|
|
39
41
|
if TYPE_CHECKING:
|
|
40
42
|
from superlocalmemory.math.ebbinghaus import EbbinghausCurve
|
|
@@ -262,7 +264,13 @@ class EAPScheduler:
|
|
|
262
264
|
Tries embedding_metadata -> fact_embeddings (vec0 table).
|
|
263
265
|
Falls back to atomic_facts.embedding JSON column.
|
|
264
266
|
"""
|
|
265
|
-
# Try atomic_facts.embedding (JSON
|
|
267
|
+
# Try atomic_facts.embedding (TEXT JSON or BLOB float32).
|
|
268
|
+
# Two-stage error handling:
|
|
269
|
+
# - DB query failures (any exception from execute) → log + return None.
|
|
270
|
+
# - Format errors from decode_embedding (ValueError) → propagate.
|
|
271
|
+
# A corrupt embedding must NOT be silently collapsed to None because
|
|
272
|
+
# the caller cannot distinguish "embedding absent" from "data lost".
|
|
273
|
+
raw = None
|
|
266
274
|
try:
|
|
267
275
|
rows = self._db.execute(
|
|
268
276
|
"SELECT embedding FROM atomic_facts WHERE fact_id = ?",
|
|
@@ -270,14 +278,17 @@ class EAPScheduler:
|
|
|
270
278
|
)
|
|
271
279
|
if rows:
|
|
272
280
|
raw = dict(rows[0]).get("embedding")
|
|
273
|
-
|
|
274
|
-
data = json.loads(raw) if isinstance(raw, str) else raw
|
|
275
|
-
if data:
|
|
276
|
-
return np.array(data, dtype=np.float64)
|
|
277
|
-
except Exception as exc:
|
|
281
|
+
except Exception as exc: # noqa: BLE001 — isolates DB failure from decode failure
|
|
278
282
|
logger.debug(
|
|
279
283
|
"Could not load embedding from atomic_facts for %s: %s",
|
|
280
284
|
fact_id, exc,
|
|
281
285
|
)
|
|
286
|
+
return None
|
|
287
|
+
|
|
288
|
+
if raw and raw != "null":
|
|
289
|
+
# ValueError from decode_embedding propagates intentionally.
|
|
290
|
+
vec = decode_embedding(raw, fact_id=fact_id)
|
|
291
|
+
if vec:
|
|
292
|
+
return np.array(vec, dtype=np.float64)
|
|
282
293
|
|
|
283
294
|
return None
|
|
@@ -327,6 +327,17 @@ class CognitiveConsolidator:
|
|
|
327
327
|
AND r.lifecycle_zone IN ('warm', 'cold')
|
|
328
328
|
AND r.retention_score < ?
|
|
329
329
|
AND f.lifecycle != 'forgotten'
|
|
330
|
+
-- Withheld rows are not candidates, and this is not tidiness.
|
|
331
|
+
-- 304 of them remain warm/cold in atomic_facts after 4.0.10
|
|
332
|
+
-- withholds them, and each still carries its cluster's POOLED
|
|
333
|
+
-- canonical_entities_json. CCQ clusters on entity overlap, so one
|
|
334
|
+
-- withheld summary naming ten entities joins a cluster of real
|
|
335
|
+
-- memories, contributes model prose to the gist, and then this
|
|
336
|
+
-- pass archives every source in the cluster -- including the real
|
|
337
|
+
-- memories, at scores M043's restore would not bring back.
|
|
338
|
+
-- Exactly the damage this release exists to stop, on a path the
|
|
339
|
+
-- release did not otherwise touch.
|
|
340
|
+
AND COALESCE(f.quarantined, 0) = 0
|
|
330
341
|
AND f.fact_id NOT IN (
|
|
331
342
|
SELECT je.value
|
|
332
343
|
FROM ccq_consolidated_blocks ccb,
|
|
@@ -494,7 +505,10 @@ class CognitiveConsolidator:
|
|
|
494
505
|
f"SELECT fact_id, content, importance, confidence, "
|
|
495
506
|
f" canonical_entities_json "
|
|
496
507
|
f"FROM atomic_facts "
|
|
497
|
-
f"WHERE fact_id IN ({placeholders}) AND profile_id = ?"
|
|
508
|
+
f"WHERE fact_id IN ({placeholders}) AND profile_id = ? "
|
|
509
|
+
# Belt and braces with the identify query above: a cluster assembled
|
|
510
|
+
# before a row was withheld must not contribute its text to a gist.
|
|
511
|
+
f" AND COALESCE(quarantined, 0) = 0",
|
|
498
512
|
(*cluster.fact_ids, profile_id),
|
|
499
513
|
)
|
|
500
514
|
|
|
@@ -9,7 +9,7 @@ last 50 memories; this version searches ALL facts per canonical entity.
|
|
|
9
9
|
|
|
10
10
|
Edge types: ENTITY (shared entity, weight 1.0), TEMPORAL (exp-decay,
|
|
11
11
|
1-week window), SEMANTIC (ANN cosine > 0.7), CAUSAL (causal markers,
|
|
12
|
-
weight 0.8). CONTRADICTION exposed for external Sheaf module (
|
|
12
|
+
weight 0.8). CONTRADICTION exposed for external Sheaf module (added in v3.5).
|
|
13
13
|
|
|
14
14
|
Part of Qualixar | Author: Varun Pratap Bhardwaj
|
|
15
15
|
License: AGPL-3.0-or-later
|
|
@@ -101,7 +101,7 @@ class GraphBuilder:
|
|
|
101
101
|
self, fact_id_a: str, fact_id_b: str, profile_id: str,
|
|
102
102
|
severity: float = 1.0,
|
|
103
103
|
) -> GraphEdge:
|
|
104
|
-
"""Add a contradiction edge. Called by Sheaf module (
|
|
104
|
+
"""Add a contradiction edge. Called by the Sheaf module (introduced in v3.5)."""
|
|
105
105
|
edge = GraphEdge(
|
|
106
106
|
profile_id=profile_id,
|
|
107
107
|
source_id=fact_id_a,
|
|
@@ -15,6 +15,8 @@ Part of Qualixar | Author: Varun Pratap Bhardwaj
|
|
|
15
15
|
from __future__ import annotations
|
|
16
16
|
|
|
17
17
|
import json
|
|
18
|
+
|
|
19
|
+
from superlocalmemory.storage.embedding_codec import decode_embedding
|
|
18
20
|
import logging
|
|
19
21
|
from datetime import UTC, datetime
|
|
20
22
|
|
|
@@ -357,8 +359,12 @@ class SceneBuilder:
|
|
|
357
359
|
embedding = None
|
|
358
360
|
if raw_embedding:
|
|
359
361
|
try:
|
|
360
|
-
embedding =
|
|
361
|
-
|
|
362
|
+
embedding = decode_embedding(
|
|
363
|
+
raw_embedding, fact_id=str(data.get("scene_id", "<scene>")),
|
|
364
|
+
)
|
|
365
|
+
except ValueError:
|
|
366
|
+
# A malformed stored value, not an absent one. Scenes are an
|
|
367
|
+
# optimisation, so one bad row must not sink the batch.
|
|
362
368
|
embedding = None
|
|
363
369
|
result[str(data["scene_id"])] = embedding
|
|
364
370
|
return result
|
|
@@ -4,8 +4,6 @@
|
|
|
4
4
|
|
|
5
5
|
"""Cross-platform adapter base — shared Protocol and atomic write primitive.
|
|
6
6
|
|
|
7
|
-
LLD reference: ``.backup/active-brain/lld/LLD-05-cross-platform-adapters.md``
|
|
8
|
-
Sections: 2 (component catalog), 4.4 (atomic write), 9.3 (sync log contract).
|
|
9
7
|
|
|
10
8
|
Every adapter (Cursor, Antigravity, Copilot) implements the ``Adapter``
|
|
11
9
|
Protocol. The shared ``_atomic_write`` primitive enforces the hard-rule
|
|
@@ -4,8 +4,6 @@
|
|
|
4
4
|
|
|
5
5
|
"""Shared content builder — single source for every adapter body.
|
|
6
6
|
|
|
7
|
-
LLD reference: ``.backup/active-brain/lld/LLD-05-cross-platform-adapters.md``
|
|
8
|
-
Section 3 (Content Builder). One builder → five formatters. Every string is
|
|
9
7
|
passed through ``redact_secrets`` before entering the dataclass, so no
|
|
10
8
|
adapter ever writes an unredacted secret.
|
|
11
9
|
|
|
@@ -2,16 +2,16 @@
|
|
|
2
2
|
# Licensed under AGPL-3.0-or-later - see LICENSE file
|
|
3
3
|
# Part of SuperLocalMemory V3 | https://qualixar.com
|
|
4
4
|
|
|
5
|
-
"""
|
|
5
|
+
"""Portable kit — ``slm connect <ide>`` MCP-wiring.
|
|
6
6
|
|
|
7
7
|
Writes SLM's MCP block into the target IDE config via MERGE-NOT-CLOBBER:
|
|
8
8
|
- Only touches the ``superlocalmemory`` server key.
|
|
9
9
|
- All other servers + top-level keys are preserved byte-for-byte.
|
|
10
10
|
- Atomic write (.tmp + os.replace); aborts on parse error (file untouched).
|
|
11
|
-
- claude-code is OUT: short-circuits to a
|
|
11
|
+
- claude-code is OUT: short-circuits to a plugin pointer, no config written.
|
|
12
12
|
- AGENTS.md is appended with <!-- SLM-START/END --> markers (never overwrite).
|
|
13
13
|
|
|
14
|
-
IDE_MATRIX verified against ide/configs/* templates (read-only
|
|
14
|
+
IDE_MATRIX verified against ide/configs/* templates (read-only; configs are the authoritative source).
|
|
15
15
|
"""
|
|
16
16
|
|
|
17
17
|
from __future__ import annotations
|
|
@@ -39,7 +39,7 @@ SLM_MARKER_END = "<!-- SLM-END -->"
|
|
|
39
39
|
VALID_TRANSPORTS: frozenset[str] = frozenset({"stdio", "http", "http-mcp-remote"})
|
|
40
40
|
|
|
41
41
|
CLAUDE_CODE_PLUGIN_POINTER = (
|
|
42
|
-
"slm connect claude-code: Claude Code is configured via the SLM plugin (
|
|
42
|
+
"slm connect claude-code: Claude Code is configured via the SLM plugin (see plugin/ directory).\n"
|
|
43
43
|
"Run: slm plugin install OR see plugin-src/ for manual installation.\n"
|
|
44
44
|
"No MCP config file is written by this command."
|
|
45
45
|
)
|
|
@@ -206,17 +206,17 @@ IDE_MATRIX: dict[str, IDEDescriptor] = {
|
|
|
206
206
|
},
|
|
207
207
|
caveats="contextProviders is a LIST; append+dedupe by serverName",
|
|
208
208
|
),
|
|
209
|
-
# --- OUT: claude-code defers to
|
|
209
|
+
# --- OUT: claude-code defers to the plugin installer ---
|
|
210
210
|
"claude-code": IDEDescriptor(
|
|
211
211
|
ide_id="claude-code",
|
|
212
|
-
display="Claude Code (
|
|
212
|
+
display="Claude Code (plugin)",
|
|
213
213
|
mcp_path_global="",
|
|
214
214
|
mcp_path_project=None,
|
|
215
215
|
server_key="",
|
|
216
216
|
fmt="",
|
|
217
217
|
agents_md_path=None,
|
|
218
218
|
server_block={},
|
|
219
|
-
caveats="OUT —
|
|
219
|
+
caveats="OUT — plugin pointer only; no MCP config written",
|
|
220
220
|
),
|
|
221
221
|
# --- EXPERIMENTAL (gated, not wired by default) ---
|
|
222
222
|
# chatgpt-desktop, perplexity, cody: gated behind --experimental
|
|
@@ -702,7 +702,7 @@ if __name__ == "__main__": # pragma: no cover
|
|
|
702
702
|
metavar="IDE",
|
|
703
703
|
help=(
|
|
704
704
|
"IDE ids to connect. Pass 'all' to connect every supported IDE "
|
|
705
|
-
"(excluding claude-code which uses the
|
|
705
|
+
"(excluding claude-code which uses the plugin installer)."
|
|
706
706
|
),
|
|
707
707
|
)
|
|
708
708
|
_parser.add_argument("--home", help="Override home directory (test hook).")
|
|
@@ -4,8 +4,6 @@
|
|
|
4
4
|
|
|
5
5
|
"""PostToolUse async:true hook — fire-and-forget prewarm via stdlib urllib.
|
|
6
6
|
|
|
7
|
-
LLD reference: `.backup/active-brain/lld/LLD-01-context-cache-and-hot-path-hooks.md`
|
|
8
|
-
Section 4.4.
|
|
9
7
|
|
|
10
8
|
HARD RULES (enforced by tests):
|
|
11
9
|
- stdlib only — ``urllib.request``, NOT ``httpx`` / ``requests``.
|
|
@@ -4,8 +4,6 @@
|
|
|
4
4
|
|
|
5
5
|
"""Authentication primitives for the /internal/prewarm daemon route.
|
|
6
6
|
|
|
7
|
-
LLD reference: `.backup/active-brain/lld/LLD-01-context-cache-and-hot-path-hooks.md`
|
|
8
|
-
Section 4.5.
|
|
9
7
|
|
|
10
8
|
Four gates, applied in order, BEFORE any engine work:
|
|
11
9
|
1. Loopback-only — client address must be 127.0.0.1 / ::1.
|
|
@@ -4,8 +4,6 @@
|
|
|
4
4
|
|
|
5
5
|
"""UserPromptSubmit hook — Python fallback (compiled binary preferred).
|
|
6
6
|
|
|
7
|
-
LLD reference: `.backup/active-brain/lld/LLD-01-context-cache-and-hot-path-hooks.md`
|
|
8
|
-
Section 4.3.
|
|
9
7
|
|
|
10
8
|
HARD RULES (enforced by tests):
|
|
11
9
|
- stdlib-only imports at module load (SLM modules delayed-imported).
|
|
@@ -16,6 +16,7 @@ import hashlib
|
|
|
16
16
|
import json
|
|
17
17
|
import logging
|
|
18
18
|
import shutil
|
|
19
|
+
import tempfile
|
|
19
20
|
import sqlite3
|
|
20
21
|
import time
|
|
21
22
|
import uuid
|
|
@@ -735,15 +736,50 @@ class BackupManager:
|
|
|
735
736
|
target = self.db_path.parent / target_name
|
|
736
737
|
|
|
737
738
|
try:
|
|
738
|
-
|
|
739
|
+
# Stage the source OUTSIDE backup_dir before anything else runs.
|
|
740
|
+
#
|
|
741
|
+
# create_backup() below calls _enforce_retention(), which globs this
|
|
742
|
+
# same directory and unlinks the oldest files — including, when the
|
|
743
|
+
# backup being restored is the oldest, the source itself. A plain
|
|
744
|
+
# sqlite3.connect() on that now-missing path RECREATES it as an
|
|
745
|
+
# empty database, which is then copied over the live store while the
|
|
746
|
+
# call returns True, leaving a zero-byte file under the original
|
|
747
|
+
# name so a second attempt also appears to succeed.
|
|
748
|
+
#
|
|
749
|
+
# Reproduced before this fix: 11 snapshots, max_backups=10, a
|
|
750
|
+
# 500-fact store restored to 0 tables, restore_backup() -> True.
|
|
751
|
+
with tempfile.TemporaryDirectory(prefix="slm-restore-") as staging:
|
|
752
|
+
staged = Path(staging) / filename
|
|
753
|
+
|
|
754
|
+
# mode=ro fails loudly on a missing file instead of creating one.
|
|
755
|
+
src_ro = sqlite3.connect(f"file:{backup_path}?mode=ro", uri=True)
|
|
756
|
+
try:
|
|
757
|
+
if not [r[0] for r in src_ro.execute(
|
|
758
|
+
"SELECT name FROM sqlite_master WHERE type='table'")]:
|
|
759
|
+
logger.error(
|
|
760
|
+
"Restore rejected: %s contains no tables — refusing to "
|
|
761
|
+
"overwrite %s with an empty database",
|
|
762
|
+
filename, target_name,
|
|
763
|
+
)
|
|
764
|
+
return False
|
|
765
|
+
staged_dst = sqlite3.connect(str(staged))
|
|
766
|
+
try:
|
|
767
|
+
src_ro.backup(staged_dst)
|
|
768
|
+
finally:
|
|
769
|
+
staged_dst.close()
|
|
770
|
+
finally:
|
|
771
|
+
src_ro.close()
|
|
739
772
|
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
src.
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
773
|
+
self.create_backup(label="pre-restore")
|
|
774
|
+
|
|
775
|
+
# Restore from the staged copy, which retention cannot reach.
|
|
776
|
+
src = sqlite3.connect(f"file:{staged}?mode=ro", uri=True)
|
|
777
|
+
dst = sqlite3.connect(str(target))
|
|
778
|
+
try:
|
|
779
|
+
src.backup(dst)
|
|
780
|
+
finally:
|
|
781
|
+
dst.close()
|
|
782
|
+
src.close()
|
|
747
783
|
|
|
748
784
|
logger.info("Restored: %s -> %s", filename, target.name)
|
|
749
785
|
|