superlocalmemory 3.8.5 → 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 +34 -0
- package/README.md +3 -3
- 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/cli/commands.py +139 -404
- package/src/superlocalmemory/core/component_registry.py +4 -2
- package/src/superlocalmemory/core/embeddings.py +33 -6
- package/src/superlocalmemory/core/engine.py +94 -49
- package/src/superlocalmemory/core/engine_ingestion.py +150 -63
- package/src/superlocalmemory/core/ingestion_command.py +133 -21
- package/src/superlocalmemory/core/mutations.py +32 -10
- package/src/superlocalmemory/core/recall_pipeline.py +111 -77
- package/src/superlocalmemory/core/remember_admission.py +152 -0
- package/src/superlocalmemory/core/remember_runtime.py +712 -0
- package/src/superlocalmemory/graph/cozo_backend.py +5 -5
- package/src/superlocalmemory/learning/bandit.py +50 -1
- package/src/superlocalmemory/learning/source_quality.py +38 -35
- package/src/superlocalmemory/mcp/_daemon_proxy.py +38 -15
- 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 +8 -3
- package/src/superlocalmemory/retrieval/reranker.py +35 -10
- package/src/superlocalmemory/server/loopback.py +7 -13
- 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/behavioral.py +5 -13
- package/src/superlocalmemory/server/routes/brain.py +6 -9
- 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 +44 -23
- 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 +122 -100
- package/src/superlocalmemory/server/routes/tiers.py +3 -22
- package/src/superlocalmemory/server/routes/timeline.py +2 -4
- package/src/superlocalmemory/server/routes/v3_api.py +18 -16
- package/src/superlocalmemory/server/unified_daemon.py +200 -109
- package/src/superlocalmemory/storage/admission_codec.py +119 -0
- package/src/superlocalmemory/storage/admission_journal.py +728 -0
- package/src/superlocalmemory/storage/database.py +59 -0
- package/src/superlocalmemory/storage/deferred_writes.py +67 -11
- package/src/superlocalmemory/storage/memory_write.py +8 -12
- package/src/superlocalmemory/storage/migration_runner.py +37 -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/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
|
@@ -265,8 +265,10 @@ def probe_reranker_model(config: Any = None) -> Component:
|
|
|
265
265
|
auto_fixable=enabled,
|
|
266
266
|
fix_cmd="slm doctor --fix",
|
|
267
267
|
)
|
|
268
|
-
if not enabled
|
|
269
|
-
#
|
|
268
|
+
if not enabled:
|
|
269
|
+
# A cached reranker is still inactive when the operator disabled the
|
|
270
|
+
# channel. The dashboard must describe configured runtime state, not
|
|
271
|
+
# machine-specific HuggingFace-cache state.
|
|
270
272
|
return replace(comp, status=STATUS_OK,
|
|
271
273
|
detail="disabled (retrieval.use_cross_encoder=false)",
|
|
272
274
|
fix_cmd="", auto_fixable=False)
|
|
@@ -250,11 +250,38 @@ class EmbeddingService:
|
|
|
250
250
|
def dimension(self) -> int:
|
|
251
251
|
return self._config.dimension
|
|
252
252
|
|
|
253
|
-
def unload(self) ->
|
|
254
|
-
"""
|
|
255
|
-
|
|
253
|
+
def unload(self, timeout: float = 1.0) -> bool:
|
|
254
|
+
"""Release the worker without blocking daemon shutdown on an embed call.
|
|
255
|
+
|
|
256
|
+
An in-flight request owns ``_lock`` while it waits for the worker's
|
|
257
|
+
response. Shutdown must not wait behind a wedged response: callers
|
|
258
|
+
can continue teardown and the worker process will be handled by the
|
|
259
|
+
process supervisor if necessary.
|
|
260
|
+
"""
|
|
261
|
+
if not self._lock.acquire(timeout=max(0.0, timeout)):
|
|
262
|
+
logger.warning("EmbeddingService: unload skipped; embed worker is busy")
|
|
263
|
+
return False
|
|
264
|
+
try:
|
|
256
265
|
self._kill_worker()
|
|
257
266
|
logger.info("EmbeddingService: worker killed (idle timeout)")
|
|
267
|
+
return True
|
|
268
|
+
finally:
|
|
269
|
+
self._lock.release()
|
|
270
|
+
|
|
271
|
+
def shutdown(self, timeout: float = 1.0) -> None:
|
|
272
|
+
"""Force bounded process teardown even when an embed call owns the lock.
|
|
273
|
+
|
|
274
|
+
Shutdown is stronger than the idle-time ``unload`` operation. Once
|
|
275
|
+
the engine is closing, no new request may use this service, so it is
|
|
276
|
+
safe to detach and terminate a wedged child without waiting behind the
|
|
277
|
+
request lock.
|
|
278
|
+
"""
|
|
279
|
+
acquired = self._lock.acquire(timeout=max(0.0, timeout))
|
|
280
|
+
try:
|
|
281
|
+
self._kill_worker(timeout=min(max(0.0, timeout), 1.0))
|
|
282
|
+
finally:
|
|
283
|
+
if acquired:
|
|
284
|
+
self._lock.release()
|
|
258
285
|
|
|
259
286
|
# ------------------------------------------------------------------
|
|
260
287
|
# Public API
|
|
@@ -596,7 +623,7 @@ class EmbeddingService:
|
|
|
596
623
|
self._available = False
|
|
597
624
|
self._worker_proc = None
|
|
598
625
|
|
|
599
|
-
def _kill_worker(self) -> None:
|
|
626
|
+
def _kill_worker(self, timeout: float = 3.0) -> None:
|
|
600
627
|
"""Terminate the worker and close every owned pipe exactly once."""
|
|
601
628
|
if self._idle_timer is not None:
|
|
602
629
|
self._idle_timer.cancel()
|
|
@@ -610,7 +637,7 @@ class EmbeddingService:
|
|
|
610
637
|
try:
|
|
611
638
|
proc.stdin.write('{"cmd":"quit"}\n')
|
|
612
639
|
proc.stdin.flush()
|
|
613
|
-
proc.wait(timeout=
|
|
640
|
+
proc.wait(timeout=max(0.0, timeout))
|
|
614
641
|
except Exception:
|
|
615
642
|
try:
|
|
616
643
|
returncode = proc.poll()
|
|
@@ -621,7 +648,7 @@ class EmbeddingService:
|
|
|
621
648
|
if returncode is None or not isinstance(returncode, int):
|
|
622
649
|
try:
|
|
623
650
|
proc.kill()
|
|
624
|
-
proc.wait(timeout=
|
|
651
|
+
proc.wait(timeout=max(0.0, timeout))
|
|
625
652
|
except Exception:
|
|
626
653
|
pass
|
|
627
654
|
finally:
|
|
@@ -26,7 +26,6 @@ from typing import Any
|
|
|
26
26
|
from superlocalmemory.core.config import CANONICAL_RECALL_LIMIT, SLMConfig
|
|
27
27
|
from superlocalmemory.core.engine_capabilities import Capabilities, CapabilityError
|
|
28
28
|
from superlocalmemory.core.modes import get_capabilities
|
|
29
|
-
from superlocalmemory.learning.outcome_queue import RecallEvent, enqueue_recall
|
|
30
29
|
from superlocalmemory.storage.models import (
|
|
31
30
|
AtomicFact, FactType, MemoryRecord, Mode, RecallResponse,
|
|
32
31
|
)
|
|
@@ -755,44 +754,12 @@ class MemoryEngine:
|
|
|
755
754
|
include_shared=include_shared,
|
|
756
755
|
window=window,
|
|
757
756
|
)
|
|
758
|
-
except Exception
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
757
|
+
except Exception:
|
|
758
|
+
# Diagnostics are intentionally not recorded here. A recall is a
|
|
759
|
+
# read command; diagnostics, outcomes, and implicit feedback must
|
|
760
|
+
# be submitted through explicit write commands.
|
|
762
761
|
raise
|
|
763
762
|
|
|
764
|
-
from superlocalmemory.infra.local_diagnostics import record_recall
|
|
765
|
-
|
|
766
|
-
record_recall(self._db, response, client=agent_id)
|
|
767
|
-
|
|
768
|
-
# S9-DASH-02: enqueue for pending_outcomes. Non-blocking; errors
|
|
769
|
-
# swallowed because signal capture is never load-bearing on
|
|
770
|
-
# recall correctness (LLD-02 §4.9, LLD-08 §4.1).
|
|
771
|
-
if session_id:
|
|
772
|
-
try:
|
|
773
|
-
fact_ids = tuple(
|
|
774
|
-
getattr(r.fact, "fact_id", "") or ""
|
|
775
|
-
for r in getattr(response, "results", [])
|
|
776
|
-
if getattr(r, "fact", None) is not None
|
|
777
|
-
)
|
|
778
|
-
fact_ids = tuple(f for f in fact_ids if f)
|
|
779
|
-
if fact_ids:
|
|
780
|
-
enqueue_recall(RecallEvent(
|
|
781
|
-
session_id=session_id,
|
|
782
|
-
profile_id=pid,
|
|
783
|
-
query=query,
|
|
784
|
-
fact_ids=fact_ids,
|
|
785
|
-
query_id=getattr(response, "query_id", "") or "",
|
|
786
|
-
))
|
|
787
|
-
except Exception as _outcome_exc:
|
|
788
|
-
# Engagement-signal enqueue is non-blocking; recall
|
|
789
|
-
# correctness does not depend on it. Log so the failure
|
|
790
|
-
# is visible instead of silently losing learning signals.
|
|
791
|
-
logger.warning(
|
|
792
|
-
"outcome-queue enqueue failed (engagement signal lost): %s",
|
|
793
|
-
_outcome_exc,
|
|
794
|
-
)
|
|
795
|
-
|
|
796
763
|
return response
|
|
797
764
|
|
|
798
765
|
# -- Session operations -------------------------------------------------
|
|
@@ -820,25 +787,103 @@ class MemoryEngine:
|
|
|
820
787
|
# -- Lifecycle ----------------------------------------------------------
|
|
821
788
|
|
|
822
789
|
def close(self) -> None:
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
790
|
+
"""Release engine-owned resources without waiting for model workers.
|
|
791
|
+
|
|
792
|
+
Daemon shutdown must be a bounded operation. In particular, a
|
|
793
|
+
``store_fast`` embed submitted just before shutdown may be blocked in a
|
|
794
|
+
model runtime forever; waiting for its executor here used to make the
|
|
795
|
+
service manager SIGKILL the daemon and leave its children behind.
|
|
796
|
+
References are cleared before invoking each cleanup hook, so a second
|
|
797
|
+
close is safe even when one optional cleanup hook fails.
|
|
798
|
+
"""
|
|
799
|
+
scheduler = getattr(self, "_maintenance_scheduler", None)
|
|
800
|
+
self._maintenance_scheduler = None
|
|
801
|
+
if scheduler is not None:
|
|
826
802
|
try:
|
|
827
|
-
|
|
803
|
+
scheduler.stop()
|
|
828
804
|
except Exception:
|
|
829
|
-
|
|
830
|
-
|
|
805
|
+
logger.warning("engine cleanup: maintenance scheduler stop failed", exc_info=True)
|
|
806
|
+
|
|
807
|
+
embed_pool = getattr(self, "_store_fast_embed_pool", None)
|
|
808
|
+
embed_pool_lock = getattr(self, "_store_fast_embed_pool_lock", None)
|
|
809
|
+
if embed_pool_lock is not None:
|
|
810
|
+
with embed_pool_lock:
|
|
811
|
+
embed_pool, self._store_fast_embed_pool = self._store_fast_embed_pool, None
|
|
812
|
+
else:
|
|
813
|
+
self._store_fast_embed_pool = None
|
|
814
|
+
if embed_pool is not None:
|
|
831
815
|
try:
|
|
832
|
-
|
|
833
|
-
|
|
816
|
+
embed_pool.shutdown(wait=False, cancel_futures=True)
|
|
817
|
+
except Exception:
|
|
818
|
+
logger.warning(
|
|
819
|
+
"engine cleanup: store-fast embed pool shutdown failed",
|
|
820
|
+
exc_info=True,
|
|
834
821
|
)
|
|
835
|
-
|
|
822
|
+
|
|
823
|
+
retrieval = getattr(self, "_retrieval_engine", None)
|
|
824
|
+
self._retrieval_engine = None
|
|
825
|
+
if retrieval is not None:
|
|
826
|
+
reranker = getattr(retrieval, "_reranker", None)
|
|
827
|
+
try:
|
|
828
|
+
if reranker is not None:
|
|
829
|
+
shutdown = getattr(reranker, "shutdown", None)
|
|
830
|
+
if callable(shutdown):
|
|
831
|
+
shutdown(timeout=1.0)
|
|
832
|
+
else:
|
|
833
|
+
unload = getattr(reranker, "unload", None)
|
|
834
|
+
if callable(unload):
|
|
835
|
+
unload()
|
|
836
836
|
except Exception:
|
|
837
|
-
|
|
837
|
+
logger.warning("engine cleanup: reranker shutdown failed", exc_info=True)
|
|
838
838
|
try:
|
|
839
|
-
|
|
839
|
+
retrieval.close(wait=False)
|
|
840
|
+
except TypeError:
|
|
841
|
+
# Compatibility for plugins with the legacy no-argument
|
|
842
|
+
# close hook. Built-in RetrievalEngine accepts ``wait``.
|
|
843
|
+
try:
|
|
844
|
+
retrieval.close()
|
|
845
|
+
except Exception:
|
|
846
|
+
logger.warning(
|
|
847
|
+
"engine cleanup: legacy retrieval shutdown failed",
|
|
848
|
+
exc_info=True,
|
|
849
|
+
)
|
|
840
850
|
except Exception:
|
|
841
|
-
|
|
851
|
+
logger.warning("engine cleanup: retrieval executor shutdown failed", exc_info=True)
|
|
852
|
+
|
|
853
|
+
embedder = getattr(self, "_embedder", None)
|
|
854
|
+
self._embedder = None
|
|
855
|
+
if embedder is not None:
|
|
856
|
+
try:
|
|
857
|
+
shutdown = getattr(embedder, "shutdown", None)
|
|
858
|
+
if callable(shutdown):
|
|
859
|
+
shutdown(timeout=1.0)
|
|
860
|
+
else:
|
|
861
|
+
unload = getattr(embedder, "unload", None)
|
|
862
|
+
if not callable(unload):
|
|
863
|
+
unload = None
|
|
864
|
+
if not callable(shutdown) and callable(unload):
|
|
865
|
+
try:
|
|
866
|
+
unload(timeout=1.0)
|
|
867
|
+
except TypeError:
|
|
868
|
+
# Ollama and third-party embedders may still expose
|
|
869
|
+
# the legacy no-argument unload hook.
|
|
870
|
+
unload()
|
|
871
|
+
except Exception:
|
|
872
|
+
logger.warning("engine cleanup: embedder unload failed", exc_info=True)
|
|
873
|
+
|
|
874
|
+
db = getattr(self, "_db", None)
|
|
875
|
+
self._db = None
|
|
876
|
+
if db is not None:
|
|
877
|
+
try:
|
|
878
|
+
from superlocalmemory.core.recall_pipeline import release_recall_resources
|
|
879
|
+
|
|
880
|
+
release_recall_resources(db)
|
|
881
|
+
except Exception:
|
|
882
|
+
logger.warning("engine cleanup: recall resources release failed", exc_info=True)
|
|
883
|
+
try:
|
|
884
|
+
db.close()
|
|
885
|
+
except Exception:
|
|
886
|
+
logger.warning("engine cleanup: database close failed", exc_info=True)
|
|
842
887
|
self._initialized = False
|
|
843
888
|
|
|
844
889
|
@property
|
|
@@ -14,7 +14,7 @@ import hashlib
|
|
|
14
14
|
import logging
|
|
15
15
|
import os
|
|
16
16
|
import uuid
|
|
17
|
-
from typing import TYPE_CHECKING
|
|
17
|
+
from typing import TYPE_CHECKING, Protocol
|
|
18
18
|
|
|
19
19
|
from superlocalmemory.core.ingestion_command import (
|
|
20
20
|
IngestionCommand,
|
|
@@ -26,7 +26,7 @@ from superlocalmemory.core.ingestion_command import (
|
|
|
26
26
|
|
|
27
27
|
if TYPE_CHECKING:
|
|
28
28
|
from superlocalmemory.core.engine import MemoryEngine
|
|
29
|
-
from superlocalmemory.storage.models import AtomicFact
|
|
29
|
+
from superlocalmemory.storage.models import AtomicFact, MemoryRecord
|
|
30
30
|
|
|
31
31
|
|
|
32
32
|
logger = logging.getLogger(__name__)
|
|
@@ -36,6 +36,22 @@ _PREBUILT_FACT_KEY = "_slm_prebuilt_fact_v1"
|
|
|
36
36
|
_DERIVATION_VERSION = "v3.7-ingestion-1"
|
|
37
37
|
|
|
38
38
|
|
|
39
|
+
class _ImmediateAdmissionDatabase(Protocol):
|
|
40
|
+
"""The deliberately tiny persistence surface used by receipt admission.
|
|
41
|
+
|
|
42
|
+
The coordinator will bind this to its sole writer connection. Keeping the
|
|
43
|
+
seam this narrow prevents the immediate path from accidentally acquiring a
|
|
44
|
+
model, hook, graph, or secondary-store dependency while it owns SQLite's
|
|
45
|
+
write transaction.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
def store_memory(self, record: "MemoryRecord") -> str: ...
|
|
49
|
+
|
|
50
|
+
def store_fact(self, fact: "AtomicFact") -> str: ...
|
|
51
|
+
|
|
52
|
+
def execute(self, sql: str, params: tuple = ()) -> list: ...
|
|
53
|
+
|
|
54
|
+
|
|
39
55
|
def _pii_redaction_enabled(engine: "MemoryEngine") -> bool:
|
|
40
56
|
"""C4: opt-in PII redaction on ingest.
|
|
41
57
|
|
|
@@ -106,6 +122,124 @@ def _prebuilt_fact_from_payload(payload: dict):
|
|
|
106
122
|
return AtomicFact(**values)
|
|
107
123
|
|
|
108
124
|
|
|
125
|
+
def build_immediate_admission_handler(
|
|
126
|
+
db: _ImmediateAdmissionDatabase,
|
|
127
|
+
*,
|
|
128
|
+
profile_id: str,
|
|
129
|
+
max_verbatim_chars: int = 24_000,
|
|
130
|
+
max_ingest_bytes: int = 1_048_576,
|
|
131
|
+
):
|
|
132
|
+
"""Build the deterministic queryable projection for one engine profile.
|
|
133
|
+
|
|
134
|
+
``IngestionCommand`` wraps this callback with operation creation and the
|
|
135
|
+
RAW -> QUERYABLE receipt transition in one transaction. This function is
|
|
136
|
+
intentionally restricted to constructing a raw ``MemoryRecord`` and one
|
|
137
|
+
embedding-free ``AtomicFact`` then persisting those two records. All
|
|
138
|
+
authorization hooks, embedding, extraction, FTS-adjacent enrichment,
|
|
139
|
+
graph, provenance, and external index work run only after this receipt has
|
|
140
|
+
committed through the durable materializer.
|
|
141
|
+
"""
|
|
142
|
+
def write_queryable(request: IngestionRequest, operation_id: str) -> list[str]:
|
|
143
|
+
if request.profile_id != profile_id:
|
|
144
|
+
raise ValueError("ingestion request profile does not match engine")
|
|
145
|
+
if not request.trusted_actor_id:
|
|
146
|
+
raise ValueError("trusted actor identity is required")
|
|
147
|
+
if not content_passes_admission(request.content):
|
|
148
|
+
return []
|
|
149
|
+
|
|
150
|
+
import re
|
|
151
|
+
from datetime import UTC, datetime
|
|
152
|
+
|
|
153
|
+
from superlocalmemory.core.ingest_gate import apply_ingest_gate
|
|
154
|
+
from superlocalmemory.storage.models import AtomicFact, FactType, MemoryRecord
|
|
155
|
+
|
|
156
|
+
metadata = dict(request.metadata)
|
|
157
|
+
metadata["ingestion_operation_id"] = operation_id
|
|
158
|
+
if request.session_id:
|
|
159
|
+
metadata.setdefault("session_id", request.session_id)
|
|
160
|
+
gate = apply_ingest_gate(
|
|
161
|
+
request.content,
|
|
162
|
+
max_verbatim_chars=max_verbatim_chars,
|
|
163
|
+
max_ingest_bytes=max_ingest_bytes,
|
|
164
|
+
)
|
|
165
|
+
if gate.rejected:
|
|
166
|
+
return []
|
|
167
|
+
fact_content = gate.fact_content
|
|
168
|
+
now = datetime.now(UTC).isoformat()
|
|
169
|
+
observation_date = request.session_date or now[:10]
|
|
170
|
+
|
|
171
|
+
prebuilt_payload = metadata.get(_PREBUILT_FACT_KEY)
|
|
172
|
+
if isinstance(prebuilt_payload, dict):
|
|
173
|
+
fact = _prebuilt_fact_from_payload(prebuilt_payload)
|
|
174
|
+
fact.profile_id = request.profile_id
|
|
175
|
+
fact.scope = request.scope
|
|
176
|
+
fact.shared_with = list(request.shared_with) or None
|
|
177
|
+
fact.session_id = request.session_id or fact.session_id
|
|
178
|
+
if request.session_date:
|
|
179
|
+
fact.observation_date = request.session_date
|
|
180
|
+
else:
|
|
181
|
+
entities = sorted(
|
|
182
|
+
{match.group(1) for match in re.finditer(
|
|
183
|
+
r"\b([A-Z][a-z]+(?:\s[A-Z][a-z]+){0,3})\b", fact_content,
|
|
184
|
+
)}
|
|
185
|
+
| {match.group(1) for match in re.finditer(
|
|
186
|
+
r"\b([A-Z]{2,})\b", fact_content,
|
|
187
|
+
)}
|
|
188
|
+
)
|
|
189
|
+
fact = AtomicFact(
|
|
190
|
+
fact_id=uuid.uuid4().hex[:16],
|
|
191
|
+
profile_id=request.profile_id,
|
|
192
|
+
scope=request.scope,
|
|
193
|
+
shared_with=list(request.shared_with) or None,
|
|
194
|
+
content=fact_content,
|
|
195
|
+
fact_type=FactType.EPISODIC,
|
|
196
|
+
entities=entities,
|
|
197
|
+
observation_date=observation_date,
|
|
198
|
+
session_id=request.session_id,
|
|
199
|
+
confidence=0.7,
|
|
200
|
+
importance=0.5,
|
|
201
|
+
created_at=now,
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
# A receipt is queryable through FTS immediately, but model-derived
|
|
205
|
+
# values cannot participate in its write transaction. The materializer
|
|
206
|
+
# owns their later, retryable population.
|
|
207
|
+
fact.embedding = None
|
|
208
|
+
fact.fisher_mean = None
|
|
209
|
+
fact.fisher_variance = None
|
|
210
|
+
memory_id = fact.memory_id or uuid.uuid4().hex
|
|
211
|
+
existing = db.execute(
|
|
212
|
+
"SELECT profile_id FROM memories WHERE memory_id = ?",
|
|
213
|
+
(memory_id,),
|
|
214
|
+
)
|
|
215
|
+
if existing:
|
|
216
|
+
existing_profile = str(existing[0]["profile_id"])
|
|
217
|
+
if existing_profile != request.profile_id:
|
|
218
|
+
raise ValueError("prebuilt fact memory belongs to a different profile")
|
|
219
|
+
# Reuse the existing source row. DatabaseManager.store_memory uses
|
|
220
|
+
# INSERT OR REPLACE; calling it here would delete every child fact
|
|
221
|
+
# through the memory_id foreign-key cascade before recreating the
|
|
222
|
+
# parent.
|
|
223
|
+
fact.memory_id = memory_id
|
|
224
|
+
else:
|
|
225
|
+
record = MemoryRecord(
|
|
226
|
+
memory_id=memory_id,
|
|
227
|
+
profile_id=request.profile_id,
|
|
228
|
+
content=request.content,
|
|
229
|
+
session_id=request.session_id,
|
|
230
|
+
session_date=observation_date,
|
|
231
|
+
speaker=request.speaker,
|
|
232
|
+
role=request.role,
|
|
233
|
+
metadata=metadata,
|
|
234
|
+
scope=request.scope,
|
|
235
|
+
shared_with=list(request.shared_with) or None,
|
|
236
|
+
)
|
|
237
|
+
fact.memory_id = db.store_memory(record)
|
|
238
|
+
return [db.store_fact(fact)]
|
|
239
|
+
|
|
240
|
+
return write_queryable
|
|
241
|
+
|
|
242
|
+
|
|
109
243
|
def local_trusted_actor_id(actor_kind: str) -> str:
|
|
110
244
|
"""Derive a stable local actor from the private install capability."""
|
|
111
245
|
from superlocalmemory.core.security_primitives import ensure_install_token
|
|
@@ -283,71 +417,22 @@ def build_engine_ingestion_command(engine: MemoryEngine) -> IngestionCommand:
|
|
|
283
417
|
engine._require_full("canonical_ingestion")
|
|
284
418
|
engine._ensure_init()
|
|
285
419
|
repository = IngestionOperationRepository(engine._db)
|
|
420
|
+
store_config = getattr(engine._config, "store", None)
|
|
421
|
+
write_queryable = build_immediate_admission_handler(
|
|
422
|
+
engine._db,
|
|
423
|
+
profile_id=engine._profile_id,
|
|
424
|
+
max_verbatim_chars=getattr(store_config, "max_verbatim_chars", 24_000),
|
|
425
|
+
max_ingest_bytes=getattr(store_config, "max_ingest_bytes", 1_048_576),
|
|
426
|
+
)
|
|
286
427
|
|
|
287
|
-
def
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
if not request.trusted_actor_id:
|
|
291
|
-
raise ValueError("trusted actor identity is required")
|
|
292
|
-
hook_context = {
|
|
428
|
+
def validate_admission(request: IngestionRequest) -> None:
|
|
429
|
+
"""Apply trust policy before the journal or canonical transaction."""
|
|
430
|
+
engine._hooks.run_pre("store", {
|
|
293
431
|
"operation": "store",
|
|
294
|
-
"agent_id": request.trusted_actor_id
|
|
432
|
+
"agent_id": request.trusted_actor_id,
|
|
295
433
|
"profile_id": request.profile_id,
|
|
296
434
|
"content_preview": request.content[:100],
|
|
297
|
-
|
|
298
|
-
}
|
|
299
|
-
# Authorization and trust policy must run before raw evidence reaches
|
|
300
|
-
# durable storage. Materialization reuses this authorization decision.
|
|
301
|
-
engine._hooks.run_pre("store", hook_context)
|
|
302
|
-
metadata = dict(request.metadata)
|
|
303
|
-
metadata["ingestion_operation_id"] = operation_id
|
|
304
|
-
if request.session_id:
|
|
305
|
-
metadata.setdefault("session_id", request.session_id)
|
|
306
|
-
prebuilt_payload = metadata.get(_PREBUILT_FACT_KEY)
|
|
307
|
-
if isinstance(prebuilt_payload, dict):
|
|
308
|
-
from superlocalmemory.storage.models import MemoryRecord
|
|
309
|
-
|
|
310
|
-
fact = _prebuilt_fact_from_payload(prebuilt_payload)
|
|
311
|
-
fact.profile_id = request.profile_id
|
|
312
|
-
fact.scope = request.scope
|
|
313
|
-
fact.shared_with = list(request.shared_with) or None
|
|
314
|
-
fact.session_id = request.session_id or fact.session_id
|
|
315
|
-
if request.session_date:
|
|
316
|
-
fact.observation_date = request.session_date
|
|
317
|
-
memory_id = fact.memory_id
|
|
318
|
-
memory_rows = (
|
|
319
|
-
engine._db.execute(
|
|
320
|
-
"SELECT memory_id FROM memories WHERE memory_id=? AND profile_id=?",
|
|
321
|
-
(memory_id, request.profile_id),
|
|
322
|
-
)
|
|
323
|
-
if memory_id else []
|
|
324
|
-
)
|
|
325
|
-
if not memory_rows:
|
|
326
|
-
record = MemoryRecord(
|
|
327
|
-
memory_id=memory_id or uuid.uuid4().hex,
|
|
328
|
-
profile_id=request.profile_id,
|
|
329
|
-
content=request.content,
|
|
330
|
-
session_id=request.session_id,
|
|
331
|
-
session_date=request.session_date,
|
|
332
|
-
metadata=metadata,
|
|
333
|
-
scope=request.scope,
|
|
334
|
-
shared_with=list(request.shared_with) or None,
|
|
335
|
-
)
|
|
336
|
-
engine._db.store_memory(record)
|
|
337
|
-
memory_id = record.memory_id
|
|
338
|
-
fact.memory_id = memory_id
|
|
339
|
-
engine._db.store_fact(fact)
|
|
340
|
-
return [fact.fact_id]
|
|
341
|
-
return engine.store_fast(
|
|
342
|
-
request.content,
|
|
343
|
-
metadata=metadata,
|
|
344
|
-
scope=request.scope,
|
|
345
|
-
shared_with=list(request.shared_with) or None,
|
|
346
|
-
session_date=request.session_date or None,
|
|
347
|
-
speaker=request.speaker,
|
|
348
|
-
role=request.role,
|
|
349
|
-
index_external=False,
|
|
350
|
-
)
|
|
435
|
+
})
|
|
351
436
|
|
|
352
437
|
def resume_checkpoint(operation: IngestionOperation) -> MaterializationResult:
|
|
353
438
|
"""Repair only stages whose writes have an idempotent natural key."""
|
|
@@ -816,12 +901,14 @@ def build_engine_ingestion_command(engine: MemoryEngine) -> IngestionCommand:
|
|
|
816
901
|
repository,
|
|
817
902
|
write_queryable=write_queryable,
|
|
818
903
|
materialize=materialize,
|
|
904
|
+
validate_admission=validate_admission,
|
|
819
905
|
project=project,
|
|
820
906
|
derivation_version=_DERIVATION_VERSION,
|
|
821
907
|
)
|
|
822
908
|
|
|
823
909
|
|
|
824
910
|
__all__ = [
|
|
911
|
+
"build_immediate_admission_handler",
|
|
825
912
|
"build_engine_ingestion_command",
|
|
826
913
|
"canonical_store",
|
|
827
914
|
"canonical_store_fn",
|