superlocalmemory 3.8.13 → 4.0.0
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/ATTRIBUTION.md +4 -4
- package/CHANGELOG.md +113 -121
- package/README.md +65 -63
- package/docs/pi-dev-integration.md +1 -1
- package/package.json +6 -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-governance/SKILL.md +248 -0
- package/plugin-src/skills/slm-graph/SKILL.md +1 -1
- package/plugin-src/skills/slm-loop/SKILL.md +99 -0
- package/plugin-src/skills/slm-mesh/SKILL.md +282 -0
- package/plugin-src/skills/slm-profile/SKILL.md +148 -0
- 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-scope/SKILL.md +176 -0
- package/plugin-src/skills/slm-session/SKILL.md +1 -1
- package/plugin-src/skills/slm-status/SKILL.md +1 -1
- package/pyproject.toml +11 -4
- package/src/superlocalmemory/__init__.py +1 -1
- package/src/superlocalmemory/cli/commands.py +125 -11
- package/src/superlocalmemory/cli/daemon.py +5 -1
- package/src/superlocalmemory/cli/main.py +35 -2
- package/src/superlocalmemory/cli/ops_cmd.py +281 -0
- package/src/superlocalmemory/cli/setup_wizard.py +1 -1
- package/src/superlocalmemory/compliance/audit.py +65 -0
- package/src/superlocalmemory/compliance/eu_ai_act.py +27 -57
- package/src/superlocalmemory/compliance/gdpr.py +416 -20
- package/src/superlocalmemory/compliance/retention.py +74 -22
- package/src/superlocalmemory/compliance/scheduler.py +78 -9
- package/src/superlocalmemory/core/actor_context.py +166 -0
- package/src/superlocalmemory/core/admission.py +549 -0
- package/src/superlocalmemory/core/backend_orchestrator.py +23 -10
- package/src/superlocalmemory/core/config.py +202 -24
- package/src/superlocalmemory/core/consolidation_engine.py +13 -13
- package/src/superlocalmemory/core/context_cache.py +28 -0
- package/src/superlocalmemory/core/embeddings.py +64 -2
- package/src/superlocalmemory/core/engine.py +7 -2
- package/src/superlocalmemory/core/engine_ingestion.py +65 -3
- package/src/superlocalmemory/core/engine_wiring.py +36 -9
- package/src/superlocalmemory/core/ingest_policy.py +38 -0
- package/src/superlocalmemory/core/maintenance.py +255 -0
- package/src/superlocalmemory/core/modes.py +40 -13
- package/src/superlocalmemory/core/mutations.py +437 -44
- package/src/superlocalmemory/core/operation_policy.py +92 -0
- package/src/superlocalmemory/core/operation_policy_registry.py +542 -0
- package/src/superlocalmemory/core/operation_request.py +127 -0
- package/src/superlocalmemory/core/ops_remediation.py +542 -0
- package/src/superlocalmemory/core/recall_pipeline.py +7 -0
- package/src/superlocalmemory/core/remember_runtime.py +202 -4
- package/src/superlocalmemory/core/remote_mode.py +20 -5
- package/src/superlocalmemory/core/store_pipeline.py +150 -0
- package/src/superlocalmemory/core/topic_signature.py +19 -4
- package/src/superlocalmemory/core/transactions/__init__.py +78 -0
- package/src/superlocalmemory/core/transactions/concrete_owners.py +597 -0
- package/src/superlocalmemory/core/transactions/erasure.py +825 -0
- package/src/superlocalmemory/core/transactions/manifest.py +255 -0
- package/src/superlocalmemory/core/transactions/manifest_key.py +155 -0
- package/src/superlocalmemory/core/transactions/obligations.py +272 -0
- package/src/superlocalmemory/core/transactions/owners.py +114 -0
- package/src/superlocalmemory/core/transactions/reconciler.py +285 -0
- package/src/superlocalmemory/core/transactions/service.py +330 -0
- package/src/superlocalmemory/core/worker_pool.py +33 -5
- package/src/superlocalmemory/encoding/cognitive_consolidator.py +70 -28
- package/src/superlocalmemory/encoding/emotional.py +75 -14
- package/src/superlocalmemory/encoding/scene_builder.py +115 -13
- package/src/superlocalmemory/encoding/temporal_parser.py +4 -0
- package/src/superlocalmemory/evolution/blind_verifier.py +11 -4
- package/src/superlocalmemory/evolution/evolution_store.py +244 -4
- package/src/superlocalmemory/evolution/llm_dispatch.py +40 -0
- package/src/superlocalmemory/evolution/model_selection.py +18 -3
- package/src/superlocalmemory/evolution/mutation_generator.py +3 -0
- package/src/superlocalmemory/evolution/skill_activator.py +270 -0
- package/src/superlocalmemory/evolution/skill_evolver.py +281 -59
- package/src/superlocalmemory/evolution/types.py +30 -8
- package/src/superlocalmemory/graph/cozo_backend.py +17 -9
- package/src/superlocalmemory/hooks/auto_invoker.py +2 -1
- package/src/superlocalmemory/hooks/auto_recall.py +64 -30
- package/src/superlocalmemory/hooks/codex_assets.py +14 -1
- package/src/superlocalmemory/infra/backup.py +434 -7
- package/src/superlocalmemory/infra/process_reaper.py +18 -0
- package/src/superlocalmemory/infra/self_heal.py +401 -0
- package/src/superlocalmemory/learning/feedback.py +52 -9
- package/src/superlocalmemory/loops/engine.py +10 -0
- package/src/superlocalmemory/mcp/_daemon_proxy.py +3 -0
- package/src/superlocalmemory/mcp/http_transport.py +30 -331
- package/src/superlocalmemory/mcp/profiles.py +5 -0
- package/src/superlocalmemory/mcp/resources.py +8 -0
- package/src/superlocalmemory/mcp/server.py +51 -4
- package/src/superlocalmemory/mcp/shared.py +19 -0
- package/src/superlocalmemory/mcp/tools_active.py +25 -4
- package/src/superlocalmemory/mcp/tools_code_graph.py +26 -18
- package/src/superlocalmemory/mcp/tools_context.py +50 -8
- package/src/superlocalmemory/mcp/tools_core.py +69 -21
- package/src/superlocalmemory/mcp/tools_evolution.py +9 -2
- package/src/superlocalmemory/mcp/tools_learning.py +21 -10
- package/src/superlocalmemory/mcp/tools_loops.py +29 -18
- package/src/superlocalmemory/mcp/tools_mesh.py +8 -0
- package/src/superlocalmemory/mcp/tools_ops.py +115 -0
- package/src/superlocalmemory/mcp/tools_optimize.py +4 -0
- package/src/superlocalmemory/mcp/tools_v28.py +10 -3
- package/src/superlocalmemory/mcp/tools_v3.py +34 -14
- package/src/superlocalmemory/mcp/tools_v33.py +18 -33
- package/src/superlocalmemory/mesh/broker.py +124 -46
- package/src/superlocalmemory/mesh/broker_security.py +470 -0
- package/src/superlocalmemory/mesh/discovery.py +365 -0
- package/src/superlocalmemory/mesh/lock_protocol.py +313 -0
- package/src/superlocalmemory/mesh/node_identity.py +97 -0
- package/src/superlocalmemory/mesh/outbox_remote.py +429 -0
- package/src/superlocalmemory/mesh/remote_sync.py +511 -28
- package/src/superlocalmemory/mesh/state_sync.py +286 -0
- package/src/superlocalmemory/optimize/config/store.py +45 -0
- package/src/superlocalmemory/parameterization/cross_project.py +12 -0
- package/src/superlocalmemory/parameterization/prompt_injector.py +13 -11
- package/src/superlocalmemory/parameterization/prompt_lifecycle.py +8 -2
- package/src/superlocalmemory/parameterization/workflow_miner.py +17 -0
- package/src/superlocalmemory/retrieval/ann_index.py +5 -0
- package/src/superlocalmemory/retrieval/bm25_channel.py +49 -2
- package/src/superlocalmemory/retrieval/engine.py +19 -4
- package/src/superlocalmemory/retrieval/fusion.py +4 -1
- package/src/superlocalmemory/retrieval/hopfield_channel.py +9 -3
- package/src/superlocalmemory/retrieval/remote_reranker.py +47 -22
- package/src/superlocalmemory/retrieval/reranker.py +32 -1
- package/src/superlocalmemory/retrieval/temporal_channel.py +16 -3
- package/src/superlocalmemory/retrieval/temporal_utils.py +107 -0
- package/src/superlocalmemory/retrieval/temporal_validity_filter.py +155 -42
- package/src/superlocalmemory/retrieval/vector_store.py +214 -8
- package/src/superlocalmemory/server/api.py +5 -5
- package/src/superlocalmemory/server/egress_policy.py +258 -0
- package/src/superlocalmemory/server/rbac_enforce.py +32 -0
- package/src/superlocalmemory/server/route_mutations.py +20 -0
- package/src/superlocalmemory/server/routes/compliance.py +153 -7
- package/src/superlocalmemory/server/routes/data_io.py +43 -2
- package/src/superlocalmemory/server/routes/events.py +15 -0
- package/src/superlocalmemory/server/routes/memories.py +56 -3
- package/src/superlocalmemory/server/routes/mesh.py +82 -1
- package/src/superlocalmemory/server/routes/mesh_lock.py +54 -0
- package/src/superlocalmemory/server/routes/mesh_state.py +63 -0
- package/src/superlocalmemory/server/routes/v3_api.py +50 -27
- package/src/superlocalmemory/server/routes/ws.py +86 -0
- package/src/superlocalmemory/server/ui.py +6 -6
- package/src/superlocalmemory/server/unified_daemon.py +942 -119
- package/src/superlocalmemory/storage/_migration_internals.py +568 -0
- package/src/superlocalmemory/storage/_schema_version.py +110 -0
- package/src/superlocalmemory/storage/database.py +329 -24
- package/src/superlocalmemory/storage/embedding_migrator.py +246 -51
- package/src/superlocalmemory/storage/erasure_fence.py +45 -0
- package/src/superlocalmemory/storage/generation_fence.py +63 -0
- package/src/superlocalmemory/storage/migration_runner.py +140 -417
- package/src/superlocalmemory/storage/migrations/M009_model_lineage.py +40 -0
- package/src/superlocalmemory/storage/migrations/M033_projection_transactions.py +148 -0
- package/src/superlocalmemory/storage/migrations/M034_obligation_integrity.py +58 -0
- package/src/superlocalmemory/storage/migrations/M035_erasure_receipts.py +113 -0
- package/src/superlocalmemory/storage/migrations/M036_vector_row_map.py +107 -0
- package/src/superlocalmemory/storage/migrations/M037_manifest_hmac_version.py +162 -0
- package/src/superlocalmemory/storage/migrations/{M033_learning_feedback_channel.py → M038_learning_feedback_channel.py} +3 -3
- package/src/superlocalmemory/storage/migrations/M039_scene_fact_members.py +137 -0
- package/src/superlocalmemory/storage/migrations/__init__.py +4 -2
- package/src/superlocalmemory/storage/schema.py +67 -0
- package/src/superlocalmemory/storage/write_coordinator.py +125 -0
- package/src/superlocalmemory/trust/scorer.py +28 -4
- package/src/superlocalmemory/ui/index.html +14 -3
- package/src/superlocalmemory/ui/js/auto-settings.js +12 -1
- package/src/superlocalmemory/ui/js/brain.js +6 -4
- package/src/superlocalmemory/ui/js/compliance.js +66 -12
- package/src/superlocalmemory/ui/js/dashboard.js +13 -3
- package/src/superlocalmemory/ui/js/feedback.js +8 -2
- package/src/superlocalmemory/ui/js/lifecycle.js +7 -1
- package/src/superlocalmemory/ui/js/modal.js +272 -5
- package/src/superlocalmemory/ui/js/od-backup.js +9 -2
- package/src/superlocalmemory/ui/js/od-compliance-ext.js +301 -0
- package/src/superlocalmemory/ui/js/od-operations.js +154 -23
- package/src/superlocalmemory/ui/js/od-ops-health.js +417 -0
- package/src/superlocalmemory/ui/js/od-optimize.js +35 -21
- package/src/superlocalmemory/ui/js/od-team.js +9 -2
- package/src/superlocalmemory/ui/js/optimize.js +13 -16
- package/src/superlocalmemory/ui/js/profiles.js +7 -3
- package/src/superlocalmemory/ui/js/settings.js +7 -1
- package/src/superlocalmemory/vector/lancedb_backend.py +19 -9
- package/src/superlocalmemory/attribution/mathematical_dna.py +0 -235
- package/src/superlocalmemory/cli/post_install.py +0 -114
- package/src/superlocalmemory/core/clock_monitor.py +0 -45
- package/src/superlocalmemory/core/db_pool.py +0 -80
- package/src/superlocalmemory/core/error_catalog.py +0 -113
- package/src/superlocalmemory/core/loop_watchdog.py +0 -56
- package/src/superlocalmemory/core/priority_queue.py +0 -61
- package/src/superlocalmemory/core/pruning_engine.py +0 -216
- package/src/superlocalmemory/core/queue_dispatcher.py +0 -73
- package/src/superlocalmemory/core/slmignore.py +0 -125
- package/src/superlocalmemory/infra/heartbeat_monitor.py +0 -140
- package/src/superlocalmemory/infra/webhook_dispatcher.py +0 -247
- package/src/superlocalmemory/learning/quantization_scheduler.py +0 -320
- package/src/superlocalmemory/storage/access_control.py +0 -182
|
@@ -0,0 +1,597 @@
|
|
|
1
|
+
# Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
|
|
2
|
+
# Licensed under AGPL-3.0-or-later - see LICENSE file
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import hashlib
|
|
7
|
+
import json
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from superlocalmemory.core.transactions.erasure import ErasureService
|
|
11
|
+
from superlocalmemory.core.transactions.owners import (
|
|
12
|
+
OperationContext,
|
|
13
|
+
OwnerErasureProof,
|
|
14
|
+
OwnerHealth,
|
|
15
|
+
OwnerResult,
|
|
16
|
+
)
|
|
17
|
+
from superlocalmemory.core.transactions.service import MemoryTransactionService
|
|
18
|
+
|
|
19
|
+
REQUIRED_ADMISSION_OWNERS: tuple[str, ...] = ("bm25", "temporal", "vector")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _scope_checksum(owner: str, fingerprints: dict[str, str]) -> str:
|
|
23
|
+
parts = [f"{fact_id}={fingerprints[fact_id]}" for fact_id in sorted(fingerprints)]
|
|
24
|
+
payload = owner + "|" + "|".join(parts)
|
|
25
|
+
return hashlib.sha256(payload.encode("utf-8")).hexdigest()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _erasure_checksum(
|
|
29
|
+
owner: str, targets: tuple[str, ...], residue: set[str],
|
|
30
|
+
) -> str:
|
|
31
|
+
payload = "\0".join([
|
|
32
|
+
owner,
|
|
33
|
+
"targets=" + ",".join(sorted(set(targets))),
|
|
34
|
+
"residue=" + ",".join(sorted(residue)),
|
|
35
|
+
])
|
|
36
|
+
return hashlib.sha256(payload.encode("utf-8")).hexdigest()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _fingerprint(*parts: str) -> str:
|
|
40
|
+
return hashlib.sha256("\0".join(parts).encode("utf-8")).hexdigest()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _placeholders(count: int) -> str:
|
|
44
|
+
return ",".join("?" for _ in range(count))
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _row_fact_id(row: Any) -> str | None:
|
|
48
|
+
try:
|
|
49
|
+
return dict(row)["fact_id"]
|
|
50
|
+
except (KeyError, ValueError, TypeError):
|
|
51
|
+
pass
|
|
52
|
+
try:
|
|
53
|
+
return row["fact_id"]
|
|
54
|
+
except Exception: # noqa: BLE001
|
|
55
|
+
pass
|
|
56
|
+
try:
|
|
57
|
+
return row[0]
|
|
58
|
+
except Exception: # noqa: BLE001
|
|
59
|
+
return None
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _db_table_exists(db: Any, name: str) -> bool:
|
|
63
|
+
try:
|
|
64
|
+
rows = db.execute(
|
|
65
|
+
"SELECT 1 FROM sqlite_master WHERE type='table' AND name=?",
|
|
66
|
+
(name,),
|
|
67
|
+
)
|
|
68
|
+
except Exception: # noqa: BLE001
|
|
69
|
+
return False
|
|
70
|
+
return bool(list(rows))
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _present_fact_ids(rows: Any, targets: set[str]) -> set[str]:
|
|
74
|
+
present: set[str] = set()
|
|
75
|
+
unreadable = False
|
|
76
|
+
for row in rows:
|
|
77
|
+
fid = _row_fact_id(row)
|
|
78
|
+
if fid is None:
|
|
79
|
+
unreadable = True
|
|
80
|
+
continue
|
|
81
|
+
present.add(fid)
|
|
82
|
+
if unreadable:
|
|
83
|
+
return set(targets)
|
|
84
|
+
return present & targets
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class _FactScopedOwner:
|
|
88
|
+
_name: str
|
|
89
|
+
|
|
90
|
+
def __init__(self, db: Any) -> None:
|
|
91
|
+
self._db = db
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def name(self) -> str:
|
|
95
|
+
return self._name
|
|
96
|
+
|
|
97
|
+
def _required(self, context: OperationContext) -> set[str]:
|
|
98
|
+
return set(context.fact_ids) - self._tombstoned(context)
|
|
99
|
+
|
|
100
|
+
def _tombstoned(self, context: OperationContext) -> set[str]:
|
|
101
|
+
if not context.fact_ids:
|
|
102
|
+
return set()
|
|
103
|
+
try:
|
|
104
|
+
rows = self._db.execute(
|
|
105
|
+
f"SELECT fact_id FROM projection_tombstones WHERE profile_id = ? "
|
|
106
|
+
f"AND fact_id IN ({_placeholders(len(context.fact_ids))})",
|
|
107
|
+
(context.profile_id, *context.fact_ids),
|
|
108
|
+
)
|
|
109
|
+
except Exception:
|
|
110
|
+
return set()
|
|
111
|
+
return {fid for fid in (_row_fact_id(row) for row in rows) if fid is not None}
|
|
112
|
+
|
|
113
|
+
def _fingerprints(self, context: OperationContext) -> dict[str, str]:
|
|
114
|
+
raise NotImplementedError
|
|
115
|
+
|
|
116
|
+
def _physical_present(self, context: OperationContext) -> set[str]:
|
|
117
|
+
raise NotImplementedError
|
|
118
|
+
|
|
119
|
+
def _heal(self, context: OperationContext, fact_id: str) -> bool:
|
|
120
|
+
return False
|
|
121
|
+
|
|
122
|
+
def _remove(self, context: OperationContext, fact_id: str) -> None:
|
|
123
|
+
raise NotImplementedError
|
|
124
|
+
|
|
125
|
+
def _result(self, context: OperationContext) -> OwnerResult:
|
|
126
|
+
required = self._required(context)
|
|
127
|
+
fingerprints = {
|
|
128
|
+
fact_id: fp
|
|
129
|
+
for fact_id, fp in self._fingerprints(context).items()
|
|
130
|
+
if fact_id in required
|
|
131
|
+
}
|
|
132
|
+
missing = required - set(fingerprints)
|
|
133
|
+
return OwnerResult(
|
|
134
|
+
owner=self._name,
|
|
135
|
+
ok=not missing,
|
|
136
|
+
checksum=_scope_checksum(self._name, fingerprints),
|
|
137
|
+
detail={} if not missing else {"missing": sorted(missing)},
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
def verify(self, context: OperationContext) -> OwnerResult:
|
|
141
|
+
return self._result(context)
|
|
142
|
+
|
|
143
|
+
def prove_erased(self, context: OperationContext) -> OwnerErasureProof:
|
|
144
|
+
residue = self._physical_present(context) & set(context.fact_ids)
|
|
145
|
+
detail = {"residue": sorted(residue)} if residue else {}
|
|
146
|
+
return OwnerErasureProof(
|
|
147
|
+
owner=self._name,
|
|
148
|
+
erased=not residue,
|
|
149
|
+
checksum=_erasure_checksum(self._name, context.fact_ids, residue),
|
|
150
|
+
detail=detail,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
def apply(self, context: OperationContext) -> OwnerResult:
|
|
154
|
+
required = self._required(context)
|
|
155
|
+
current = set(self._fingerprints(context))
|
|
156
|
+
errors: list[str] = []
|
|
157
|
+
for fact_id in sorted(required - current):
|
|
158
|
+
try:
|
|
159
|
+
self._heal(context, fact_id)
|
|
160
|
+
except Exception as exc: # noqa: BLE001
|
|
161
|
+
errors.append(f"{fact_id}: {type(exc).__name__}")
|
|
162
|
+
result = self._result(context)
|
|
163
|
+
if errors:
|
|
164
|
+
return OwnerResult(
|
|
165
|
+
owner=self._name,
|
|
166
|
+
ok=result.ok,
|
|
167
|
+
checksum=result.checksum,
|
|
168
|
+
detail={**dict(result.detail), "errors": errors},
|
|
169
|
+
)
|
|
170
|
+
return result
|
|
171
|
+
|
|
172
|
+
def compensate(self, context: OperationContext) -> OwnerResult:
|
|
173
|
+
return self._delete_all(context, phase="compensate")
|
|
174
|
+
|
|
175
|
+
def erase(self, context: OperationContext) -> OwnerErasureProof:
|
|
176
|
+
result = self._delete_all(context, phase="erase")
|
|
177
|
+
residue = self._physical_present(context) & set(context.fact_ids)
|
|
178
|
+
detail: dict[str, Any] = {} if result.ok else dict(result.detail)
|
|
179
|
+
if residue:
|
|
180
|
+
detail = {**detail, "residue": sorted(residue)}
|
|
181
|
+
return OwnerErasureProof(
|
|
182
|
+
owner=self._name,
|
|
183
|
+
erased=not residue,
|
|
184
|
+
checksum=_erasure_checksum(self._name, context.fact_ids, residue),
|
|
185
|
+
detail=detail,
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
def _delete_all(self, context: OperationContext, *, phase: str) -> OwnerResult:
|
|
189
|
+
errors: list[str] = []
|
|
190
|
+
for fact_id in context.fact_ids:
|
|
191
|
+
try:
|
|
192
|
+
self._remove(context, fact_id)
|
|
193
|
+
except Exception as exc: # noqa: BLE001
|
|
194
|
+
errors.append(f"{fact_id}: {type(exc).__name__}")
|
|
195
|
+
return OwnerResult(
|
|
196
|
+
owner=self._name,
|
|
197
|
+
ok=not errors,
|
|
198
|
+
detail={} if not errors else {"phase": phase, "errors": errors},
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
def health(self) -> OwnerHealth:
|
|
202
|
+
return OwnerHealth(owner=self._name, healthy=True)
|
|
203
|
+
|
|
204
|
+
def _fact_content(self, context: OperationContext, fact_id: str) -> str | None:
|
|
205
|
+
rows = list(self._db.execute(
|
|
206
|
+
"SELECT content FROM atomic_facts WHERE fact_id = ? AND profile_id = ?",
|
|
207
|
+
(fact_id, context.profile_id),
|
|
208
|
+
))
|
|
209
|
+
if not rows:
|
|
210
|
+
return None
|
|
211
|
+
try:
|
|
212
|
+
return dict(rows[0])["content"]
|
|
213
|
+
except (KeyError, ValueError, TypeError):
|
|
214
|
+
try:
|
|
215
|
+
return rows[0]["content"]
|
|
216
|
+
except Exception: # noqa: BLE001
|
|
217
|
+
return None
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
class Bm25Owner(_FactScopedOwner):
|
|
221
|
+
_name = "bm25"
|
|
222
|
+
|
|
223
|
+
def __init__(self, db: Any, *, retrieval: Any = None) -> None:
|
|
224
|
+
super().__init__(db)
|
|
225
|
+
self._retrieval = retrieval
|
|
226
|
+
|
|
227
|
+
def _fingerprints(self, context: OperationContext) -> dict[str, str]:
|
|
228
|
+
if not context.fact_ids:
|
|
229
|
+
return {}
|
|
230
|
+
from superlocalmemory.retrieval.bm25_channel import tokenize
|
|
231
|
+
|
|
232
|
+
rows = self._db.execute(
|
|
233
|
+
f"SELECT fact_id, tokens FROM bm25_tokens WHERE profile_id = ? "
|
|
234
|
+
f"AND fact_id IN ({_placeholders(len(context.fact_ids))})",
|
|
235
|
+
(context.profile_id, *context.fact_ids),
|
|
236
|
+
)
|
|
237
|
+
stored: dict[str, list[str]] = {}
|
|
238
|
+
for row in rows:
|
|
239
|
+
record = dict(row)
|
|
240
|
+
fid = record.get("fact_id")
|
|
241
|
+
if fid is None:
|
|
242
|
+
continue
|
|
243
|
+
try:
|
|
244
|
+
tokens = json.loads(record["tokens"])
|
|
245
|
+
except (TypeError, KeyError, json.JSONDecodeError):
|
|
246
|
+
continue
|
|
247
|
+
if not isinstance(tokens, list) or not tokens:
|
|
248
|
+
continue
|
|
249
|
+
if not all(isinstance(token, str) for token in tokens):
|
|
250
|
+
continue
|
|
251
|
+
stored[fid] = tokens
|
|
252
|
+
result: dict[str, str] = {}
|
|
253
|
+
for fact_id, tokens in stored.items():
|
|
254
|
+
content = self._fact_content(context, fact_id)
|
|
255
|
+
if content is None:
|
|
256
|
+
continue
|
|
257
|
+
expected = sorted(tokenize(content))
|
|
258
|
+
if not expected or sorted(tokens) != expected:
|
|
259
|
+
continue
|
|
260
|
+
result[fact_id] = _fingerprint(
|
|
261
|
+
"bm25", json.dumps(expected, separators=(",", ":"))
|
|
262
|
+
)
|
|
263
|
+
return result
|
|
264
|
+
|
|
265
|
+
def _physical_present(self, context: OperationContext) -> set[str]:
|
|
266
|
+
if not context.fact_ids:
|
|
267
|
+
return set()
|
|
268
|
+
rows = self._db.execute(
|
|
269
|
+
f"SELECT fact_id FROM bm25_tokens WHERE profile_id = ? "
|
|
270
|
+
f"AND fact_id IN ({_placeholders(len(context.fact_ids))})",
|
|
271
|
+
(context.profile_id, *context.fact_ids),
|
|
272
|
+
)
|
|
273
|
+
return _present_fact_ids(rows, set(context.fact_ids))
|
|
274
|
+
|
|
275
|
+
def _heal(self, context: OperationContext, fact_id: str) -> bool:
|
|
276
|
+
bm25 = getattr(self._retrieval, "_bm25", None)
|
|
277
|
+
content = self._fact_content(context, fact_id)
|
|
278
|
+
if bm25 is None or content is None:
|
|
279
|
+
return False
|
|
280
|
+
if hasattr(bm25, "update_fact"):
|
|
281
|
+
bm25.update_fact(fact_id, content, context.profile_id)
|
|
282
|
+
return True
|
|
283
|
+
if hasattr(bm25, "add") and hasattr(bm25, "remove_fact"):
|
|
284
|
+
bm25.remove_fact(fact_id)
|
|
285
|
+
bm25.add(fact_id, content, context.profile_id)
|
|
286
|
+
return True
|
|
287
|
+
return False
|
|
288
|
+
|
|
289
|
+
def _remove(self, context: OperationContext, fact_id: str) -> None:
|
|
290
|
+
self._db.delete_bm25_tokens_for_fact(fact_id)
|
|
291
|
+
bm25 = getattr(self._retrieval, "_bm25", None)
|
|
292
|
+
if bm25 is not None and hasattr(bm25, "remove_fact"):
|
|
293
|
+
bm25.remove_fact(fact_id)
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
class TemporalOwner(_FactScopedOwner):
|
|
297
|
+
_name = "temporal"
|
|
298
|
+
|
|
299
|
+
def _fingerprints(self, context: OperationContext) -> dict[str, str]:
|
|
300
|
+
if not context.fact_ids:
|
|
301
|
+
return {}
|
|
302
|
+
rows = self._db.execute(
|
|
303
|
+
f"SELECT fact_id, valid_from, valid_until FROM fact_temporal_validity "
|
|
304
|
+
f"WHERE profile_id = ? AND fact_id IN ({_placeholders(len(context.fact_ids))})",
|
|
305
|
+
(context.profile_id, *context.fact_ids),
|
|
306
|
+
)
|
|
307
|
+
result: dict[str, str] = {}
|
|
308
|
+
for row in rows:
|
|
309
|
+
record = dict(row)
|
|
310
|
+
fid = record.get("fact_id")
|
|
311
|
+
if fid is None or record.get("valid_from") is None:
|
|
312
|
+
continue
|
|
313
|
+
result[fid] = _fingerprint(
|
|
314
|
+
"temporal",
|
|
315
|
+
str(record["valid_from"]),
|
|
316
|
+
str(record.get("valid_until") or ""),
|
|
317
|
+
)
|
|
318
|
+
return result
|
|
319
|
+
|
|
320
|
+
def _physical_present(self, context: OperationContext) -> set[str]:
|
|
321
|
+
if not context.fact_ids:
|
|
322
|
+
return set()
|
|
323
|
+
rows = self._db.execute(
|
|
324
|
+
f"SELECT fact_id FROM fact_temporal_validity WHERE profile_id = ? "
|
|
325
|
+
f"AND fact_id IN ({_placeholders(len(context.fact_ids))})",
|
|
326
|
+
(context.profile_id, *context.fact_ids),
|
|
327
|
+
)
|
|
328
|
+
return _present_fact_ids(rows, set(context.fact_ids))
|
|
329
|
+
|
|
330
|
+
def _heal(self, context: OperationContext, fact_id: str) -> bool:
|
|
331
|
+
rows = self._db.execute(
|
|
332
|
+
"SELECT created_at FROM atomic_facts WHERE fact_id = ? AND profile_id = ?",
|
|
333
|
+
(fact_id, context.profile_id),
|
|
334
|
+
)
|
|
335
|
+
if not rows:
|
|
336
|
+
return False
|
|
337
|
+
valid_from = dict(rows[0]).get("created_at")
|
|
338
|
+
if not valid_from:
|
|
339
|
+
return False
|
|
340
|
+
self._db.store_temporal_validity(fact_id, context.profile_id, valid_from)
|
|
341
|
+
return True
|
|
342
|
+
|
|
343
|
+
def _remove(self, context: OperationContext, fact_id: str) -> None:
|
|
344
|
+
self._db.delete_temporal_validity(fact_id)
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
class VectorOwner(_FactScopedOwner):
|
|
348
|
+
_name = "vector"
|
|
349
|
+
|
|
350
|
+
def __init__(
|
|
351
|
+
self, db: Any, *, vector_store: Any = None, ann_index: Any = None,
|
|
352
|
+
) -> None:
|
|
353
|
+
super().__init__(db)
|
|
354
|
+
self._vector_store = vector_store
|
|
355
|
+
self._ann_index = ann_index
|
|
356
|
+
|
|
357
|
+
def _store_available(self) -> bool:
|
|
358
|
+
return self._vector_store is not None and bool(
|
|
359
|
+
getattr(self._vector_store, "available", False)
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
def _embedded_fact_ids(self, context: OperationContext) -> set[str]:
|
|
363
|
+
"""Query which facts have embeddings, regardless of store availability.
|
|
364
|
+
|
|
365
|
+
Raises whatever exception the DB raises — callers that need fail-closed
|
|
366
|
+
behaviour must handle (or propagate) the exception. Do NOT swallow
|
|
367
|
+
here: a silent empty-set return would let verify() produce a vacuous
|
|
368
|
+
NOT_APPLICABLE, masking a real DB fault as "no obligation".
|
|
369
|
+
"""
|
|
370
|
+
if not context.fact_ids:
|
|
371
|
+
return set()
|
|
372
|
+
rows = self._db.execute(
|
|
373
|
+
f"SELECT fact_id FROM atomic_facts WHERE profile_id = ? "
|
|
374
|
+
f"AND embedding IS NOT NULL AND embedding != '' "
|
|
375
|
+
f"AND fact_id IN ({_placeholders(len(context.fact_ids))})",
|
|
376
|
+
(context.profile_id, *context.fact_ids),
|
|
377
|
+
)
|
|
378
|
+
found = {fid for fid in (_row_fact_id(row) for row in rows) if fid is not None}
|
|
379
|
+
return found - self._tombstoned(context)
|
|
380
|
+
|
|
381
|
+
def _required(self, context: OperationContext) -> set[str]:
|
|
382
|
+
if not context.fact_ids:
|
|
383
|
+
return set()
|
|
384
|
+
embedded = self._embedded_fact_ids(context)
|
|
385
|
+
if not embedded or not self._store_available():
|
|
386
|
+
return set()
|
|
387
|
+
return embedded
|
|
388
|
+
|
|
389
|
+
def verify(self, context: OperationContext) -> OwnerResult:
|
|
390
|
+
"""Return NOT_APPLICABLE (ok=True) when no facts have embeddings.
|
|
391
|
+
|
|
392
|
+
Return REQUIRED_UNAVAILABLE (ok=False, detail.required_unavailable=True)
|
|
393
|
+
when embedded facts exist but the vector store is unavailable, OR when
|
|
394
|
+
the embedding DB query itself fails (fail-closed: unknown state is never
|
|
395
|
+
treated as "no obligation").
|
|
396
|
+
|
|
397
|
+
Return normal fingerprint-based result when the store is available.
|
|
398
|
+
"""
|
|
399
|
+
try:
|
|
400
|
+
embedded = self._embedded_fact_ids(context)
|
|
401
|
+
except Exception: # noqa: BLE001
|
|
402
|
+
# DB query failure — we cannot determine whether embeddings exist.
|
|
403
|
+
# Fail closed: surface as REQUIRED_UNAVAILABLE so the obligation is
|
|
404
|
+
# recorded as unverified rather than silently waived.
|
|
405
|
+
return OwnerResult(
|
|
406
|
+
owner=self._name,
|
|
407
|
+
ok=False,
|
|
408
|
+
detail={"required_unavailable": True, "db_query_failed": True},
|
|
409
|
+
)
|
|
410
|
+
if not embedded:
|
|
411
|
+
return OwnerResult(
|
|
412
|
+
owner=self._name,
|
|
413
|
+
ok=True,
|
|
414
|
+
checksum=_scope_checksum(self._name, {}),
|
|
415
|
+
detail={"not_applicable": True},
|
|
416
|
+
)
|
|
417
|
+
if not self._store_available():
|
|
418
|
+
return OwnerResult(
|
|
419
|
+
owner=self._name,
|
|
420
|
+
ok=False,
|
|
421
|
+
detail={"required_unavailable": True, "fact_count": len(embedded)},
|
|
422
|
+
)
|
|
423
|
+
return self._result(context)
|
|
424
|
+
|
|
425
|
+
def _fingerprints(self, context: OperationContext) -> dict[str, str]:
|
|
426
|
+
if not context.fact_ids or not self._store_available():
|
|
427
|
+
return {}
|
|
428
|
+
indexed = self._vector_store.indexed_fact_ids(context.profile_id)
|
|
429
|
+
candidates = [fid for fid in context.fact_ids if fid in indexed]
|
|
430
|
+
if not candidates:
|
|
431
|
+
return {}
|
|
432
|
+
rows = self._db.execute(
|
|
433
|
+
f"SELECT fact_id, embedding FROM atomic_facts WHERE profile_id = ? "
|
|
434
|
+
f"AND fact_id IN ({_placeholders(len(candidates))})",
|
|
435
|
+
(context.profile_id, *candidates),
|
|
436
|
+
)
|
|
437
|
+
result: dict[str, str] = {}
|
|
438
|
+
for row in rows:
|
|
439
|
+
record = dict(row)
|
|
440
|
+
fid = record.get("fact_id")
|
|
441
|
+
embedding = record.get("embedding")
|
|
442
|
+
if fid is None or not embedding:
|
|
443
|
+
continue
|
|
444
|
+
result[fid] = _fingerprint("vector", str(embedding))
|
|
445
|
+
return result
|
|
446
|
+
|
|
447
|
+
def _table_present(
|
|
448
|
+
self, table: str, context: OperationContext, targets: set[str],
|
|
449
|
+
) -> set[str]:
|
|
450
|
+
if not _db_table_exists(self._db, table):
|
|
451
|
+
return set()
|
|
452
|
+
try:
|
|
453
|
+
rows = self._db.execute(
|
|
454
|
+
f"SELECT fact_id FROM {table} WHERE profile_id = ? "
|
|
455
|
+
f"AND fact_id IN ({_placeholders(len(context.fact_ids))})",
|
|
456
|
+
(context.profile_id, *context.fact_ids),
|
|
457
|
+
)
|
|
458
|
+
except Exception: # noqa: BLE001
|
|
459
|
+
return set(targets)
|
|
460
|
+
return _present_fact_ids(rows, targets)
|
|
461
|
+
|
|
462
|
+
def _physical_present(self, context: OperationContext) -> set[str]:
|
|
463
|
+
if not context.fact_ids:
|
|
464
|
+
return set()
|
|
465
|
+
targets = set(context.fact_ids)
|
|
466
|
+
present: set[str] = set()
|
|
467
|
+
present |= self._table_present("embedding_metadata", context, targets)
|
|
468
|
+
present |= self._table_present("vector_row_map", context, targets)
|
|
469
|
+
store = self._vector_store
|
|
470
|
+
if store is not None and hasattr(store, "raw_vector_present"):
|
|
471
|
+
for fid in targets - present:
|
|
472
|
+
try:
|
|
473
|
+
if store.raw_vector_present(fid):
|
|
474
|
+
present.add(fid)
|
|
475
|
+
except Exception: # noqa: BLE001
|
|
476
|
+
present.add(fid)
|
|
477
|
+
ann = self._ann_index
|
|
478
|
+
if ann is not None and hasattr(ann, "contains"):
|
|
479
|
+
for fid in targets - present:
|
|
480
|
+
try:
|
|
481
|
+
if ann.contains(fid):
|
|
482
|
+
present.add(fid)
|
|
483
|
+
except Exception: # noqa: BLE001
|
|
484
|
+
present.add(fid)
|
|
485
|
+
return present
|
|
486
|
+
|
|
487
|
+
def _heal(self, context: OperationContext, fact_id: str) -> bool:
|
|
488
|
+
if not self._store_available():
|
|
489
|
+
return False
|
|
490
|
+
rows = self._db.execute(
|
|
491
|
+
"SELECT embedding FROM atomic_facts WHERE fact_id = ? AND profile_id = ?",
|
|
492
|
+
(fact_id, context.profile_id),
|
|
493
|
+
)
|
|
494
|
+
if not rows:
|
|
495
|
+
return False
|
|
496
|
+
raw = dict(rows[0]).get("embedding")
|
|
497
|
+
if not raw:
|
|
498
|
+
return False
|
|
499
|
+
try:
|
|
500
|
+
embedding = json.loads(raw)
|
|
501
|
+
except (TypeError, json.JSONDecodeError):
|
|
502
|
+
return False
|
|
503
|
+
if not isinstance(embedding, list) or not embedding:
|
|
504
|
+
return False
|
|
505
|
+
ok = self._vector_store.upsert(
|
|
506
|
+
fact_id=fact_id, profile_id=context.profile_id, embedding=embedding,
|
|
507
|
+
)
|
|
508
|
+
if ok and self._ann_index is not None and hasattr(self._ann_index, "add"):
|
|
509
|
+
try:
|
|
510
|
+
self._ann_index.add(fact_id, embedding)
|
|
511
|
+
except Exception: # noqa: BLE001
|
|
512
|
+
pass
|
|
513
|
+
return bool(ok)
|
|
514
|
+
|
|
515
|
+
def _remove(self, context: OperationContext, fact_id: str) -> None:
|
|
516
|
+
if self._vector_store is not None and hasattr(self._vector_store, "delete"):
|
|
517
|
+
self._vector_store.delete(fact_id)
|
|
518
|
+
else:
|
|
519
|
+
self._db.execute(
|
|
520
|
+
"DELETE FROM embedding_metadata WHERE fact_id = ?", (fact_id,)
|
|
521
|
+
)
|
|
522
|
+
if self._ann_index is not None and hasattr(self._ann_index, "remove"):
|
|
523
|
+
self._ann_index.remove(fact_id)
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
def _admission_owners(engine: Any) -> dict[str, Any]:
|
|
527
|
+
db = engine._db
|
|
528
|
+
retrieval = getattr(engine, "_retrieval_engine", None)
|
|
529
|
+
return {
|
|
530
|
+
"bm25": Bm25Owner(db, retrieval=retrieval),
|
|
531
|
+
"temporal": TemporalOwner(db),
|
|
532
|
+
"vector": VectorOwner(
|
|
533
|
+
db,
|
|
534
|
+
vector_store=getattr(engine, "_vector_store", None),
|
|
535
|
+
ann_index=getattr(engine, "_ann_index", None),
|
|
536
|
+
),
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
def build_transaction_service(engine: Any) -> MemoryTransactionService:
|
|
541
|
+
return MemoryTransactionService(_admission_owners(engine))
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
def build_erasure_service(engine: Any) -> ErasureService:
|
|
545
|
+
audit_logger = _audit_chain_logger()
|
|
546
|
+
return ErasureService(_admission_owners(engine), audit_logger=audit_logger)
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
def build_erasure_service_for_db(db: Any, engine: Any = None) -> ErasureService:
|
|
550
|
+
"""Build an ErasureService given a db wrapper and an optional engine.
|
|
551
|
+
|
|
552
|
+
Used by GDPR erasure paths that have a db handle but may not have an
|
|
553
|
+
engine reference (e.g. entity erase from GDPRCompliance).
|
|
554
|
+
"""
|
|
555
|
+
audit_logger = _audit_chain_logger()
|
|
556
|
+
owners: dict[str, Any] = {
|
|
557
|
+
"bm25": Bm25Owner(db),
|
|
558
|
+
"temporal": TemporalOwner(db),
|
|
559
|
+
"vector": VectorOwner(
|
|
560
|
+
db,
|
|
561
|
+
vector_store=getattr(engine, "_vector_store", None),
|
|
562
|
+
ann_index=getattr(engine, "_ann_index", None),
|
|
563
|
+
),
|
|
564
|
+
}
|
|
565
|
+
return ErasureService(owners, audit_logger=audit_logger)
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
def _audit_chain_logger() -> Any:
|
|
569
|
+
def _log(event: dict[str, Any]) -> None:
|
|
570
|
+
from superlocalmemory.compliance.audit import AuditChain
|
|
571
|
+
from superlocalmemory.infra.data_root import state_path
|
|
572
|
+
|
|
573
|
+
AuditChain(str(state_path("audit_chain.db"))).log(
|
|
574
|
+
"projection_erase",
|
|
575
|
+
agent_id=str(event.get("requested_by", "")),
|
|
576
|
+
profile_id=str(event.get("profile_id", "")),
|
|
577
|
+
content_hash=str(event.get("audit_hash", "")),
|
|
578
|
+
metadata={
|
|
579
|
+
"erasure_id": event.get("erasure_id", ""),
|
|
580
|
+
"subject_type": event.get("subject_type", ""),
|
|
581
|
+
"subject_id": event.get("subject_id", ""),
|
|
582
|
+
"state": event.get("state", ""),
|
|
583
|
+
},
|
|
584
|
+
)
|
|
585
|
+
|
|
586
|
+
return _log
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
__all__ = [
|
|
590
|
+
"REQUIRED_ADMISSION_OWNERS",
|
|
591
|
+
"Bm25Owner",
|
|
592
|
+
"TemporalOwner",
|
|
593
|
+
"VectorOwner",
|
|
594
|
+
"build_erasure_service",
|
|
595
|
+
"build_erasure_service_for_db",
|
|
596
|
+
"build_transaction_service",
|
|
597
|
+
]
|