superlocalmemory 3.7.6 → 3.7.7

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.
Files changed (29) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +2 -2
  3. package/package.json +2 -2
  4. package/plugin/.claude-plugin/plugin.json +1 -1
  5. package/plugin/requirements.txt +1 -1
  6. package/plugin-src/manifest.json +1 -1
  7. package/plugin-src/requirements.txt +1 -1
  8. package/pyproject.toml +6 -6
  9. package/src/superlocalmemory/__init__.py +1 -1
  10. package/src/superlocalmemory/cli/commands.py +169 -9
  11. package/src/superlocalmemory/cli/setup_wizard.py +18 -1
  12. package/src/superlocalmemory/infra/auth_middleware.py +5 -5
  13. package/src/superlocalmemory/mcp/_daemon_proxy.py +8 -10
  14. package/src/superlocalmemory/mcp/server.py +1 -0
  15. package/src/superlocalmemory/mcp/tools_core.py +178 -20
  16. package/src/superlocalmemory/optimize/cache/centroid_store.py +21 -3
  17. package/src/superlocalmemory/optimize/cache/manager.py +7 -0
  18. package/src/superlocalmemory/optimize/cache/semantic.py +27 -10
  19. package/src/superlocalmemory/server/profile_runtime.py +384 -0
  20. package/src/superlocalmemory/server/recall_health.py +12 -6
  21. package/src/superlocalmemory/server/routes/helpers.py +9 -16
  22. package/src/superlocalmemory/server/routes/profiles.py +24 -14
  23. package/src/superlocalmemory/server/routes/v3_api.py +97 -20
  24. package/src/superlocalmemory/server/unified_daemon.py +243 -55
  25. package/src/superlocalmemory/storage/migration_runner.py +17 -3
  26. package/src/superlocalmemory/storage/migrations/M002_model_state_history.py +32 -3
  27. package/src/superlocalmemory/ui/index.html +32 -1
  28. package/src/superlocalmemory/ui/js/auto-settings.js +48 -0
  29. package/src/superlocalmemory/ui/js/profiles.js +11 -2
@@ -25,6 +25,27 @@ from superlocalmemory.mcp.shared import authorize_mcp_mutation
25
25
 
26
26
  logger = logging.getLogger(__name__)
27
27
 
28
+
29
+ async def _runtime_profile(get_engine: Callable, explicit: str = "") -> str:
30
+ """Resolve an MCP default profile from daemon runtime truth."""
31
+ if explicit:
32
+ return explicit
33
+ import asyncio
34
+
35
+ try:
36
+ from superlocalmemory.cli.daemon import daemon_request, is_daemon_running
37
+
38
+ if await asyncio.to_thread(is_daemon_running):
39
+ status = await asyncio.to_thread(daemon_request, "GET", "/status")
40
+ if isinstance(status, dict) and status.get("profile"):
41
+ return str(status["profile"])
42
+ raise RuntimeError("resident daemon did not report its active profile")
43
+ except RuntimeError:
44
+ raise
45
+ except Exception as exc:
46
+ logger.debug("daemon profile resolution failed: %s", exc)
47
+ return str(get_engine().profile_id)
48
+
28
49
  def _emit_event(event_type: str, payload: dict | None = None,
29
50
  source_agent: str = "mcp_client") -> None:
30
51
  """Emit an event to the EventBus (best-effort, never raises)."""
@@ -42,6 +63,7 @@ def _record_recall_hits(
42
63
  query: str,
43
64
  results: list[dict],
44
65
  *,
66
+ profile_id: str = "",
45
67
  query_id: str = "",
46
68
  fact_ids_candidates: list[str] | None = None,
47
69
  ) -> None:
@@ -63,7 +85,7 @@ def _record_recall_hits(
63
85
  )
64
86
 
65
87
  engine = get_engine()
66
- pid = engine.profile_id
88
+ pid = profile_id or engine.profile_id
67
89
  slm_dir = canonical_data_root()
68
90
 
