superlocalmemory 4.0.8 → 4.0.10

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 (147) hide show
  1. package/CHANGELOG.md +221 -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 +164 -11
  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 +7 -7
  34. package/src/superlocalmemory/cli/service_installer.py +2 -1
  35. package/src/superlocalmemory/cli/setup_wizard.py +1 -1
  36. package/src/superlocalmemory/code_graph/bridge/maintenance.py +8 -0
  37. package/src/superlocalmemory/core/config.py +41 -7
  38. package/src/superlocalmemory/core/context_cache.py +0 -2
  39. package/src/superlocalmemory/core/engine.py +371 -63
  40. package/src/superlocalmemory/core/evidence_bundle.py +3 -1
  41. package/src/superlocalmemory/core/fact_consolidator.py +316 -125
  42. package/src/superlocalmemory/core/install_detector.py +131 -0
  43. package/src/superlocalmemory/core/maintenance.py +44 -6
  44. package/src/superlocalmemory/core/memory_health.py +266 -0
  45. package/src/superlocalmemory/core/operation_policy_registry.py +1 -1
  46. package/src/superlocalmemory/core/operation_request.py +1 -1
  47. package/src/superlocalmemory/core/ops_remediation.py +2 -2
  48. package/src/superlocalmemory/core/progressive_abstraction.py +1 -1
  49. package/src/superlocalmemory/core/security_primitives.py +3 -6
  50. package/src/superlocalmemory/core/store_pipeline.py +172 -29
  51. package/src/superlocalmemory/core/topic_signature.py +0 -2
  52. package/src/superlocalmemory/core/transactions/concrete_owners.py +15 -8
  53. package/src/superlocalmemory/dynamics/eap_scheduler.py +17 -6
  54. package/src/superlocalmemory/encoding/cognitive_consolidator.py +15 -1
  55. package/src/superlocalmemory/encoding/graph_builder.py +2 -2
  56. package/src/superlocalmemory/encoding/scene_builder.py +8 -2
  57. package/src/superlocalmemory/hooks/adapter_base.py +0 -2
  58. package/src/superlocalmemory/hooks/context_payload.py +0 -2
  59. package/src/superlocalmemory/hooks/portable_kit.py +8 -8
  60. package/src/superlocalmemory/hooks/post_tool_async_hook.py +0 -2
  61. package/src/superlocalmemory/hooks/prewarm_auth.py +0 -2
  62. package/src/superlocalmemory/hooks/user_prompt_hook.py +0 -2
  63. package/src/superlocalmemory/infra/backup.py +44 -8
  64. package/src/superlocalmemory/integrations/bounded_loops_mcp.py +24 -7
  65. package/src/superlocalmemory/learning/arm_catalog.py +0 -2
  66. package/src/superlocalmemory/learning/bandit.py +0 -2
  67. package/src/superlocalmemory/learning/bandit_cache.py +0 -2
  68. package/src/superlocalmemory/learning/dedup_hnsw.py +11 -11
  69. package/src/superlocalmemory/learning/ensemble.py +0 -2
  70. package/src/superlocalmemory/learning/labeler.py +0 -2
  71. package/src/superlocalmemory/learning/legacy_migration.py +0 -2
  72. package/src/superlocalmemory/learning/model_cache.py +0 -2
  73. package/src/superlocalmemory/learning/ranker.py +0 -2
  74. package/src/superlocalmemory/learning/reward_archive.py +6 -1
  75. package/src/superlocalmemory/learning/reward_proxy.py +0 -2
  76. package/src/superlocalmemory/learning/signal_worker.py +0 -2
  77. package/src/superlocalmemory/math/fisher.py +1 -1
  78. package/src/superlocalmemory/math/hopfield.py +4 -1
  79. package/src/superlocalmemory/math/langevin.py +1 -1
  80. package/src/superlocalmemory/math/sheaf.py +7 -3
  81. package/src/superlocalmemory/mcp/cli_fallback.py +1 -1
  82. package/src/superlocalmemory/mcp/profiles.py +1 -1
  83. package/src/superlocalmemory/mcp/server.py +2 -2
  84. package/src/superlocalmemory/mcp/session_binding.py +92 -0
  85. package/src/superlocalmemory/mcp/tools_active.py +56 -0
  86. package/src/superlocalmemory/mcp/tools_core.py +41 -40
  87. package/src/superlocalmemory/mcp/tools_ops.py +2 -2
  88. package/src/superlocalmemory/optimize/cache/manager.py +2 -2
  89. package/src/superlocalmemory/optimize/compress/ccr.py +1 -1
  90. package/src/superlocalmemory/optimize/compress/router.py +1 -1
  91. package/src/superlocalmemory/optimize/proxy/_helpers.py +2 -2
  92. package/src/superlocalmemory/optimize/proxy/server.py +1 -1
  93. package/src/superlocalmemory/optimize/proxy/vertex_surface.py +2 -2
  94. package/src/superlocalmemory/optimize/storage/db.py +2 -2
  95. package/src/superlocalmemory/retrieval/agentic.py +1 -1
  96. package/src/superlocalmemory/retrieval/ann_index.py +9 -2
  97. package/src/superlocalmemory/retrieval/bm25_channel.py +6 -10
  98. package/src/superlocalmemory/retrieval/bridge_discovery.py +2 -2
  99. package/src/superlocalmemory/retrieval/engine.py +272 -43
  100. package/src/superlocalmemory/retrieval/entity_channel.py +8 -2
  101. package/src/superlocalmemory/retrieval/hopfield_channel.py +8 -2
  102. package/src/superlocalmemory/retrieval/profile_channel.py +1 -1
  103. package/src/superlocalmemory/retrieval/quantization_aware_search.py +1 -1
  104. package/src/superlocalmemory/retrieval/remote_reranker.py +2 -2
  105. package/src/superlocalmemory/retrieval/reranker.py +3 -3
  106. package/src/superlocalmemory/retrieval/scope_policy.py +22 -1
  107. package/src/superlocalmemory/retrieval/semantic_channel.py +3 -3
  108. package/src/superlocalmemory/retrieval/spreading_activation.py +8 -8
  109. package/src/superlocalmemory/retrieval/strategy.py +94 -0
  110. package/src/superlocalmemory/retrieval/temporal_channel.py +179 -10
  111. package/src/superlocalmemory/retrieval/temporal_validity_filter.py +1 -1
  112. package/src/superlocalmemory/retrieval/vector_store.py +151 -10
  113. package/src/superlocalmemory/server/api.py +6 -1
  114. package/src/superlocalmemory/server/asset_versions.py +171 -0
  115. package/src/superlocalmemory/server/recall_serializer.py +10 -0
  116. package/src/superlocalmemory/server/routes/abstraction.py +201 -0
  117. package/src/superlocalmemory/server/routes/brain.py +2 -2
  118. package/src/superlocalmemory/server/routes/data_io.py +29 -1
  119. package/src/superlocalmemory/server/routes/entity.py +13 -1
  120. package/src/superlocalmemory/server/routes/mesh.py +1 -1
  121. package/src/superlocalmemory/server/routes/prewarm.py +4 -4
  122. package/src/superlocalmemory/server/routes/v3_api.py +2 -2
  123. package/src/superlocalmemory/server/ui.py +8 -1
  124. package/src/superlocalmemory/server/unified_daemon.py +570 -16
  125. package/src/superlocalmemory/storage/_migration_internals.py +4 -0
  126. package/src/superlocalmemory/storage/_schema_version.py +46 -3
  127. package/src/superlocalmemory/storage/backup.py +531 -0
  128. package/src/superlocalmemory/storage/database.py +139 -34
  129. package/src/superlocalmemory/storage/embedding_codec.py +129 -0
  130. package/src/superlocalmemory/storage/embedding_migrator.py +5 -3
  131. package/src/superlocalmemory/storage/migration_runner.py +153 -2
  132. package/src/superlocalmemory/storage/migrations/M043_quarantine_display_summaries.py +488 -0
  133. package/src/superlocalmemory/storage/migrations/__init__.py +1 -1
  134. package/src/superlocalmemory/storage/migrations.py +15 -1
  135. package/src/superlocalmemory/storage/models.py +7 -0
  136. package/src/superlocalmemory/storage/quantized_store.py +4 -2
  137. package/src/superlocalmemory/storage/schema.py +98 -0
  138. package/src/superlocalmemory/summaries/base.py +1 -1
  139. package/src/superlocalmemory/summaries/non_answer.py +223 -0
  140. package/src/superlocalmemory/summaries/session_summary.py +1 -1
  141. package/src/superlocalmemory/ui/css/legacy-dashboard.css +1 -1
  142. package/src/superlocalmemory/ui/css/neural-glass.css +1 -1
  143. package/src/superlocalmemory/ui/index.html +1 -1
  144. package/src/superlocalmemory/ui/js/core.js +1 -1
  145. package/src/superlocalmemory/ui/js/od-brain.js +1 -1
  146. package/src/superlocalmemory/ui/js/od-memories.js +190 -1
  147. package/src/superlocalmemory/ui/js/od-ops-health.js +1 -1
