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
|
@@ -16,18 +16,33 @@ Part of Qualixar | Author: Varun Pratap Bhardwaj
|
|
|
16
16
|
|
|
17
17
|
from __future__ import annotations
|
|
18
18
|
|
|
19
|
+
import hashlib
|
|
20
|
+
import itertools
|
|
19
21
|
import json
|
|
20
22
|
import logging
|
|
21
23
|
import os as _os
|
|
24
|
+
import sqlite3
|
|
25
|
+
import tempfile
|
|
22
26
|
import time
|
|
23
27
|
from pathlib import Path
|
|
24
28
|
from typing import TYPE_CHECKING, Any
|
|
25
29
|
|
|
30
|
+
import numpy as np
|
|
31
|
+
|
|
26
32
|
if TYPE_CHECKING:
|
|
27
33
|
from superlocalmemory.core.config import SLMConfig
|
|
28
34
|
|
|
29
35
|
logger = logging.getLogger(__name__)
|
|
30
36
|
|
|
37
|
+
|
|
38
|
+
class EmbeddingMigrationAborted(RuntimeError):
|
|
39
|
+
"""Raised when embedding migration aborts; old signature stays active.
|
|
40
|
+
|
|
41
|
+
Distinct from a no-op return of ``0`` (no embedder / no facts). Callers
|
|
42
|
+
must treat this as failure, not success.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
|
|
31
46
|
# ---------------------------------------------------------------------------
|
|
32
47
|
# Backfill constants
|
|
33
48
|
# ---------------------------------------------------------------------------
|
|
@@ -60,6 +75,155 @@ _NO_MODEL = ""
|
|
|
60
75
|
_REINDEX_BATCH_SIZE = 50
|
|
61
76
|
|
|
62
77
|
|
|
78
|
+
def _activate_staged_vectors(
|
|
79
|
+
config: SLMConfig,
|
|
80
|
+
db: Any,
|
|
81
|
+
stage_path: Path,
|
|
82
|
+
expected_count: int,
|
|
83
|
+
) -> None:
|
|
84
|
+
"""Atomically replace canonical and sqlite-vec embeddings from a shadow DB."""
|
|
85
|
+
db_path = getattr(db, "db_path", None)
|
|
86
|
+
if not isinstance(db_path, (str, Path)):
|
|
87
|
+
raise RuntimeError("embedding migration requires an authoritative db_path")
|
|
88
|
+
|
|
89
|
+
import sqlite_vec
|
|
90
|
+
|
|
91
|
+
from superlocalmemory.storage.write_lock import get_write_lock
|
|
92
|
+
|
|
93
|
+
db_path = Path(db_path)
|
|
94
|
+
with get_write_lock(db_path), sqlite3.connect(stage_path) as stage:
|
|
95
|
+
conn = sqlite3.connect(db_path)
|
|
96
|
+
try:
|
|
97
|
+
conn.execute("PRAGMA busy_timeout=10000")
|
|
98
|
+
conn.enable_load_extension(True)
|
|
99
|
+
sqlite_vec.load(conn)
|
|
100
|
+
conn.enable_load_extension(False)
|
|
101
|
+
conn.execute("BEGIN IMMEDIATE")
|
|
102
|
+
canonical = conn.execute(
|
|
103
|
+
"SELECT fact_id, profile_id, content "
|
|
104
|
+
"FROM atomic_facts ORDER BY fact_id"
|
|
105
|
+
)
|
|
106
|
+
staged = stage.execute(
|
|
107
|
+
"SELECT fact_id, profile_id, content_hash "
|
|
108
|
+
"FROM staged_embeddings ORDER BY fact_id"
|
|
109
|
+
)
|
|
110
|
+
for current, shadow in itertools.zip_longest(canonical, staged):
|
|
111
|
+
if current is None or shadow is None:
|
|
112
|
+
raise RuntimeError("canonical fact set changed during migration")
|
|
113
|
+
current_key = (str(current[0]), str(current[1]))
|
|
114
|
+
shadow_key = (str(shadow[0]), str(shadow[1]))
|
|
115
|
+
current_hash = hashlib.sha256(str(current[2]).encode("utf-8")).hexdigest()
|
|
116
|
+
if current_key != shadow_key or current_hash != str(shadow[2]):
|
|
117
|
+
raise RuntimeError(
|
|
118
|
+
f"canonical fact changed during migration: {current_key[0]}"
|
|
119
|
+
)
|
|
120
|
+
conn.execute("DROP TABLE IF EXISTS embedding_metadata")
|
|
121
|
+
conn.execute("DROP TABLE IF EXISTS vector_row_map")
|
|
122
|
+
conn.execute("DROP TABLE IF EXISTS fact_embeddings")
|
|
123
|
+
conn.execute(
|
|
124
|
+
"CREATE VIRTUAL TABLE fact_embeddings USING vec0("
|
|
125
|
+
"profile_id TEXT PARTITION KEY, "
|
|
126
|
+
f"embedding float[{config.embedding.dimension}] distance_metric=cosine)"
|
|
127
|
+
)
|
|
128
|
+
conn.execute(
|
|
129
|
+
"CREATE TABLE embedding_metadata ("
|
|
130
|
+
"vec_rowid INTEGER PRIMARY KEY, fact_id TEXT NOT NULL UNIQUE, "
|
|
131
|
+
"profile_id TEXT NOT NULL DEFAULT 'default', "
|
|
132
|
+
"model_name TEXT NOT NULL DEFAULT '', "
|
|
133
|
+
"dimension INTEGER NOT NULL DEFAULT 768, "
|
|
134
|
+
"created_at TEXT NOT NULL DEFAULT (datetime('now')))"
|
|
135
|
+
)
|
|
136
|
+
conn.execute(
|
|
137
|
+
"CREATE INDEX idx_embmeta_fact ON embedding_metadata (fact_id)"
|
|
138
|
+
)
|
|
139
|
+
conn.execute(
|
|
140
|
+
"CREATE INDEX idx_embmeta_profile ON embedding_metadata (profile_id)"
|
|
141
|
+
)
|
|
142
|
+
conn.execute(
|
|
143
|
+
"CREATE TABLE vector_row_map ("
|
|
144
|
+
"fact_id TEXT NOT NULL PRIMARY KEY, profile_id TEXT NOT NULL, "
|
|
145
|
+
"vec_rowid INTEGER NOT NULL)"
|
|
146
|
+
)
|
|
147
|
+
conn.execute(
|
|
148
|
+
"CREATE INDEX idx_vector_row_map_profile "
|
|
149
|
+
"ON vector_row_map (profile_id)"
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
first_probe: tuple[bytes, str] | None = None
|
|
153
|
+
activated = 0
|
|
154
|
+
for rowid, (fact_id, profile_id, embedding_json) in enumerate(
|
|
155
|
+
stage.execute(
|
|
156
|
+
"SELECT fact_id, profile_id, embedding "
|
|
157
|
+
"FROM staged_embeddings ORDER BY fact_id"
|
|
158
|
+
),
|
|
159
|
+
start=1,
|
|
160
|
+
):
|
|
161
|
+
vector = json.loads(embedding_json)
|
|
162
|
+
vec_bytes = np.asarray(vector, dtype=np.float32).tobytes()
|
|
163
|
+
conn.execute(
|
|
164
|
+
"INSERT INTO fact_embeddings(rowid, profile_id, embedding) "
|
|
165
|
+
"VALUES (?, ?, ?)",
|
|
166
|
+
(rowid, profile_id, vec_bytes),
|
|
167
|
+
)
|
|
168
|
+
conn.execute(
|
|
169
|
+
"INSERT INTO embedding_metadata "
|
|
170
|
+
"(vec_rowid, fact_id, profile_id, model_name, dimension) "
|
|
171
|
+
"VALUES (?, ?, ?, ?, ?)",
|
|
172
|
+
(
|
|
173
|
+
rowid,
|
|
174
|
+
fact_id,
|
|
175
|
+
profile_id,
|
|
176
|
+
config.embedding.model_name,
|
|
177
|
+
config.embedding.dimension,
|
|
178
|
+
),
|
|
179
|
+
)
|
|
180
|
+
conn.execute(
|
|
181
|
+
"INSERT INTO vector_row_map (fact_id, profile_id, vec_rowid) "
|
|
182
|
+
"VALUES (?, ?, ?)",
|
|
183
|
+
(fact_id, profile_id, rowid),
|
|
184
|
+
)
|
|
185
|
+
updated = conn.execute(
|
|
186
|
+
"UPDATE atomic_facts SET embedding = ? "
|
|
187
|
+
"WHERE fact_id = ? AND profile_id = ?",
|
|
188
|
+
(embedding_json, fact_id, profile_id),
|
|
189
|
+
)
|
|
190
|
+
if updated.rowcount != 1:
|
|
191
|
+
raise RuntimeError(
|
|
192
|
+
f"canonical fact changed during migration: {fact_id}"
|
|
193
|
+
)
|
|
194
|
+
if first_probe is None:
|
|
195
|
+
first_probe = (vec_bytes, profile_id)
|
|
196
|
+
activated += 1
|
|
197
|
+
|
|
198
|
+
counts = [
|
|
199
|
+
int(conn.execute(f"SELECT COUNT(*) FROM {table}").fetchone()[0])
|
|
200
|
+
for table in (
|
|
201
|
+
"fact_embeddings",
|
|
202
|
+
"embedding_metadata",
|
|
203
|
+
"vector_row_map",
|
|
204
|
+
)
|
|
205
|
+
]
|
|
206
|
+
if activated != expected_count or any(c != expected_count for c in counts):
|
|
207
|
+
raise RuntimeError(
|
|
208
|
+
f"vector activation incomplete: activated={activated}, "
|
|
209
|
+
f"projection_counts={counts}, expected={expected_count}"
|
|
210
|
+
)
|
|
211
|
+
if first_probe is not None:
|
|
212
|
+
probe = conn.execute(
|
|
213
|
+
"SELECT rowid FROM fact_embeddings "
|
|
214
|
+
"WHERE embedding MATCH ? AND profile_id = ? AND k = 1",
|
|
215
|
+
(first_probe[0], first_probe[1]),
|
|
216
|
+
).fetchone()
|
|
217
|
+
if probe is None:
|
|
218
|
+
raise RuntimeError("post-activation KNN probe returned no row")
|
|
219
|
+
conn.commit()
|
|
220
|
+
except Exception:
|
|
221
|
+
conn.rollback()
|
|
222
|
+
raise
|
|
223
|
+
finally:
|
|
224
|
+
conn.close()
|
|
225
|
+
|
|
226
|
+
|
|
63
227
|
def _model_signature(config: SLMConfig) -> str:
|
|
64
228
|
"""Derive a deterministic signature from the active embedding config.
|
|
65
229
|
|
|
@@ -160,26 +324,28 @@ def run_embedding_migration(
|
|
|
160
324
|
db: Any,
|
|
161
325
|
embedder: Any,
|
|
162
326
|
) -> int:
|
|
163
|
-
"""
|
|
164
|
-
|
|
165
|
-
Processes facts in batches to avoid memory spikes. Updates the
|
|
166
|
-
embedding_metadata table and vector store for each fact.
|
|
327
|
+
"""Stage and atomically activate embeddings for the current model.
|
|
167
328
|
|
|
168
|
-
|
|
329
|
+
Embedding is performed in bounded batches into a temporary shadow store.
|
|
330
|
+
Canonical rows are changed only after every target fact has a valid vector;
|
|
331
|
+
the activation itself runs in one database transaction. A failed batch,
|
|
332
|
+
malformed vector set, or database write therefore leaves both the old
|
|
333
|
+
embeddings and the old model signature active.
|
|
169
334
|
"""
|
|
170
335
|
if embedder is None:
|
|
171
336
|
logger.warning("No embedder available. Skipping re-indexing.")
|
|
172
337
|
return 0
|
|
173
338
|
|
|
174
339
|
current_sig = _model_signature(config)
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
# Get all fact IDs that need re-embedding (all facts for the profile).
|
|
340
|
+
# Embedding configuration is database-wide. Rebuild every profile so one
|
|
341
|
+
# vec0 table never contains vectors from mixed model spaces.
|
|
178
342
|
rows = db.execute(
|
|
179
|
-
"SELECT fact_id, content FROM atomic_facts
|
|
180
|
-
(profile_id,),
|
|
343
|
+
"SELECT fact_id, profile_id, content FROM atomic_facts ORDER BY created_at",
|
|
181
344
|
)
|
|
182
|
-
facts = [
|
|
345
|
+
facts = [
|
|
346
|
+
(dict(r)["fact_id"], dict(r)["profile_id"], dict(r)["content"])
|
|
347
|
+
for r in rows
|
|
348
|
+
]
|
|
183
349
|
total = len(facts)
|
|
184
350
|
|
|
185
351
|
if total == 0:
|
|
@@ -193,54 +359,83 @@ def run_embedding_migration(
|
|
|
193
359
|
_REINDEX_BATCH_SIZE,
|
|
194
360
|
)
|
|
195
361
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
i + len(batch),
|
|
209
|
-
exc,
|
|
210
|
-
)
|
|
211
|
-
break
|
|
212
|
-
|
|
213
|
-
for j, (fid, vec) in enumerate(zip(fact_ids, vectors)):
|
|
214
|
-
if vec is None:
|
|
215
|
-
continue
|
|
216
|
-
# Update embedding in the database (embedding column on atomic_facts).
|
|
217
|
-
try:
|
|
218
|
-
embedding_json = json.dumps(vec)
|
|
219
|
-
db.execute(
|
|
220
|
-
"UPDATE atomic_facts SET embedding = ? WHERE fact_id = ?",
|
|
221
|
-
(embedding_json, fid),
|
|
222
|
-
)
|
|
223
|
-
# Update embedding_metadata with new model name.
|
|
224
|
-
db.execute(
|
|
225
|
-
"UPDATE embedding_metadata SET model_name = ? WHERE fact_id = ?",
|
|
226
|
-
(config.embedding.model_name, fid),
|
|
362
|
+
config.base_dir.mkdir(parents=True, exist_ok=True)
|
|
363
|
+
try:
|
|
364
|
+
with tempfile.TemporaryDirectory(
|
|
365
|
+
prefix="embedding-migration-",
|
|
366
|
+
dir=config.base_dir,
|
|
367
|
+
) as stage_dir:
|
|
368
|
+
stage_path = Path(stage_dir) / "shadow.sqlite3"
|
|
369
|
+
with sqlite3.connect(stage_path) as stage:
|
|
370
|
+
stage.execute(
|
|
371
|
+
"CREATE TABLE staged_embeddings ("
|
|
372
|
+
"fact_id TEXT PRIMARY KEY, profile_id TEXT NOT NULL, "
|
|
373
|
+
"content_hash TEXT NOT NULL, embedding TEXT NOT NULL)"
|
|
227
374
|
)
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
375
|
+
for i in range(0, total, _REINDEX_BATCH_SIZE):
|
|
376
|
+
batch = facts[i : i + _REINDEX_BATCH_SIZE]
|
|
377
|
+
texts = [content for _, _, content in batch]
|
|
378
|
+
fact_ids = [fid for fid, _, _ in batch]
|
|
379
|
+
profile_ids = [pid for _, pid, _ in batch]
|
|
380
|
+
vectors = list(embedder.embed_batch(texts))
|
|
381
|
+
if len(vectors) != len(batch):
|
|
382
|
+
raise ValueError(
|
|
383
|
+
"embedder returned "
|
|
384
|
+
f"{len(vectors)} vectors for {len(batch)} facts"
|
|
385
|
+
)
|
|
386
|
+
staged_rows: list[tuple[str, str, str, str]] = []
|
|
387
|
+
for fid, profile_id, content, vec in zip(
|
|
388
|
+
fact_ids, profile_ids, texts, vectors, strict=True
|
|
389
|
+
):
|
|
390
|
+
if vec is None or len(vec) != config.embedding.dimension:
|
|
391
|
+
raise ValueError(
|
|
392
|
+
f"invalid embedding for fact {fid[:16]}: "
|
|
393
|
+
f"expected dimension {config.embedding.dimension}"
|
|
394
|
+
)
|
|
395
|
+
staged_rows.append(
|
|
396
|
+
(
|
|
397
|
+
fid,
|
|
398
|
+
profile_id,
|
|
399
|
+
hashlib.sha256(content.encode("utf-8")).hexdigest(),
|
|
400
|
+
json.dumps([float(value) for value in vec]),
|
|
401
|
+
)
|
|
402
|
+
)
|
|
403
|
+
stage.executemany(
|
|
404
|
+
"INSERT INTO staged_embeddings "
|
|
405
|
+
"(fact_id, profile_id, content_hash, embedding) "
|
|
406
|
+
"VALUES (?, ?, ?, ?)",
|
|
407
|
+
staged_rows,
|
|
408
|
+
)
|
|
409
|
+
stage.commit()
|
|
410
|
+
|
|
411
|
+
staged_count = int(
|
|
412
|
+
stage.execute("SELECT COUNT(*) FROM staged_embeddings").fetchone()[0]
|
|
234
413
|
)
|
|
414
|
+
if staged_count != total:
|
|
415
|
+
raise ValueError(
|
|
416
|
+
f"shadow migration incomplete: {staged_count}/{total} facts"
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
_activate_staged_vectors(config, db, stage_path, total)
|
|
420
|
+
except Exception as exc:
|
|
421
|
+
logger.error(
|
|
422
|
+
"Embedding migration aborted; previous embedding space remains active: %s",
|
|
423
|
+
exc,
|
|
424
|
+
)
|
|
425
|
+
# Do NOT write a new signature — old embedding space stays active.
|
|
426
|
+
# Raise so callers can distinguish abort from no-op (return 0).
|
|
427
|
+
raise EmbeddingMigrationAborted(
|
|
428
|
+
"Embedding migration aborted; previous embedding space remains "
|
|
429
|
+
f"active: {exc}"
|
|
430
|
+
) from exc
|
|
235
431
|
|
|
236
|
-
# Update stored signature after successful migration.
|
|
237
432
|
_write_stored_signature(config.base_dir, current_sig)
|
|
238
433
|
logger.info(
|
|
239
434
|
"Embedding migration complete: %d/%d facts re-embedded.",
|
|
240
|
-
|
|
435
|
+
total,
|
|
241
436
|
total,
|
|
242
437
|
)
|
|
243
|
-
return
|
|
438
|
+
return total
|
|
244
439
|
|
|
245
440
|
|
|
246
441
|
# ---------------------------------------------------------------------------
|
|
@@ -0,0 +1,45 @@
|
|
|
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 threading
|
|
7
|
+
import time
|
|
8
|
+
|
|
9
|
+
_TTL_SECONDS = 300.0
|
|
10
|
+
|
|
11
|
+
_lock = threading.RLock()
|
|
12
|
+
_marks: dict[tuple[str, str], float] = {}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def mark_erasing(profile_id: str, fact_id: str) -> None:
|
|
16
|
+
now = time.time()
|
|
17
|
+
with _lock:
|
|
18
|
+
_marks[(profile_id, fact_id)] = now
|
|
19
|
+
_prune(now)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def clear_erasing(profile_id: str, fact_id: str) -> None:
|
|
23
|
+
with _lock:
|
|
24
|
+
_marks.pop((profile_id, fact_id), None)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def is_erasing(profile_id: str, fact_id: str) -> bool:
|
|
28
|
+
now = time.time()
|
|
29
|
+
with _lock:
|
|
30
|
+
ts = _marks.get((profile_id, fact_id))
|
|
31
|
+
if ts is None:
|
|
32
|
+
return False
|
|
33
|
+
if now - ts > _TTL_SECONDS:
|
|
34
|
+
_marks.pop((profile_id, fact_id), None)
|
|
35
|
+
return False
|
|
36
|
+
return True
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _prune(now: float) -> None:
|
|
40
|
+
expired = [key for key, ts in _marks.items() if now - ts > _TTL_SECONDS]
|
|
41
|
+
for key in expired:
|
|
42
|
+
_marks.pop(key, None)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
__all__ = ["mark_erasing", "clear_erasing", "is_erasing"]
|
|
@@ -0,0 +1,63 @@
|
|
|
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 threading
|
|
7
|
+
import time
|
|
8
|
+
|
|
9
|
+
_TTL_SECONDS = 300.0
|
|
10
|
+
# Sentinel epoch that can never equal a real generation (which is >= 0), so a
|
|
11
|
+
# conflicted key is always rejected by the fence.
|
|
12
|
+
_CONFLICT_EPOCH = -1
|
|
13
|
+
|
|
14
|
+
_lock = threading.RLock()
|
|
15
|
+
_epochs: dict[tuple[str, str], tuple[int, float]] = {}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def record_admission_epoch(profile_id: str, idempotency_key: str, epoch: int) -> None:
|
|
19
|
+
if not idempotency_key:
|
|
20
|
+
return
|
|
21
|
+
now = time.time()
|
|
22
|
+
with _lock:
|
|
23
|
+
_prune(now)
|
|
24
|
+
key = (profile_id, idempotency_key)
|
|
25
|
+
existing = _epochs.get(key)
|
|
26
|
+
if existing is not None and existing[0] not in (epoch, _CONFLICT_EPOCH):
|
|
27
|
+
# Two concurrent admits recorded different epochs for one key: fail
|
|
28
|
+
# closed so neither can satisfy the fence — both retry against the
|
|
29
|
+
# current binding rather than letting a stale epoch slip through.
|
|
30
|
+
_epochs[key] = (_CONFLICT_EPOCH, now)
|
|
31
|
+
else:
|
|
32
|
+
_epochs.setdefault(key, (epoch, now))
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def admitted_epoch(profile_id: str, idempotency_key: str) -> int | None:
|
|
36
|
+
if not idempotency_key:
|
|
37
|
+
return None
|
|
38
|
+
now = time.time()
|
|
39
|
+
with _lock:
|
|
40
|
+
entry = _epochs.get((profile_id, idempotency_key))
|
|
41
|
+
if entry is None:
|
|
42
|
+
return None
|
|
43
|
+
epoch, ts = entry
|
|
44
|
+
if now - ts > _TTL_SECONDS:
|
|
45
|
+
_epochs.pop((profile_id, idempotency_key), None)
|
|
46
|
+
return None
|
|
47
|
+
return epoch
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def clear_admission_epoch(profile_id: str, idempotency_key: str) -> None:
|
|
51
|
+
if not idempotency_key:
|
|
52
|
+
return
|
|
53
|
+
with _lock:
|
|
54
|
+
_epochs.pop((profile_id, idempotency_key), None)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _prune(now: float) -> None:
|
|
58
|
+
expired = [key for key, (_, ts) in _epochs.items() if now - ts > _TTL_SECONDS]
|
|
59
|
+
for key in expired:
|
|
60
|
+
_epochs.pop(key, None)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
__all__ = ["record_admission_epoch", "admitted_epoch", "clear_admission_epoch"]
|