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
|
@@ -18,15 +18,16 @@ Part of Qualixar | Author: Varun Pratap Bhardwaj
|
|
|
18
18
|
from __future__ import annotations
|
|
19
19
|
|
|
20
20
|
import logging
|
|
21
|
+
import os
|
|
22
|
+
import threading
|
|
21
23
|
from pathlib import Path
|
|
22
24
|
from typing import Any
|
|
23
25
|
|
|
24
26
|
from superlocalmemory.core.config import CANONICAL_RECALL_LIMIT, SLMConfig
|
|
25
27
|
from superlocalmemory.core.engine_capabilities import Capabilities, CapabilityError
|
|
26
28
|
from superlocalmemory.core.modes import get_capabilities
|
|
27
|
-
from superlocalmemory.learning.outcome_queue import RecallEvent, enqueue_recall
|
|
28
29
|
from superlocalmemory.storage.models import (
|
|
29
|
-
AtomicFact, MemoryRecord, Mode, RecallResponse,
|
|
30
|
+
AtomicFact, FactType, MemoryRecord, Mode, RecallResponse,
|
|
30
31
|
)
|
|
31
32
|
|
|
32
33
|
logger = logging.getLogger(__name__)
|
|
@@ -47,6 +48,34 @@ def _verify_ingestion_schema(memory_db: Path) -> bool:
|
|
|
47
48
|
connection.close()
|
|
48
49
|
|
|
49
50
|
|
|
51
|
+
# ---------------------------------------------------------------------------
|
|
52
|
+
# Workstream D (3.8.4) — warm-guard sync embed helpers
|
|
53
|
+
# ---------------------------------------------------------------------------
|
|
54
|
+
|
|
55
|
+
def _is_remote_embedder(embedder: object) -> bool:
|
|
56
|
+
"""Return True if *embedder* makes remote HTTP calls (cloud / OpenAI-compatible).
|
|
57
|
+
|
|
58
|
+
Remote embedders (100–400ms round-trip) must never block the store_fast()
|
|
59
|
+
write path synchronously — they stay on the async materializer path.
|
|
60
|
+
|
|
61
|
+
Local embedders:
|
|
62
|
+
- EmbeddingService (subprocess, local ONNX/sentence-transformers): has
|
|
63
|
+
``_config`` with ``is_cloud=False`` and ``is_openai_compatible=False``.
|
|
64
|
+
- OllamaEmbedder (localhost HTTP, ~73ms): has NO ``_config`` attribute.
|
|
65
|
+
|
|
66
|
+
Remote embedders:
|
|
67
|
+
- EmbeddingService with ``_config.is_cloud=True`` (Azure, etc.)
|
|
68
|
+
- EmbeddingService with ``_config.is_openai_compatible=True``
|
|
69
|
+
"""
|
|
70
|
+
cfg = getattr(embedder, "_config", None)
|
|
71
|
+
if cfg is None:
|
|
72
|
+
return False # OllamaEmbedder — local
|
|
73
|
+
return bool(
|
|
74
|
+
getattr(cfg, "is_cloud", False)
|
|
75
|
+
or getattr(cfg, "is_openai_compatible", False)
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
50
79
|
class MemoryEngine:
|
|
51
80
|
"""Main orchestrator for the SuperLocalMemory V3 memory system.
|
|
52
81
|
|
|
@@ -103,6 +132,11 @@ class MemoryEngine:
|
|
|
103
132
|
self._consolidation_engine = None
|
|
104
133
|
self._maintenance_scheduler = None
|
|
105
134
|
self._hooks = HookRegistry()
|
|
135
|
+
# Workstream D (3.8.4): single-worker pool reused across store_fast() calls.
|
|
136
|
+
# Lazy-created on first warm-guard attempt; avoids per-call thread churn.
|
|
137
|
+
self._store_fast_embed_pool: object | None = None
|
|
138
|
+
# Lock guards the lazy-init to prevent TOCTOU race on concurrent first calls.
|
|
139
|
+
self._store_fast_embed_pool_lock = threading.Lock()
|
|
106
140
|
|
|
107
141
|
# -- Public properties (Phase 2+ access) --------------------------------
|
|
108
142
|
|
|
@@ -525,9 +559,6 @@ class MemoryEngine:
|
|
|
525
559
|
import re as _re
|
|
526
560
|
import uuid as _uuid
|
|
527
561
|
from datetime import datetime, timezone
|
|
528
|
-
from superlocalmemory.storage.models import (
|
|
529
|
-
AtomicFact, FactType, MemoryRecord,
|
|
530
|
-
)
|
|
531
562
|
from superlocalmemory.core.engine_ingestion import content_passes_admission
|
|
532
563
|
if not content_passes_admission(content):
|
|
533
564
|
return []
|
|
@@ -568,15 +599,64 @@ class MemoryEngine:
|
|
|
568
599
|
r"\b([A-Z][a-z]+(?:\s[A-Z][a-z]+){0,3})\b", fact_text)}
|
|
569
600
|
| {m.group(1) for m in _re.finditer(r"\b([A-Z]{2,})\b", fact_text)}
|
|
570
601
|
)
|
|
571
|
-
#
|
|
572
|
-
#
|
|
573
|
-
#
|
|
574
|
-
#
|
|
575
|
-
#
|
|
576
|
-
#
|
|
577
|
-
#
|
|
602
|
+
# Workstream D (3.8.4) — warm-guard synchronous embed.
|
|
603
|
+
#
|
|
604
|
+
# Original contract (3.8.2): queryable admission NEVER acquires the
|
|
605
|
+
# embedding worker lock. On a clean Mode-A install the background model
|
|
606
|
+
# load owns that lock for up to 180s, turning the receipt-first path into
|
|
607
|
+
# a hidden synchronous wait. The canonical materializer promotes this
|
|
608
|
+
# fact with its full pipeline (embedding, Fisher, entities, graph edges).
|
|
609
|
+
#
|
|
610
|
+
# 3.8.4 extension: when the embedder is PROVABLY warm (_available is True)
|
|
611
|
+
# AND is a local embedder (not a remote cloud/OpenAI endpoint), compute the
|
|
612
|
+
# embedding synchronously with a hard 500ms cap. On timeout or any
|
|
613
|
+
# exception, fall through to emb=None — the materializer fills it async.
|
|
614
|
+
# This preserves the 3.8.2 invariant for cold start while eliminating the
|
|
615
|
+
# semantic-channel blind spot on warm daemons (the top UX complaint).
|
|
578
616
|
emb = None
|
|
579
617
|
fmean = fvar = None
|
|
618
|
+
_embedder_ref = self._embedder
|
|
619
|
+
if (
|
|
620
|
+
_embedder_ref is not None
|
|
621
|
+
and getattr(_embedder_ref, "_available", None) is True
|
|
622
|
+
and not _is_remote_embedder(_embedder_ref)
|
|
623
|
+
):
|
|
624
|
+
import concurrent.futures as _cf
|
|
625
|
+
# Lazy-init the pool once per engine instance — avoids per-call
|
|
626
|
+
# thread churn and the associated resource leak from discard-on-exit.
|
|
627
|
+
# Double-checked locking guards against TOCTOU on concurrent first calls.
|
|
628
|
+
if self._store_fast_embed_pool is None:
|
|
629
|
+
with self._store_fast_embed_pool_lock:
|
|
630
|
+
if self._store_fast_embed_pool is None:
|
|
631
|
+
self._store_fast_embed_pool = _cf.ThreadPoolExecutor(
|
|
632
|
+
max_workers=1,
|
|
633
|
+
thread_name_prefix="slm-sg-embed",
|
|
634
|
+
)
|
|
635
|
+
try:
|
|
636
|
+
_timeout_s = int(os.environ.get("SLM_STORE_FAST_EMBED_TIMEOUT_MS", 500)) / 1000.0
|
|
637
|
+
except (ValueError, TypeError):
|
|
638
|
+
_timeout_s = 0.5 # default 500 ms
|
|
639
|
+
try:
|
|
640
|
+
_future = self._store_fast_embed_pool.submit(_embedder_ref.embed, fact_text)
|
|
641
|
+
try:
|
|
642
|
+
emb = _future.result(timeout=_timeout_s)
|
|
643
|
+
if emb:
|
|
644
|
+
fmean, fvar = _embedder_ref.compute_fisher_params(emb)
|
|
645
|
+
except _cf.TimeoutError:
|
|
646
|
+
logger.debug(
|
|
647
|
+
"store_fast: warm-guard embed timed out (>%.0fms) — deferring to materializer",
|
|
648
|
+
_timeout_s * 1000,
|
|
649
|
+
)
|
|
650
|
+
emb = None
|
|
651
|
+
except Exception as _exc:
|
|
652
|
+
logger.debug(
|
|
653
|
+
"store_fast: warm-guard embed failed (%s) — deferring to materializer",
|
|
654
|
+
_exc,
|
|
655
|
+
)
|
|
656
|
+
emb = None
|
|
657
|
+
except Exception as _exc:
|
|
658
|
+
logger.debug("store_fast: warm-guard pool submit failed (%s)", _exc)
|
|
659
|
+
emb = None
|
|
580
660
|
fact = AtomicFact(
|
|
581
661
|
fact_id=_uuid.uuid4().hex[:16], memory_id=record.memory_id,
|
|
582
662
|
profile_id=self._profile_id, content=fact_text,
|
|
@@ -674,44 +754,12 @@ class MemoryEngine:
|
|
|
674
754
|
include_shared=include_shared,
|
|
675
755
|
window=window,
|
|
676
756
|
)
|
|
677
|
-
except Exception
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
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.
|
|
681
761
|
raise
|
|
682
762
|
|
|
683
|
-
from superlocalmemory.infra.local_diagnostics import record_recall
|
|
684
|
-
|
|
685
|
-
record_recall(self._db, response, client=agent_id)
|
|
686
|
-
|
|
687
|
-
# S9-DASH-02: enqueue for pending_outcomes. Non-blocking; errors
|
|
688
|
-
# swallowed because signal capture is never load-bearing on
|
|
689
|
-
# recall correctness (LLD-02 §4.9, LLD-08 §4.1).
|
|
690
|
-
if session_id:
|
|
691
|
-
try:
|
|
692
|
-
fact_ids = tuple(
|
|
693
|
-
getattr(r.fact, "fact_id", "") or ""
|
|
694
|
-
for r in getattr(response, "results", [])
|
|
695
|
-
if getattr(r, "fact", None) is not None
|
|
696
|
-
)
|
|
697
|
-
fact_ids = tuple(f for f in fact_ids if f)
|
|
698
|
-
if fact_ids:
|
|
699
|
-
enqueue_recall(RecallEvent(
|
|
700
|
-
session_id=session_id,
|
|
701
|
-
profile_id=pid,
|
|
702
|
-
query=query,
|
|
703
|
-
fact_ids=fact_ids,
|
|
704
|
-
query_id=getattr(response, "query_id", "") or "",
|
|
705
|
-
))
|
|
706
|
-
except Exception as _outcome_exc:
|
|
707
|
-
# Engagement-signal enqueue is non-blocking; recall
|
|
708
|
-
# correctness does not depend on it. Log so the failure
|
|
709
|
-
# is visible instead of silently losing learning signals.
|
|
710
|
-
logger.warning(
|
|
711
|
-
"outcome-queue enqueue failed (engagement signal lost): %s",
|
|
712
|
-
_outcome_exc,
|
|
713
|
-
)
|
|
714
|
-
|
|
715
763
|
return response
|
|
716
764
|
|
|
717
765
|
# -- Session operations -------------------------------------------------
|
|
@@ -739,25 +787,103 @@ class MemoryEngine:
|
|
|
739
787
|
# -- Lifecycle ----------------------------------------------------------
|
|
740
788
|
|
|
741
789
|
def close(self) -> None:
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
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:
|
|
745
802
|
try:
|
|
746
|
-
|
|
803
|
+
scheduler.stop()
|
|
747
804
|
except Exception:
|
|
748
|
-
|
|
749
|
-
|
|
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:
|
|
750
815
|
try:
|
|
751
|
-
|
|
752
|
-
|
|
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,
|
|
753
821
|
)
|
|
754
|
-
|
|
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()
|
|
755
836
|
except Exception:
|
|
756
|
-
|
|
837
|
+
logger.warning("engine cleanup: reranker shutdown failed", exc_info=True)
|
|
757
838
|
try:
|
|
758
|
-
|
|
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
|
+
)
|
|
759
850
|
except Exception:
|
|
760
|
-
|
|
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)
|
|
761
887
|
self._initialized = False
|
|
762
888
|
|
|
763
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",
|