69
91
  shown_ids = [r.get("fact_id", "") for r in results[:10]
@@ -335,7 +357,12 @@ def register_core_tools(server, get_engine: Callable) -> None:
335
357
  if result.get("ok"):
336
358
  # Record implicit feedback: every returned result is a recall_hit
337
359
  try:
338
- _record_recall_hits(get_engine, query, result.get("results", []))
360
+ _record_recall_hits(
361
+ get_engine,
362
+ query,
363
+ result.get("results", []),
364
+ profile_id=str(result.get("profile", "")),
365
+ )
339
366
  except Exception:
340
367
  pass # Feedback is non-critical, never block recall
341
368
  _emit_event("memory.recalled", {
@@ -370,7 +397,7 @@ def register_core_tools(server, get_engine: Callable) -> None:
370
397
  """Full-text search across memories using FTS5 with BM25 ranking."""
371
398
  try:
372
399
  engine = get_engine()
373
- pid = profile_id or engine.profile_id
400
+ pid = await _runtime_profile(get_engine, profile_id)
374
401
  facts = engine._db.search_facts_fts(query, pid, limit=limit)
375
402
  items = []
376
403
  for f in facts:
@@ -392,7 +419,8 @@ def register_core_tools(server, get_engine: Callable) -> None:
392
419
  try:
393
420
  engine = get_engine()
394
421
  ids = [fid.strip() for fid in fact_ids.split(",") if fid.strip()]
395
- facts = engine._db.get_facts_by_ids(ids, engine.profile_id)
422
+ pid = await _runtime_profile(get_engine)
423
+ facts = engine._db.get_facts_by_ids(ids, pid)
396
424
  items = []
397
425
  for f in facts:
398
426
  items.append({
@@ -417,7 +445,7 @@ def register_core_tools(server, get_engine: Callable) -> None:
417
445
  """List most recently stored memories, newest first."""
418
446
  try:
419
447
  engine = get_engine()
420
- pid = profile_id or engine.profile_id
448
+ pid = await _runtime_profile(get_engine, profile_id)
421
449
  # v3.6.12 (search-2): push the limit into the query — was loading the
422
450
  # ENTIRE facts table (deserializing every 768-float embedding) just
423
451
  # to return the top N. get_all_facts preserves created_at DESC order.
@@ -440,7 +468,37 @@ def register_core_tools(server, get_engine: Callable) -> None:
440
468
  async def get_status() -> dict:
441
469
  """Get memory system status: fact count, entity count, mode, profile, db size."""
442
470
  try:
471
+ import asyncio
443
472
  import os
473
+
474
+ from superlocalmemory.cli.daemon import (
475
+ daemon_request,
476
+ is_daemon_running,
477
+ )
478
+
479
+ if await asyncio.to_thread(is_daemon_running):
480
+ daemon_status = await asyncio.to_thread(
481
+ daemon_request,
482
+ "GET",
483
+ "/status",
484
+ )
485
+ if isinstance(daemon_status, dict) and daemon_status.get("profile"):
486
+ return {
487
+ "success": True,
488
+ "mode": daemon_status.get("mode", "unknown"),
489
+ "provider": daemon_status.get("provider", "none"),
490
+ "profile": daemon_status["profile"],
491
+ "base_dir": daemon_status.get("base_dir", ""),
492
+ "db_path": daemon_status.get("db_path", ""),
493
+ "db_size_mb": float(daemon_status.get("db_size_mb", 0.0)),
494
+ "fact_count": int(daemon_status.get("fact_count", 0)),
495
+ "entity_count": int(daemon_status.get("entity_count", 0)),
496
+ "edge_count": int(daemon_status.get("edge_count", 0)),
497
+ "profile_generation": int(
498
+ daemon_status.get("profile_generation", 0)
499
+ ),
500
+ }
501
+
444
502
  engine = get_engine()
445
503
  pid = engine.profile_id
446
504
  fact_count = engine._db.get_fact_count(pid)
@@ -474,6 +532,7 @@ def register_core_tools(server, get_engine: Callable) -> None:
474
532
  "fact_count": fact_count,
475
533
  "entity_count": entity_count,
476
534
  "edge_count": edge_count,
535
+ "profile_generation": 0,
477
536
  }
478
537
  except Exception as exc:
479
538
  logger.exception("get_status failed")
@@ -484,7 +543,7 @@ def register_core_tools(server, get_engine: Callable) -> None:
484
543
  """Rebuild knowledge graph edges for all facts in the active profile."""
485
544
  try:
486
545
  engine = get_engine()
487
- pid = profile_id or engine.profile_id
546
+ pid = await _runtime_profile(get_engine, profile_id)
488
547
  authorization = authorize_mcp_mutation(
489
548
  engine,
490
549
  "update",
@@ -511,6 +570,8 @@ def register_core_tools(server, get_engine: Callable) -> None:
511
570
  async def switch_profile(profile_id: str) -> dict:
512
571
  """Switch the active memory profile. All operations scope to this profile."""
513
572
  try:
573
+ import asyncio
574
+
514
575
  engine = get_engine()
515
576
  old = engine.profile_id
516
577
  authorization = authorize_mcp_mutation(
@@ -520,18 +581,57 @@ def register_core_tools(server, get_engine: Callable) -> None:
520
581
  profile_id=profile_id,
521
582
  content_preview=f"{old} -> {profile_id}",
522
583
  )
523
- engine.profile_id = profile_id
584
+ from superlocalmemory.cli.daemon import (
585
+ daemon_request,
586
+ is_daemon_running,
587
+ )
524
588
 
525
- # Persist to both config stores so CLI and Dashboard stay in sync
526
- try:
527
- from superlocalmemory.server.routes.helpers import (
528
- ensure_profile_in_db, set_active_profile_everywhere,
589
+ generation = 0
590
+ if await asyncio.to_thread(is_daemon_running):
591
+ result = await asyncio.to_thread(
592
+ daemon_request,
593
+ "POST",
594
+ f"/api/profiles/{profile_id}/switch",
595
+ )
596
+ if not result or not result.get("success"):
597
+ return {
598
+ "success": False,
599
+ "error": "resident daemon rejected the profile switch",
600
+ }
601
+ acknowledged = str(result.get("active_profile", ""))
602
+ if acknowledged != profile_id:
603
+ return {
604
+ "success": False,
605
+ "error": "resident daemon acknowledged a different profile",
606
+ }
607
+ generation = int(result.get("generation", 0))
608
+ else:
609
+ rows = engine._db.execute(
610
+ "SELECT 1 FROM profiles WHERE profile_id = ?",
611
+ (profile_id,),
612
+ )
613
+ if not rows:
614
+ return {
615
+ "success": False,
616
+ "error": f"Profile '{profile_id}' does not exist.",
617
+ }
618
+ from superlocalmemory.server.profile_runtime import (
619
+ persist_active_profile,
529
620
  )
530
- ensure_profile_in_db(profile_id)
531
- set_active_profile_everywhere(profile_id)
532
- except ImportError:
533
- # Dashboard not installed — profile switch still works for MCP/CLI
534
- logger.debug("Dashboard routes not available, profile set in engine only")
621
+
622
+ persistence = persist_active_profile(profile_id)
623
+ try:
624
+ engine.profile_id = profile_id
625
+ engine._config.active_profile = profile_id
626
+ except BaseException:
627
+ engine.profile_id = old
628
+ engine._config.active_profile = old
629
+ persistence.rollback()
630
+ raise
631
+
632
+ # Synchronize this MCP process only after daemon acknowledgement.
633
+ engine.profile_id = profile_id
634
+ engine._config.active_profile = profile_id
535
635
 
536
636
  # v3.6.12 (search-3): recall/delete run in a separate worker
537
637
  # subprocess that caches its engine (and profile_id) at init. Recycle
@@ -547,6 +647,7 @@ def register_core_tools(server, get_engine: Callable) -> None:
547
647
  "success": True,
548
648
  "previous_profile": old,
549
649
  "current_profile": profile_id,
650
+ "generation": generation,
550
651
  }
551
652
  except Exception as exc:
552
653
  logger.exception("switch_profile failed")
@@ -569,7 +670,7 @@ def register_core_tools(server, get_engine: Callable) -> None:
569
670
  """Get memory usage breakdown by fact type and lifecycle state."""
570
671
  try:
571
672
  engine = get_engine()
572
- pid = engine.profile_id
673
+ pid = await _runtime_profile(get_engine)
573
674
  facts = engine._db.get_all_facts(pid)
574
675
  by_type: dict[str, int] = {}
575
676
  by_lifecycle: dict[str, int] = {}
@@ -594,11 +695,12 @@ def register_core_tools(server, get_engine: Callable) -> None:
594
695
  """Get learned behavioral patterns (interests, refinements, archival habits)."""
595
696
  try:
596
697
  engine = get_engine()
698
+ pid = await _runtime_profile(get_engine)
597
699
  from superlocalmemory.learning.behavioral import BehavioralPatternStore
598
700
  store = BehavioralPatternStore(engine._db.db_path)
599
701
  ptype = pattern_type if pattern_type else None
600
702
  patterns = store.get_patterns(
601
- engine.profile_id, pattern_type=ptype, limit=limit,
703
+ pid, pattern_type=ptype, limit=limit,
602
704
  )
603
705
  return {"success": True, "patterns": patterns, "count": len(patterns)}
604
706
  except Exception as exc:
@@ -610,18 +712,19 @@ def register_core_tools(server, get_engine: Callable) -> None:
610
712
  """Correct or annotate a learned behavioral pattern to improve retrieval."""
611
713
  try:
612
714
  engine = get_engine()
715
+ pid = await _runtime_profile(get_engine)
613
716
  authorization = authorize_mcp_mutation(
614
717
  engine,
615
718
  "update",
616
719
  mutation_source="mcp-correct-pattern",
617
- profile_id=engine.profile_id,
720
+ profile_id=pid,
618
721
  fact_id=pattern_id,
619
722
  content_preview=correction,
620
723
  )
621
724
  from superlocalmemory.learning.behavioral import BehavioralPatternStore
622
725
  store = BehavioralPatternStore(engine._db.db_path)
623
726
  store.record(
624
- engine.profile_id,
727
+ pid,
625
728
  pattern_type="correction",
626
729
  pattern_key=pattern_id,
627
730
  metadata={"correction": correction},
@@ -649,6 +752,34 @@ def register_core_tools(server, get_engine: Callable) -> None:
649
752
  from superlocalmemory.mcp.agent_context import get_current_agent_id
650
753
  agent_id = get_current_agent_id()
651
754
  try:
755
+ import asyncio
756
+ import urllib.parse
757
+
758
+ from superlocalmemory.cli.daemon import (
759
+ daemon_request,
760
+ is_daemon_running,
761
+ )
762
+
763
+ if await asyncio.to_thread(is_daemon_running):
764
+ path = "/api/memories/" + urllib.parse.quote(fact_id, safe="")
765
+ result = await asyncio.to_thread(
766
+ daemon_request, "DELETE", path,
767
+ )
768
+ if isinstance(result, dict) and result.get("success"):
769
+ _emit_event("memory.deleted", {
770
+ "fact_id": fact_id,
771
+ "agent_id": agent_id,
772
+ }, source_agent=agent_id)
773
+ return {
774
+ "success": True, "deleted": fact_id,
775
+ "agent_id": agent_id,
776
+ }
777
+ return {
778
+ "success": False,
779
+ "retryable": True,
780
+ "error": "resident daemon rejected the delete operation",
781
+ }
782
+
652
783
  from superlocalmemory.core.worker_pool import WorkerPool
653
784
  pool = WorkerPool.shared()
654
785
  result = pool._send({
@@ -691,6 +822,33 @@ def register_core_tools(server, get_engine: Callable) -> None:
691
822
  try:
692
823
  if not content or not content.strip():
693
824
  return {"success": False, "error": "content cannot be empty"}
825
+ import asyncio
826
+ import urllib.parse
827
+
828
+ from superlocalmemory.cli.daemon import (
829
+ daemon_request,
830
+ is_daemon_running,
831
+ )
832
+
833
+ if await asyncio.to_thread(is_daemon_running):
834
+ path = "/api/memories/" + urllib.parse.quote(fact_id, safe="")
835
+ result = await asyncio.to_thread(
836
+ daemon_request,
837
+ "PATCH",
838
+ path,
839
+ {"content": content.strip()},
840
+ )
841
+ if isinstance(result, dict) and result.get("success"):
842
+ return {
843
+ "success": True, "fact_id": fact_id,
844
+ "content": content.strip(),
845
+ }
846
+ return {
847
+ "success": False,
848
+ "retryable": True,
849
+ "error": "resident daemon rejected the update operation",
850
+ }
851
+
694
852
  from superlocalmemory.core.worker_pool import WorkerPool
695
853
  pool = WorkerPool.shared()
696
854
  result = pool._send({
@@ -32,7 +32,6 @@ if TYPE_CHECKING:
32
32
  logger = logging.getLogger(__name__)
33
33
 
34
34
  _VARIANCE_FLOOR: float = 1e-6
35
- _EMBED_DIM: int = 768
36
35
 
37
36
  # Stage-9 fix: cap the NUMBER of tenants held in memory. The per-tenant entry
38
37
  # caps (WP-A/B) bound depth, but _centroids/_counts grew once per distinct
@@ -59,11 +58,16 @@ class CentroidStore:
59
58
  Centroid update rule (Welford running mean — exact, O(1) per update):
60
59
  new_centroid = old_centroid * (n / (n+1)) + new_vec * (1 / (n+1))
61
60
  """
62
- def __init__(self, max_tenants: int = _MAX_TENANTS) -> None:
61
+ def __init__(
62
+ self,
63
+ max_tenants: int = _MAX_TENANTS,
64
+ embedding_dimension: int | None = None,
65
+ ) -> None:
63
66
  # OrderedDict for O(1) LRU eviction by tenant count.
64
67
  self._centroids: "OrderedDict[str, np.ndarray]" = OrderedDict() # tenant → vec
65
68
  self._counts: "OrderedDict[str, int]" = OrderedDict() # tenant → count
66
69
  self._max_tenants = max_tenants
70
+ self._embedding_dimension = embedding_dimension
67
71
  self._lock = threading.RLock()
68
72
 
69
73
  def _evict_tenants_if_needed(self) -> None:
@@ -89,7 +93,9 @@ class CentroidStore:
89
93
  for _entry_id, blob, _ctx_fp in rows:
90
94
  try:
91
95
  vec = np.frombuffer(blob, dtype=np.float32).copy()
92
- if vec.shape[0] == _EMBED_DIM:
96
+ if self._embedding_dimension is None:
97
+ self._embedding_dimension = int(vec.shape[0])
98
+ if vec.shape[0] == self._embedding_dimension:
93
99
  vectors.append(vec)
94
100
  except Exception:
95
101
  continue
@@ -116,7 +122,17 @@ class CentroidStore:
116
122
  """
117
123
  try:
118
124
  vec = new_vector.astype(np.float32)
125
+ if vec.ndim != 1 or vec.size == 0:
126
+ return
119
127
  with self._lock:
128
+ if self._embedding_dimension is None:
129
+ self._embedding_dimension = int(vec.shape[0])
130
+ if vec.shape[0] != self._embedding_dimension:
131
+ logger.warning(
132
+ "CentroidStore.update skipped dimension %d; expected %d",
133
+ vec.shape[0], self._embedding_dimension,
134
+ )
135
+ return
120
136
  if tenant_id not in self._centroids:
121
137
  self._centroids[tenant_id] = vec.copy()
122
138
  self._counts[tenant_id] = 1
@@ -156,6 +172,8 @@ class CentroidStore:
156
172
  if centroid is None or count < 5:
157
173
  return False
158
174
  q = query_vector.astype(np.float32)
175
+ if q.shape != centroid.shape:
176
+ return False
159
177
  sim = _cosine_similarity(q, centroid)
160
178
  threshold = 1.0 - distance_floor
161
179
  if sim < threshold:
@@ -96,6 +96,12 @@ class _LazySemanticEmbedder:
96
96
  self._service = EmbeddingService(SLMConfig.load().embedding)
97
97
  return self._service.embed(text)
98
98
 
99
+ @property
100
+ def dimension(self) -> int:
101
+ from superlocalmemory.core.config import SLMConfig
102
+
103
+ return int(SLMConfig.load().embedding.dimension)
104
+
99
105
  def close(self) -> None:
100
106
  service = self._service
101
107
  self._service = None
@@ -492,6 +498,7 @@ class CacheManager:
492
498
  db=self._db,
493
499
  config=optimize_config,
494
500
  embedder=desired_embedder,
501
+ embedding_dimension=getattr(desired_embedder, "dimension", None),
495
502
  ))
496
503
 
497
504
  # ---- core request path ----
@@ -43,6 +43,7 @@ if TYPE_CHECKING:
43
43
 
44
44
  logger = logging.getLogger(__name__)
45
45
 
46
+ # Backward-compatible exported default; runtime validation uses _embed_dim.
46
47
  _EMBED_DIM: int = 768
47
48
  _DEFAULT_MAX_TURNS: int = 6
48
49
  _DEFAULT_CONTEXT_WINDOW: int = 3
@@ -79,10 +80,13 @@ class VCacheSemantic(SemanticTier):
79
80
  config: "OptimizeConfig",
80
81
  *,
81
82
  embedder: Callable[[str], list[float] | np.ndarray | None] | None = None,
83
+ embedding_dimension: int | None = None,
82
84
  ) -> None:
83
85
  self._db = db
84
86
  self._config = config
85
87
  self._embedder = embedder
88
+ self._embed_dim = embedding_dimension
89
+ self._dimension_lock = threading.Lock()
86
90
  # TODO(v3.7): when entry_count > 10_000, promote to sqlite-vec. Config flag: semantic_use_vec.
87
91
 
88
92
  self._boundary_store = BoundaryStore(
@@ -94,7 +98,9 @@ class VCacheSemantic(SemanticTier):
94
98
  step=float(getattr(config, "semantic_boundary_step", 0.01)),
95
99
  epsilon=float(getattr(config, "semantic_error_target", _DEFAULT_ERROR_TARGET)),
96
100
  )
97
- self._centroid_store = CentroidStore()
101
+ self._centroid_store = CentroidStore(
102
+ embedding_dimension=embedding_dimension,
103
+ )
98
104
  self._context_key_builder = ContextKeyBuilder(
99
105
  window_turns=int(getattr(config, "semantic_context_window_turns", _DEFAULT_CONTEXT_WINDOW))
100
106
  )
@@ -145,10 +151,10 @@ class VCacheSemantic(SemanticTier):
145
151
  if embed is None:
146
152
  return None
147
153
  vec = np.asarray(embed, dtype=np.float32)
148
- if vec.shape[0] != _EMBED_DIM:
154
+ if not self._accept_dimension(vec):
149
155
  logger.debug(
150
- "VCacheSemantic.lookup: skip — embed dim=%d (expected %d)",
151
- vec.shape[0], _EMBED_DIM,
156
+ "VCacheSemantic.lookup: skip — embed dim=%s (expected %s)",
157
+ vec.shape, self._embed_dim,
152
158
  )
153
159
  return None
154
160
  return self._lookup_inner(req, tenant_id, vec)
@@ -267,6 +273,16 @@ class VCacheSemantic(SemanticTier):
267
273
  if callable(close):
268
274
  close()
269
275
 
276
+ def _accept_dimension(self, vector: np.ndarray) -> bool:
277
+ """Lock one configured/inferred vector width for this cache instance."""
278
+ if vector.ndim != 1 or vector.size == 0:
279
+ return False
280
+ with self._dimension_lock:
281
+ if self._embed_dim is None:
282
+ self._embed_dim = int(vector.shape[0])
283
+ self._centroid_store._embedding_dimension = self._embed_dim
284
+ return int(vector.shape[0]) == self._embed_dim
285
+
270
286
  # ------------------------------------------------------------------
271
287
  # Internal lookup
272
288
  # ------------------------------------------------------------------
@@ -387,7 +403,7 @@ class VCacheSemantic(SemanticTier):
387
403
  for entry_id, blob, ctx_fp in rows: # C-10: unpack persisted context_fp
388
404
  try:
389
405
  v = np.frombuffer(blob, dtype=np.float32).copy()
390
- if v.shape[0] == _EMBED_DIM:
406
+ if self._accept_dimension(v):
391
407
  entries.append((entry_id, ctx_fp, v))
392
408
  except Exception:
393
409
  continue
@@ -436,10 +452,11 @@ class VCacheSemantic(SemanticTier):
436
452
  ) -> None:
437
453
  """Persist vector + boundary record; update in-memory index + centroid."""
438
454
  vec = np.asarray(embed, dtype=np.float32)
439
- if vec.shape[0] != _EMBED_DIM:
455
+ if not self._accept_dimension(vec):
440
456
  logger.warning(
441
- "VCacheSemantic._set_inner: unexpected embedding dim %d (expected %d) "
442
- "for entry=%s — skipping", vec.shape[0], _EMBED_DIM, entry_id,
457
+ "VCacheSemantic._set_inner: unexpected embedding shape %s "
458
+ "(expected %s) for entry=%s — skipping",
459
+ vec.shape, self._embed_dim, entry_id,
443
460
  )
444
461
  return
445
462
 
@@ -450,8 +467,8 @@ class VCacheSemantic(SemanticTier):
450
467
  tenant_id=tenant_id,
451
468
  vector=vec_bytes,
452
469
  meta={
453
- "model": "nomic-ai/nomic-embed-text-v1.5",
454
- "dim": _EMBED_DIM,
470
+ "model": "configured-embedding-provider",
471
+ "dim": self._embed_dim,
455
472
  "context_fp": context_fp,
456
473
  },
457
474
  )