@@ -96,6 +96,29 @@ async def observe_terminal_runs(
96
96
  return observed
97
97
 
98
98
 
99
+ def _assert_trusted_executable(executable: Path) -> None:
100
+ """Raise BridgeUnavailable if executable does not pass the bridge trust checks."""
101
+ try:
102
+ st = executable.stat()
103
+ except OSError as exc:
104
+ raise BridgeUnavailable("bounded-loops bridge path is unavailable") from exc
105
+ mode = st.st_mode
106
+ if not stat.S_ISREG(mode) or (
107
+ os.name != "nt" and mode & (stat.S_IWGRP | stat.S_IWOTH)
108
+ ):
109
+ raise BridgeUnavailable("bounded-loops executable is not a trusted regular file")
110
+ # Windows trust-check limitation: only S_ISREG applies on Windows.
111
+ # The mode-bit check above (group/other writable) and the uid/owner check
112
+ # below are both skipped because:
113
+ # - Python's os.stat() on Windows returns emulated Unix-style mode bits
114
+ # that do not reflect real ACL permissions; the check would be meaningless.
115
+ # - os.geteuid() does not exist on Windows (AttributeError); st_uid is
116
+ # always 0 there, so the ownership check cannot identify non-root owners.
117
+ # Proper Windows ownership verification requires Win32 ACL APIs (advapi32),
118
+ # which would introduce a heavy optional dependency. Scoped for a future release.
119
+ if os.name != "nt" and st.st_uid not in {0, os.geteuid()}:
120
+ raise BridgeUnavailable("bounded-loops executable owner is not trusted")
121
+
99
122
  async def observe_from_stdio(*, command: str, cwd: str, profile_id: str) -> list[dict[str, Any]]:
100
123
  """Run one bounded, explicit MCP 2 observation; never call from recall or remember."""
101
124
  executable, workspace = Path(command), Path(cwd)
@@ -112,15 +135,9 @@ async def observe_from_stdio(*, command: str, cwd: str, profile_id: str) -> list
112
135
  try:
113
136
  executable = executable.resolve(strict=True)
114
137
  workspace = workspace.resolve(strict=True)
115
- mode = executable.stat().st_mode
116
138
  except OSError as exc:
117
139
  raise BridgeUnavailable("bounded-loops bridge path is unavailable") from exc
118
- if not stat.S_ISREG(mode) or (
119
- os.name != "nt" and mode & (stat.S_IWGRP | stat.S_IWOTH)
120
- ):
121
- raise BridgeUnavailable("bounded-loops executable is not a trusted regular file")
122
- if executable.stat().st_uid not in {0, os.geteuid()}:
123
- raise BridgeUnavailable("bounded-loops executable owner is not trusted")
140
+ _assert_trusted_executable(executable)
124
141
 
125
142
  from mcp import ClientSession, StdioServerParameters
126
143
  from mcp.client.stdio import stdio_client
@@ -4,8 +4,6 @@
4
4
 
5
5
  """Static 40-arm catalog for the contextual Thompson bandit.
6
6
 
7
- LLD reference: ``.backup/active-brain/lld/LLD-03-contextual-bandit-and-ensemble.md``
8
- Section 5.1 — arm = (semantic, bm25, entity_graph, temporal,
9
7
  cross_encoder_bias) weight bundle drawn from a 7-point canonical grid.
10
8
 
11
9
  Pure-data module — zero imports from the rest of the codebase. Audit-friendly
@@ -4,8 +4,6 @@
4
4
 
5
5
  """Contextual Thompson-sampling bandit over discrete channel-weight arms.
6
6
 
7
- LLD reference: ``.backup/active-brain/lld/LLD-03-contextual-bandit-and-ensemble.md``
8
- Sections 3 (algorithm), 5.3 (file spec), 8 (hard rules).
9
7
 
10
8
  Schema: ``bandit_arms`` + ``bandit_plays`` live in ``learning.db``, created by
11
9
  LLD-07 M005. This module NEVER defines DDL — it only READs / WRITEs.
@@ -4,8 +4,6 @@
4
4
 
5
5
  """Per-(profile, stratum) posterior LRU cache for the contextual bandit.
6
6
 
7
- LLD reference: ``.backup/active-brain/lld/LLD-03-contextual-bandit-and-ensemble.md``
8
- Section 5.2.
9
7
 
10
8
  Key design:
11
9
  - Loader runs OUTSIDE the lock so DB reads never serialise across strata.
@@ -28,6 +28,7 @@ from pathlib import Path
28
28
  from typing import Any, Iterable, Sequence
29
29
 
30
30
  from superlocalmemory.core.ram_lock import ram_reservation
31
+ from superlocalmemory.storage.embedding_codec import decode_embedding
31
32
 
32
33
  logger = logging.getLogger(__name__)
33
34
 
@@ -71,19 +72,18 @@ __all__ = (
71
72
  )
72
73
 
73
74
 
74
- def _parse_embedding(raw: str | None) -> list[float] | None:
75
+ def _parse_embedding(raw: bytes | str | None) -> list[float] | None:
76
+ """Parse an embedding from either TEXT (JSON) or BLOB (binary float32).
77
+
78
+ Returns None only when the embedding is genuinely absent (None or empty).
79
+ Raises ValueError for malformed data so callers can distinguish absence
80
+ from corruption.
81
+ """
75
82
  if not raw:
76
83
  return None
77
- try:
78
- vec = json.loads(raw)
79
- except (TypeError, ValueError):
80
- return None
81
- if not isinstance(vec, list) or not vec:
82
- return None
83
- try:
84
- return [float(x) for x in vec]
85
- except (TypeError, ValueError):
86
- return None
84
+ # decode_embedding raises ValueError for corrupt data; callers that want
85
+ # to skip a bad fact should catch ValueError explicitly, not silently.
86
+ return decode_embedding(raw)
87
87
 
88
88
 
89
89
  # L-P-01: vectorise ``_cosine`` via NumPy when available. NumPy cold
@@ -4,8 +4,6 @@
4
4
 
5
5
  """Bandit / LightGBM ensemble blender.
6
6
 
7
- LLD reference: ``.backup/active-brain/lld/LLD-03-contextual-bandit-and-ensemble.md``
8
- Section 5.4.
9
7
 
10
8
  D8 blend policy (``choose_ensemble``):
11
9
  - 0..199 signals OR model is None → ``EnsembleWeights(1.0, 0.0)`` (bandit-only).
@@ -4,8 +4,6 @@
4
4
 
5
5
  """Integer-label mapping for LightGBM ``lambdarank`` training.
6
6
 
7
- LLD reference: ``.backup/active-brain/lld/LLD-02-signal-pipeline-and-lightgbm.md``
8
- Section 4.7 — single source of truth for outcome-reward / position → int label.
9
7
 
10
8
  Rules:
11
9
  - Labels are integers in ``[0, 4]`` (5 relevance tiers).
@@ -4,8 +4,6 @@
4
4
 
5
5
  """Legacy ``learning_feedback`` row migration — data movement only.
6
6
 
7
- LLD reference: ``.backup/active-brain/lld/LLD-07-schema-migrations-and-security-primitives.md``
8
- Section 5 (Legacy learning_feedback Migration).
9
7
 
10
8
  Hard rule H15 (LLD-06 §10): this module MUST NOT contain any schema
11
9
  DDL. All schema definitions live in
@@ -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.
@@ -282,7 +282,7 @@ register_optimize_tools(_target) # v3.6.11: Surface B Optimize tools (proxy-fre
282
282
  from superlocalmemory.mcp.tools_loops import register_loop_tools
283
283
  register_loop_tools(_target, get_engine) # v3.8.0: bounded-loop tools (CLI+command+MCP)
284
284
  from superlocalmemory.mcp.tools_ops import register_ops_tools
285
- register_ops_tools(_target, get_engine) # Wave-3: operational recovery & admin remediation
285
+ register_ops_tools(_target, get_engine) # operational recovery & admin remediation
286
286
  from superlocalmemory.mcp.tools_brain import register_brain_tools
287
287
  register_brain_tools(_target, get_engine) # v4.0.2 portable Brain receipts
288
288
  from superlocalmemory.mcp.tools_summaries import register_summary_tools
@@ -0,0 +1,92 @@
1
+ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
2
+ # Licensed under AGPL-3.0-or-later - see LICENSE file
3
+
4
+ """Work out which session a tool call belongs to, the same way every time.
5
+
6
+ ``recall`` has resolved this through a four-step ladder since S9-DASH-10 — the
7
+ explicit argument, then the environment, then the hook registry, then a stable
8
+ per-agent fallback — so an engagement signal lands on the right pending outcome.
9
+ ``remember`` never had it. It took ``session_id: str = ""`` and stored whatever
10
+ it was handed, which for a caller that does not pass one is nothing.
11
+
12
+ Measured on the author's store: **192 of 3,894 genuine facts carry a session_id
13
+ (4.9%)**, and 4 of the 200 most recent (2%). Every one of the rest was written
14
+ through a path that could have known and did not.
15
+
16
+ That is not bookkeeping. ``RetrievalEngine`` promotes results so the top of an
17
+ answer spans more than one session (its ``sessions_in_top`` pass), and a fact
18
+ with no session_id can never be promoted by it — so the diversity mechanism was
19
+ running against a corpus where 95% of rows were indistinguishable. It also means
20
+ "what did we discuss in that session" has almost nothing to match on.
21
+
22
+ One implementation, called by both tools, so the read path and the write path
23
+ cannot disagree about which session they are in.
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ import logging
29
+ import os
30
+
31
+ logger = logging.getLogger(__name__)
32
+
33
+ __all__ = ["resolve_session_id", "SESSION_ENV_VARS"]
34
+
35
+ #: Checked in order. Hosts set one or the other; SLM's own takes precedence so a
36
+ #: user can override a host that sets its variable to something unhelpful.
37
+ SESSION_ENV_VARS = ("SLM_SESSION_ID", "CLAUDE_SESSION_ID")
38
+
39
+
40
+ def resolve_session_id(
41
+ explicit: str = "",
42
+ *,
43
+ agent_id: str = "unknown",
44
+ allow_agent_fallback: bool = True,
45
+ ) -> str:
46
+ """Best available session id for this call. Never raises.
47
+
48
+ Order, most to least specific:
49
+
50
+ 1. ``explicit`` — what the caller passed. Always wins.
51
+ 2. ``SLM_SESSION_ID`` / ``CLAUDE_SESSION_ID`` from the environment.
52
+ 3. The hook registry: the session whose parent process is ours, else the
53
+ most recently active one inside 60 seconds. Parent-PID lookup is
54
+ collision-free across parallel host sessions, because each MCP
55
+ server's parent is the editor that spawned it.
56
+ 4. ``mcp:<agent_id>`` — stable per agent, and deliberately NOT matched by
57
+ the Stop hook, so the reaper settles those outcomes at a neutral 0.5
58
+ rather than crediting or blaming a session that never existed.
59
+
60
+ ``allow_agent_fallback=False`` stops before step 4 and returns "". Use it
61
+ where a synthetic id would be worse than none: grouping memories under
62
+ ``mcp:<agent>`` would put every memory an agent ever wrote into one bucket
63
+ and make session-diversity promotion rank them as a single session, which
64
+ is the opposite of what it is for.
65
+ """
66
+ if explicit and explicit.strip():
67
+ return explicit.strip()
68
+
69
+ for name in SESSION_ENV_VARS:
70
+ value = os.environ.get(name)
71
+ if value and value.strip():
72
+ return value.strip()
73
+
74
+ try:
75
+ from superlocalmemory.hooks.session_registry import (
76
+ lookup_by_parent,
77
+ most_recent_active,
78
+ )
79
+
80
+ found = (
81
+ lookup_by_parent(within_seconds=60)
82
+ or most_recent_active(agent_type="claude", within_seconds=60)
83
+ or ""
84
+ )
85
+ if found:
86
+ return found
87
+ except Exception as exc: # noqa: BLE001 — a hint must never fail a call
88
+ logger.debug("session registry lookup unavailable: %s", exc)
89
+
90
+ if allow_agent_fallback:
91
+ return f"mcp:{agent_id}"
92
+ return ""
@@ -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")
@@ -73,6 +73,7 @@ def register_core_tools(server, get_engine: Callable) -> None:
73
73
  scope: str | None = None,
74
74
  shared_with: str = "",
75
75
  idempotency_key: str = "",
76
+ session_date: str = "",
76
77
  ) -> dict:
77
78
  """Store content to memory with intelligent indexing.
78
79
 
@@ -82,11 +83,35 @@ def register_core_tools(server, get_engine: Callable) -> None:
82
83
  Multi-scope: ``scope`` sets visibility (personal/shared/global).
83
84
  ``shared_with`` is a comma-separated list of profile_ids for
84
85
  shared scope.
86
+
87
+ ``session_date`` says WHEN the memory is about, as opposed to when it
88
+ was written. Omit it and the memory is dated today, which is what every
89
+ memory got before 4.0.10 because there was no way to say otherwise.
90
+ Accepts YYYY-MM-DD or a full ISO 8601 timestamp.
85
91
  """
86
92
  # v3.6.10: resolve "mcp_client" sentinel → URL path (HTTP) or env var (stdio)
87
93
  if agent_id == "mcp_client":
88
94
  from superlocalmemory.mcp.agent_context import get_current_agent_id
89
95
  agent_id = get_current_agent_id()
96
+ # Bind the write to a session the same way the read path does.
97
+ #
98
+ # recall has resolved this through a four-step ladder since S9-DASH-10;
99
+ # remember stored whatever it was handed, which for a caller that does
100
+ # not pass one is nothing. Result on the author's store: 192 of 3,894
101
+ # facts carry a session_id (4.9%). The engine's session-diversity
102
+ # promotion cannot promote a fact with no session, so it was running
103
+ # against a corpus where 95% of rows looked like the same session.
104
+ #
105
+ # allow_agent_fallback is OFF here, unlike recall. `mcp:<agent_id>` is a
106
+ # useful key for settling one outcome; as a stored session_id it would
107
+ # file every memory an agent ever wrote under one session, and diversity
108
+ # promotion would then treat a whole history as a single conversation —
109
+ # worse than the empty string it replaces.
110
+ from superlocalmemory.mcp.session_binding import resolve_session_id
111
+
112
+ session_id = resolve_session_id(
113
+ session_id, agent_id=agent_id, allow_agent_fallback=False,
114
+ )
90
115
  meta = {
91
116
  "project": project,
92
117
  "importance": importance,
@@ -141,6 +166,7 @@ def register_core_tools(server, get_engine: Callable) -> None:
141
166
  "content": content, "tags": tags, "metadata": meta,
142
167
  "scope": scope, "shared_with": _shared_list,
143
168
  "session_id": session_id,
169
+ "session_date": session_date,
144
170
  "idempotency_key": effective_idempotency_key or None,
145
171
  })
146
172
  if resp and (resp.get("fact_ids") is not None or resp.get("ok")):
@@ -320,45 +346,20 @@ def register_core_tools(server, get_engine: Callable) -> None:
320
346
  import asyncio
321
347
  try:
322
348
  from superlocalmemory.mcp._daemon_proxy import choose_pool
323
- # S9-DASH-10: priority for session_id, so engagement
324
- # signals land on the right pending_outcome:
325
- # 1. Explicit ``session_id`` tool-call argument.
326
- # 2. ``SLM_SESSION_ID`` / ``CLAUDE_SESSION_ID`` env var.
327
- # 3. Most-recent-active Claude session from the hook
328
- # registry (last 60s). This catches the common case
329
- # where Claude Code's hooks ran the UserPromptSubmit
330
- # hook right before invoking the MCP tool.
331
- # 4. Stable per-agent fallback ``mcp:<agent_id>`` the
332
- # Stop hook will NOT match this, so the reaper
333
- # settles it at neutral 0.5.
334
- effective_sid = session_id
335
- if not effective_sid:
336
- import os as _os
337
- effective_sid = (
338
- _os.environ.get("SLM_SESSION_ID")
339
- or _os.environ.get("CLAUDE_SESSION_ID")
340
- or ""
341
- )
342
- if not effective_sid:
343
- try:
344
- from superlocalmemory.hooks.session_registry import (
345
- lookup_by_parent,
346
- most_recent_active,
347
- )
348
- # Parent-PID lookup is collision-free across multiple
349
- # parallel Claude sessions (each MCP server's parent
350
- # is the IDE that spawned it).
351
- effective_sid = (
352
- lookup_by_parent(within_seconds=60)
353
- or most_recent_active(
354
- agent_type="claude", within_seconds=60,
355
- )
356
- or ""
357
- )
358
- except Exception:
359
- pass
360
- if not effective_sid:
361
- effective_sid = f"mcp:{agent_id}"
349
+ from superlocalmemory.mcp.session_binding import resolve_session_id
350
+
351
+ # S9-DASH-10's four-step ladder, now shared with remember() so the
352
+ # read path and the write path cannot disagree about which session
353
+ # they are in. remember() had no ladder at all, which is why 95% of
354
+ # stored facts carry no session_id. See mcp/session_binding.py.
355
+ #
356
+ # The per-agent fallback stays ON here: this id settles a pending
357
+ # outcome, and `mcp:<agent_id>` is deliberately not matched by the
358
+ # Stop hook, so the reaper settles it at a neutral 0.5 rather than
359
+ # attributing engagement to a session that never existed.
360
+ effective_sid = resolve_session_id(
361
+ session_id, agent_id=agent_id, allow_agent_fallback=True,
362
+ )
362
363
  # Resolve the daemon proxy inside the worker too. ``choose_pool``
363
364
  # verifies daemon ownership through a synchronous /health request;
364
365
  # when this tool is served by the daemon's mounted HTTP MCP app,
@@ -560,7 +561,7 @@ def register_core_tools(server, get_engine: Callable) -> None:
560
561
  if db_path.exists():
561
562
  db_size_mb = round(os.path.getsize(db_path) / (1024 * 1024), 2)
562
563
 
563
- # WP-02 D8: additive canonical key set — provider/base_dir/db_path added.
564
+ # additive canonical key set — provider/base_dir/db_path added.
564
565
  # All pre-existing keys are preserved (zero removals).
565
566
  cfg = engine._config
566
567
  return {
@@ -2,7 +2,7 @@
2
2
  # Licensed under AGPL-3.0-or-later - see LICENSE file
3
3
  # Part of SuperLocalMemory V4 | https://qualixar.com | https://varunpratap.com
4
4
 
5
- """Wave-3 Operational Recovery & Admin Remediation MCP tools (2 tools).
5
+ """Operational Recovery & Admin Remediation MCP tools (2 tools).
6
6
 
7
7
  list_failed_operations — Surface dead-letter, degraded, and exhausted ops.
8
8
  resolve_operation — Admin retry/force-reconcile/cancel for stuck ops.
@@ -28,7 +28,7 @@ _VALID_ACTIONS = frozenset({"retry", "force_reconcile", "cancel"})
28
28
 
29
29
 
30
30
  def register_ops_tools(server, get_engine: Callable) -> None:
31
- """Register Wave-3 operational-recovery MCP tools on *server*."""
31
+ """Register operational-recovery MCP tools on *server*."""
32
32
 
33
33
  # ------------------------------------------------------------------
34
34
  # 1. list_failed_operations — surface all stuck/failed/degraded ops
@@ -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: