superlocalmemory 4.0.8 → 4.0.9

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 (120) hide show
  1. package/CHANGELOG.md +146 -1
  2. package/README.md +6 -6
  3. package/package.json +1 -1
  4. package/plugin/.claude-plugin/plugin.json +1 -1
  5. package/plugin/CLAUDE.md +3 -3
  6. package/plugin/agents/slm-governance-advisor.md +1 -1
  7. package/plugin/agents/slm-loop-runner.md +1 -1
  8. package/plugin/agents/slm-memory-advisor.md +1 -1
  9. package/plugin/agents/slm-optimize-advisor.md +1 -1
  10. package/plugin/requirements.txt +1 -1
  11. package/plugin/scripts/ensure-venv.sh +1 -1
  12. package/plugin/skills/slm-cache/SKILL.md +1 -1
  13. package/plugin/skills/slm-compress/SKILL.md +1 -1
  14. package/plugin/skills/slm-governance/SKILL.md +1 -1
  15. package/plugin/skills/slm-graph/SKILL.md +1 -1
  16. package/plugin/skills/slm-loop/SKILL.md +1 -1
  17. package/plugin/skills/slm-mesh/SKILL.md +1 -1
  18. package/plugin/skills/slm-profile/SKILL.md +1 -1
  19. package/plugin/skills/slm-recall/SKILL.md +1 -1
  20. package/plugin/skills/slm-remember/SKILL.md +1 -1
  21. package/plugin/skills/slm-scope/SKILL.md +1 -1
  22. package/plugin/skills/slm-session/SKILL.md +1 -1
  23. package/plugin/skills/slm-status/SKILL.md +3 -3
  24. package/plugin-src/rules/AGENTS.md +1 -1
  25. package/plugin-src/skills/slm-status/SKILL.md +2 -2
  26. package/pyproject.toml +1 -1
  27. package/scripts/postinstall.js +4 -0
  28. package/src/superlocalmemory/__init__.py +1 -1
  29. package/src/superlocalmemory/cli/_lazy_init.py +1 -1
  30. package/src/superlocalmemory/cli/commands.py +119 -9
  31. package/src/superlocalmemory/cli/db_migrate.py +0 -2
  32. package/src/superlocalmemory/cli/gdpr_io.py +1 -1
  33. package/src/superlocalmemory/cli/main.py +5 -5
  34. package/src/superlocalmemory/cli/service_installer.py +2 -1
  35. package/src/superlocalmemory/cli/setup_wizard.py +1 -1
  36. package/src/superlocalmemory/core/config.py +41 -7
  37. package/src/superlocalmemory/core/context_cache.py +0 -2
  38. package/src/superlocalmemory/core/engine.py +371 -63
  39. package/src/superlocalmemory/core/evidence_bundle.py +3 -1
  40. package/src/superlocalmemory/core/install_detector.py +131 -0
  41. package/src/superlocalmemory/core/progressive_abstraction.py +1 -1
  42. package/src/superlocalmemory/core/security_primitives.py +3 -6
  43. package/src/superlocalmemory/core/store_pipeline.py +94 -26
  44. package/src/superlocalmemory/core/topic_signature.py +0 -2
  45. package/src/superlocalmemory/core/transactions/concrete_owners.py +15 -8
  46. package/src/superlocalmemory/dynamics/eap_scheduler.py +17 -6
  47. package/src/superlocalmemory/encoding/graph_builder.py +2 -2
  48. package/src/superlocalmemory/encoding/scene_builder.py +8 -2
  49. package/src/superlocalmemory/hooks/adapter_base.py +0 -2
  50. package/src/superlocalmemory/hooks/context_payload.py +0 -2
  51. package/src/superlocalmemory/hooks/portable_kit.py +8 -8
  52. package/src/superlocalmemory/hooks/post_tool_async_hook.py +0 -2
  53. package/src/superlocalmemory/hooks/prewarm_auth.py +0 -2
  54. package/src/superlocalmemory/hooks/user_prompt_hook.py +0 -2
  55. package/src/superlocalmemory/infra/backup.py +44 -8
  56. package/src/superlocalmemory/integrations/bounded_loops_mcp.py +24 -7
  57. package/src/superlocalmemory/learning/arm_catalog.py +0 -2
  58. package/src/superlocalmemory/learning/bandit.py +0 -2
  59. package/src/superlocalmemory/learning/bandit_cache.py +0 -2
  60. package/src/superlocalmemory/learning/dedup_hnsw.py +11 -11
  61. package/src/superlocalmemory/learning/ensemble.py +0 -2
  62. package/src/superlocalmemory/learning/labeler.py +0 -2
  63. package/src/superlocalmemory/learning/legacy_migration.py +0 -2
  64. package/src/superlocalmemory/learning/model_cache.py +0 -2
  65. package/src/superlocalmemory/learning/ranker.py +0 -2
  66. package/src/superlocalmemory/learning/reward_archive.py +6 -1
  67. package/src/superlocalmemory/learning/reward_proxy.py +0 -2
  68. package/src/superlocalmemory/learning/signal_worker.py +0 -2
  69. package/src/superlocalmemory/math/fisher.py +1 -1
  70. package/src/superlocalmemory/math/hopfield.py +4 -1
  71. package/src/superlocalmemory/math/langevin.py +1 -1
  72. package/src/superlocalmemory/math/sheaf.py +7 -3
  73. package/src/superlocalmemory/mcp/cli_fallback.py +1 -1
  74. package/src/superlocalmemory/mcp/profiles.py +1 -1
  75. package/src/superlocalmemory/mcp/server.py +1 -1
  76. package/src/superlocalmemory/mcp/tools_active.py +56 -0
  77. package/src/superlocalmemory/mcp/tools_core.py +1 -1
  78. package/src/superlocalmemory/optimize/cache/manager.py +2 -2
  79. package/src/superlocalmemory/optimize/compress/ccr.py +1 -1
  80. package/src/superlocalmemory/optimize/compress/router.py +1 -1
  81. package/src/superlocalmemory/optimize/proxy/_helpers.py +2 -2
  82. package/src/superlocalmemory/optimize/proxy/server.py +1 -1
  83. package/src/superlocalmemory/optimize/proxy/vertex_surface.py +2 -2
  84. package/src/superlocalmemory/optimize/storage/db.py +2 -2
  85. package/src/superlocalmemory/retrieval/agentic.py +1 -1
  86. package/src/superlocalmemory/retrieval/ann_index.py +9 -2
  87. package/src/superlocalmemory/retrieval/bm25_channel.py +2 -2
  88. package/src/superlocalmemory/retrieval/bridge_discovery.py +2 -2
  89. package/src/superlocalmemory/retrieval/engine.py +272 -43
  90. package/src/superlocalmemory/retrieval/entity_channel.py +1 -1
  91. package/src/superlocalmemory/retrieval/hopfield_channel.py +8 -2
  92. package/src/superlocalmemory/retrieval/profile_channel.py +1 -1
  93. package/src/superlocalmemory/retrieval/quantization_aware_search.py +1 -1
  94. package/src/superlocalmemory/retrieval/remote_reranker.py +2 -2
  95. package/src/superlocalmemory/retrieval/reranker.py +3 -3
  96. package/src/superlocalmemory/retrieval/semantic_channel.py +3 -3
  97. package/src/superlocalmemory/retrieval/spreading_activation.py +8 -8
  98. package/src/superlocalmemory/retrieval/strategy.py +94 -0
  99. package/src/superlocalmemory/retrieval/temporal_channel.py +167 -10
  100. package/src/superlocalmemory/retrieval/temporal_validity_filter.py +1 -1
  101. package/src/superlocalmemory/retrieval/vector_store.py +88 -10
  102. package/src/superlocalmemory/server/recall_serializer.py +10 -0
  103. package/src/superlocalmemory/server/routes/brain.py +2 -2
  104. package/src/superlocalmemory/server/routes/prewarm.py +4 -4
  105. package/src/superlocalmemory/server/unified_daemon.py +459 -7
  106. package/src/superlocalmemory/storage/_schema_version.py +46 -3
  107. package/src/superlocalmemory/storage/backup.py +531 -0
  108. package/src/superlocalmemory/storage/database.py +11 -4
  109. package/src/superlocalmemory/storage/embedding_codec.py +129 -0
  110. package/src/superlocalmemory/storage/embedding_migrator.py +5 -3
  111. package/src/superlocalmemory/storage/migration_runner.py +142 -2
  112. package/src/superlocalmemory/storage/migrations/__init__.py +1 -1
  113. package/src/superlocalmemory/storage/migrations.py +15 -1
  114. package/src/superlocalmemory/storage/models.py +7 -0
  115. package/src/superlocalmemory/storage/quantized_store.py +4 -2
  116. package/src/superlocalmemory/summaries/session_summary.py +1 -1
  117. package/src/superlocalmemory/ui/css/legacy-dashboard.css +1 -1
  118. package/src/superlocalmemory/ui/css/neural-glass.css +1 -1
  119. package/src/superlocalmemory/ui/js/core.js +1 -1
  120. package/src/superlocalmemory/ui/js/od-brain.js +1 -1
@@ -4,8 +4,6 @@
4
4
 
5
5
  """Active-model cache + integrity verification.
6
6
 
7
- LLD reference: ``.backup/active-brain/lld/LLD-02-signal-pipeline-and-lightgbm.md``
8
- Section 4.4 — every model load goes through here.
9
7
 
10
8
  Hard rules enforced:
11
9
  M1 — ``pickle.loads`` is FORBIDDEN on ``state_bytes``.
@@ -4,8 +4,6 @@
4
4
 
5
5
  """3-phase adaptive ranker — from heuristic to ML.
6
6
 
7
- LLD reference: ``.backup/active-brain/lld/LLD-02-signal-pipeline-and-lightgbm.md``
8
- Sections 4.4 + 4.5.
9
7
 
10
8
  Phase 1: cross-encoder score only (cold start)
11
9
  Phase 2: heuristic boosts (some data)
@@ -34,6 +34,7 @@ from pathlib import Path
34
34
  from superlocalmemory.learning.fact_outcome_joins import (
35
35
  has_recent_positive_reward,
36
36
  )
37
+ from superlocalmemory.storage.embedding_codec import decode_embedding
37
38
  from superlocalmemory.storage.write_lock import get_write_lock
38
39
 
39
40
  logger = logging.getLogger(__name__)
@@ -113,13 +114,17 @@ def run_reward_gated_archive(
113
114
  window_days=REWARD_WINDOW_DAYS,
114
115
  ):
115
116
  continue
117
+ # Decode the embedding to list[float] so json.dumps(payload)
118
+ # succeeds regardless of whether the row is TEXT or BLOB.
119
+ # ValueError (corrupt buffer) propagates — a silent None here
120
+ # would archive a fact with a missing embedding, which loses data.
116
121
  to_archive.append({
117
122
  "fid": fid,
118
123
  "content": row["content"],
119
124
  "canonical_entities_json": row["canonical_entities_json"],
120
125
  "importance": row["importance"],
121
126
  "confidence": row["confidence"],
122
- "embedding": row["embedding"],
127
+ "embedding": decode_embedding(row["embedding"], fact_id=fid),
123
128
  "created_at": row["created_at"],
124
129
  })
125
130
  finally:
@@ -4,8 +4,6 @@
4
4
 
5
5
  """Proxy settlement for bandit plays (v3.4.22 only).
6
6
 
7
- LLD reference: ``.backup/active-brain/lld/LLD-03-contextual-bandit-and-ensemble.md``
8
- Section 3.5 and 5.6.
9
7
 
10
8
  Replaced in v3.4.22 by ``reward_from_outcomes.py`` — DO NOT extend this
11
9
  module beyond the proxy window contract.
@@ -4,8 +4,6 @@
4
4
 
5
5
  """Background signal drain worker.
6
6
 
7
- LLD reference: ``.backup/active-brain/lld/LLD-02-signal-pipeline-and-lightgbm.md``
8
- Section 4.2 — moves signal writes off the recall hot path.
9
7
 
10
8
  Contract (hard rules, enforced by tests):
11
9
  SW1 — Hot path never waits for disk.
@@ -19,7 +19,7 @@ Geodesic distance (Atkinson & Mitchell 1981, Pinele et al. 2020):
19
19
  Diagonal multivariate (product-manifold decomposition):
20
20
  d_FR(p, q) = sqrt( sum_i d_i^2 )
21
21
 
22
- Bayesian variance update (NEW in Innovation Wave 4):
22
+ Bayesian variance update (added in v3.5):
23
23
 
24
24
  V1 bug: query always received UNIFORM variance, so Fisher degenerated
25
25
  to a monotonic transform of cosine. FIX: every fact maintains its own
@@ -56,7 +56,10 @@ class HopfieldConfig:
56
56
  max_iterations: int = 1
57
57
  convergence_epsilon: float = 1e-6
58
58
  prefilter_threshold: int = 10_000
59
- prefilter_candidates: int = 1000
59
+ # MUST match HopfieldConfig.prefilter_candidates in core/config.py, where the
60
+ # reasoning for the value is written. This stage decides final membership, so
61
+ # this number is a hard limit on which memories can be returned at all.
62
+ prefilter_candidates: int = 500
60
63
  skip_threshold: int = 100_000
61
64
  cache_ttl_seconds: float = 60.0
62
65
 
@@ -26,7 +26,7 @@ Potential function:
26
26
  - Old / unused: gamma term raises potential -> drift to boundary
27
27
  - Important memories: delta term lowers potential -> retain near origin
28
28
 
29
- V1 bugs fixed in Innovation Wave 4:
29
+ V1 bugs fixed in v3.5:
30
30
  1. Positions were computed per-recall then DISCARDED. Now ``step()``
31
31
  and ``batch_step()`` return new positions for the caller to persist.
32
32
  2. Weight range was [0.7, 1.0] --- too narrow to change rankings.
@@ -24,6 +24,8 @@ from typing import TYPE_CHECKING
24
24
 
25
25
  import numpy as np
26
26
 
27
+ from superlocalmemory.storage.embedding_codec import decode_embedding
28
+
27
29
  if TYPE_CHECKING:
28
30
  from superlocalmemory.storage.database import DatabaseManager
29
31
  from superlocalmemory.storage.models import AtomicFact
@@ -251,7 +253,9 @@ class SheafConsistencyChecker:
251
253
  if raw is None or raw == "":
252
254
  return None
253
255
  try:
254
- data = json.loads(raw) if isinstance(raw, str) else raw
255
- return np.asarray(data, dtype=np.float64)
256
- except (json.JSONDecodeError, TypeError, ValueError):
256
+ data = decode_embedding(raw, fact_id=str(fact_id))
257
+ except ValueError:
258
+ return None
259
+ if data is None:
257
260
  return None
261
+ return np.asarray(data, dtype=np.float64)
@@ -1,6 +1,6 @@
1
1
  # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
2
2
  # Licensed under AGPL-3.0-or-later - see LICENSE file
3
- # WP-09: MCP→CLI fallback adapter (ships INERTDQ-2=B)
3
+ # MCP→CLI fallback adapter (ships inertdisabled by default, no active use yet)
4
4
 
5
5
  """MCP→CLI fallback adapter for SuperLocalMemory.
6
6
 
@@ -14,7 +14,7 @@ Do NOT import FastMCP, MemoryEngine, or any heavy dependency here.
14
14
  from __future__ import annotations
15
15
 
16
16
  # ---------------------------------------------------------------------------
17
- # v3.6.14 WP-01: Named profile definitions
17
+ # Named profile definitions (introduced in v3.6.14)
18
18
  # ---------------------------------------------------------------------------
19
19
 
20
20
  _PROFILE_CORE: frozenset[str] = frozenset({ # 17
@@ -159,7 +159,7 @@ _all_tools = _os_reg.environ.get("SLM_MCP_ALL_TOOLS") == "1"
159
159
  _user_allowlist_str = _os_reg.environ.get("SLM_MCP_TOOLS", "").strip()
160
160
 
161
161
  # ---------------------------------------------------------------------------
162
- # v3.6.14 WP-01: Named profile definitions
162
+ # Named profile definitions (introduced in v3.6.14)
163
163
  # Extracted to mcp/profiles.py (v3.8.0) — pure data, no side effects.
164
164
  # All names re-exported here for backward compatibility with existing tests
165
165
  # and any code that imports them from this module.
@@ -255,6 +255,56 @@ def _canonical_feedback_count(profile_id: str) -> int | None:
255
255
  return None
256
256
 
257
257
 
258
+ # How far ahead a session looks for scheduled facts, and how many it shows.
259
+ # A session preamble is not a calendar: a long horizon or a large cap turns a
260
+ # useful heads-up into a wall of text nobody reads.
261
+ _SCHEDULED_HORIZON_DAYS = 14
262
+ _SCHEDULED_LIMIT = 5
263
+
264
+
265
+ def _upcoming_scheduled_facts(engine, now: datetime.datetime) -> list[dict]:
266
+ """Facts scheduled from today through the horizon, soonest first.
267
+
268
+ Bounded and index-backed, because this runs on every session start. Returns
269
+ an empty list on any failure: a session must still open when this query
270
+ cannot answer, so the caller omits the surface entirely rather than showing
271
+ an empty section that reads like a defect.
272
+
273
+ Selects on the stored type of the fact, which is a classification recorded at
274
+ write time. It is unrelated to the similarly named retrieval channel despite
275
+ sharing the word.
276
+ """
277
+ try:
278
+ db = getattr(engine, "_db", None) or getattr(engine, "db", None)
279
+ if db is None or not hasattr(db, "execute"):
280
+ return []
281
+ # Both 'YYYY-MM-DD' and full timestamps compare correctly as text,
282
+ # because ISO-8601 orders lexicographically. The upper bound is
283
+ # exclusive, so the horizon day itself is included.
284
+ start = now.date().isoformat()
285
+ end = (now + datetime.timedelta(days=_SCHEDULED_HORIZON_DAYS + 1)).date().isoformat()
286
+ rows = db.execute(
287
+ "SELECT fact_id, content, referenced_date"
288
+ " FROM atomic_facts"
289
+ " WHERE profile_id = ?"
290
+ " AND fact_type = 'temporal'"
291
+ " AND referenced_date IS NOT NULL"
292
+ " AND referenced_date >= ?"
293
+ " AND referenced_date < ?"
294
+ " ORDER BY referenced_date ASC"
295
+ f" LIMIT {_SCHEDULED_LIMIT}",
296
+ (engine.profile_id, start, end),
297
+ )
298
+ return [
299
+ {"fact_id": r["fact_id"], "content": r["content"],
300
+ "scheduled_at": r["referenced_date"]}
301
+ for r in rows
302
+ ]
303
+ except Exception as exc:
304
+ logger.warning("scheduled-fact surface failed: %s", exc)
305
+ return []
306
+
307
+
258
308
  def register_active_tools(server, get_engine: Callable) -> None:
259
309
  """Register 3 active memory tools on *server*."""
260
310
 
@@ -519,6 +569,8 @@ def register_active_tools(server, get_engine: Callable) -> None:
519
569
  f"-{uuid.uuid4().hex[:8]}"
520
570
  )
521
571
 
572
+ _upcoming_events = _upcoming_scheduled_facts(engine, _now)
573
+
522
574
  return {
523
575
  "success": True,
524
576
  "session_id": session_id,
@@ -553,6 +605,10 @@ def register_active_tools(server, get_engine: Callable) -> None:
553
605
  else "trained"
554
606
  ),
555
607
  },
608
+ # Scheduled-event surface: present only when facts exist in the
609
+ # 14-day window. Absent means the window is empty, not an error.
610
+ **( {"upcoming_events": _upcoming_events}
611
+ if _upcoming_events else {} ),
556
612
  }
557
613
  except Exception as exc:
558
614
  logger.exception("session_init failed")
@@ -560,7 +560,7 @@ def register_core_tools(server, get_engine: Callable) -> None:
560
560
  if db_path.exists():
561
561
  db_size_mb = round(os.path.getsize(db_path) / (1024 * 1024), 2)
562
562
 
563
- # WP-02 D8: additive canonical key set — provider/base_dir/db_path added.
563
+ # additive canonical key set — provider/base_dir/db_path added.
564
564
  # All pre-existing keys are preserved (zero removals).
565
565
  cfg = engine._config
566
566
  return {
@@ -223,7 +223,7 @@ class CacheManager:
223
223
  tenant_id = _hashlib.sha256(tenant_id.encode()).hexdigest()
224
224
 
225
225
  if isinstance(req, ProxyRequest) and req.provider == "vertex":
226
- # CRIT-2 (WP-11, LOCKED): Vertex bodies have NO model/messages/system.
226
+ # LOCKED: Vertex bodies have NO model/messages/system.
227
227
  # Model is in the PATH; prompts are under 'contents'; system under
228
228
  # 'systemInstruction'. Without this branch ALL Vertex requests hash to
229
229
  # ONE key → first response poisons every subsequent prompt.
@@ -701,7 +701,7 @@ def json_dumps_bytes(d: dict) -> bytes:
701
701
 
702
702
 
703
703
  # ---------------------------------------------------------------------------
704
- # Vertex helpers (WP-11 / CRIT-2)
704
+ # Vertex helpers
705
705
  # ---------------------------------------------------------------------------
706
706
 
707
707
  def _vertex_project_location_from_path(path: str) -> tuple[str, str]:
@@ -99,7 +99,7 @@ class CCRStore:
99
99
  def delete(self, ccr_id: str, *, tenant_id: str = "default") -> None:
100
100
  """Delete a CCR row by ccr_id scoped to tenant. Idempotent — never raises.
101
101
 
102
- WP-10 D6: defensive infra. Deleting a non-existent ccr_id is a no-op.
102
+ Defensive delete idempotent: deleting a non-existent ccr_id is a no-op.
103
103
  H-02: tenant_id guard prevents cross-tenant deletion.
104
104
  """
105
105
  try:
@@ -411,7 +411,7 @@ class CompressRouter:
411
411
  logger.debug("CCR update_compressed failed (non-fatal): %s", exc)
412
412
 
413
413
  def _ccr_delete(self, ccr_id: str) -> None:
414
- """WP-10 D6: Defensive delete — idempotent, never raises.
414
+ """Defensive delete — idempotent, never raises.
415
415
 
416
416
  Used to clean up a CCR row if post-store processing fails. In the
417
417
  store-after-success D6 path this should never be needed (no orphans
@@ -123,10 +123,10 @@ _OPENAI_FORWARD_HEADERS = frozenset([
123
123
  _GEMINI_NATIVE_FORWARD_HEADERS = frozenset([
124
124
  "x-goog-api-key",
125
125
  "content-type",
126
- "authorization", # WP-11a: Antigravity ADC/OAuth bearer was dropped; add it back
126
+ "authorization", # Antigravity ADC/OAuth bearer was previously dropped; explicitly add it back
127
127
  ])
128
128
 
129
- # WP-11: Vertex AI forward headers — Authorization passed untouched (AC-2).
129
+ # Vertex AI forward headers — Authorization is passed through untouched.
130
130
  # x-goog-user-project required for quota attribution on Vertex calls.
131
131
  _VERTEX_FORWARD_HEADERS = frozenset([
132
132
  "authorization",
@@ -150,7 +150,7 @@ def build_proxy_router(proxy: ProxyApp) -> APIRouter:
150
150
  async def gemini_openai_models_route(request: Request) -> Response:
151
151
  return await handle_gemini_openai_compat(proxy, request)
152
152
 
153
- # WP-11: Vertex AI passthrough — must be registered AFTER exact /v1/* routes
153
+ # Vertex AI passthrough — must be registered AFTER exact /v1/* routes
154
154
  # to avoid shadowing /v1/messages, /v1/chat/completions, /v1/embeddings, etc.
155
155
  # FastAPI resolves routes in registration order; the exact routes above are
156
156
  # declared before this catch-path, so there is no shadowing.
@@ -1,4 +1,4 @@
1
- """vertex_surface.py — Vertex AI passthrough proxy surface (WP-11).
1
+ """vertex_surface.py — Vertex AI passthrough proxy surface.
2
2
 
3
3
  Transparent passthrough: forward Authorization bearer untouched (AC-2),
4
4
  cache by body content never by token (SEC), no SSE — always single JSON.
@@ -14,7 +14,7 @@ Key design decisions (per LLD §5 STAGE-5 RESOLUTIONS):
14
14
  SECURITY (AC-3): bearer token structurally excluded from cache key, value,
15
15
  logs, and stored ProxyRequest.headers (redacted via _redact_headers).
16
16
 
17
- WP-11a (gemini-native fix) lives in _helpers.py:_GEMINI_NATIVE_FORWARD_HEADERS.
17
+ The gemini-native Authorization-header fix lives in _helpers.py:_GEMINI_NATIVE_FORWARD_HEADERS.
18
18
  """
19
19
 
20
20
  from __future__ import annotations
@@ -959,7 +959,7 @@ class CacheDB:
959
959
  def ccr_delete(self, ccr_id: str, *, tenant_id: str = "default") -> None:
960
960
  """Delete a CCR row by ccr_id scoped to tenant. Idempotent — warns on sqlite error, never raises.
961
961
 
962
- WP-10 D6: defensive infra + sweep parity. Deleting a non-existent row is a no-op.
962
+ Defensive delete idempotent: deleting a non-existent row is a no-op.
963
963
  H-02: tenant_id guard prevents a tenant from deleting another tenant's CCR.
964
964
  """
965
965
  try:
@@ -973,7 +973,7 @@ class CacheDB:
973
973
  def ccr_count(self) -> int:
974
974
  """Return UNFILTERED count of rows in llmcache_ccr_originals.
975
975
 
976
- WP-10 CRIT-2: Do NOT reuse TTL-filtered count at :646. A fresh no-expiry row
976
+ Do NOT reuse TTL-filtered count from the query above. A fresh no-expiry row
977
977
  has ttl_expires=None, so the TTL filter returns 0 and D6 orphan tests would
978
978
  falsely pass. This unfiltered count is test infrastructure only.
979
979
  """
@@ -163,7 +163,7 @@ class AgenticRetriever:
163
163
  RetrievalRound(2, rq, len(rn), _avg(rn), True),
164
164
  )
165
165
 
166
- merged = sorted(pool.values(), key=lambda x: x[1], reverse=True)
166
+ merged = sorted(pool.values(), key=lambda x: (-x[1], x[0].fact_id))
167
167
  return [f for f, _ in merged[:top_k]]
168
168
 
169
169
  # -- Sufficiency check ---------------------------------------------------
@@ -178,8 +178,15 @@ class ANNIndex:
178
178
  if k <= 0:
179
179
  return []
180
180
 
181
- top_indices = np.argpartition(scores, -k)[-k:]
182
- top_indices = top_indices[np.argsort(scores[top_indices])[::-1]]
181
+ # Full sort with a secondary key on fact_id so that equal cosine
182
+ # scores are broken deterministically. argpartition cannot
183
+ # guarantee which facts reach the top-k when scores tie at the
184
+ # boundary — the selection follows array layout (insertion order)
185
+ # and changes between restarts. lexsort picks the same k facts
186
+ # in the same order regardless of how the index was loaded.
187
+ id_arr = np.array(self._ids, dtype=object)
188
+ order = np.lexsort((id_arr, -scores)) # primary: -score, secondary: fact_id
189
+ top_indices = order[:k]
183
190
 
184
191
  return [
185
192
  (self._ids[i], float(scores[i]))
@@ -267,7 +267,7 @@ class BM25Channel:
267
267
  except Exception as exc: # pragma: no cover — legacy/missing expansion FTS
268
268
  logger.debug("Expansion FTS search skipped: %s", exc)
269
269
 
270
- out.sort(key=lambda x: x[1], reverse=True)
270
+ out.sort(key=lambda x: (-x[1], x[0]))
271
271
  return out[:top_k]
272
272
 
273
273
  def search(
@@ -341,7 +341,7 @@ class BM25Channel:
341
341
  bonus *= 1.5 # 50% boost for exact phrase match
342
342
  scored.append((self._fact_ids[i], bonus))
343
343
 
344
- scored.sort(key=lambda x: x[1], reverse=True)
344
+ scored.sort(key=lambda x: (-x[1], x[0]))
345
345
  return scored[:top_k]
346
346
 
347
347
  def update_fact(self, fact_id: str, new_content: str, profile_id: str) -> None:
@@ -160,7 +160,7 @@ class BridgeDiscovery:
160
160
  if len(bridges) >= max_bridges:
161
161
  break
162
162
 
163
- bridges.sort(key=lambda x: x[1], reverse=True)
163
+ bridges.sort(key=lambda x: (-x[1], x[0]))
164
164
  return filter_authorized_results(
165
165
  self._db,
166
166
  bridges,
@@ -243,7 +243,7 @@ class BridgeDiscovery:
243
243
  for fid, score in activations.items()
244
244
  if fid not in set(seed_ids) and score > 0.01
245
245
  ]
246
- results.sort(key=lambda x: x[1], reverse=True)
246
+ results.sort(key=lambda x: (-x[1], x[0]))
247
247
  return filter_authorized_results(
248
248
  self._db,
249
249
  results,