superlocalmemory 3.4.18 → 3.4.21

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 (172) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/README.md +42 -34
  3. package/bin/slm +11 -0
  4. package/bin/slm.bat +12 -0
  5. package/package.json +4 -3
  6. package/pyproject.toml +3 -2
  7. package/scripts/build-slm-hook.ps1 +40 -0
  8. package/scripts/build-slm-hook.sh +45 -0
  9. package/scripts/build_entry.py +452 -0
  10. package/scripts/ci/stage5b_gate.sh +50 -0
  11. package/scripts/postinstall/validation.js +187 -0
  12. package/scripts/postinstall-interactive.js +756 -0
  13. package/scripts/postinstall_binary.js +287 -0
  14. package/scripts/release_manifest.py +273 -0
  15. package/scripts/slm-hook.spec +56 -0
  16. package/skills/slm-build-graph/SKILL.md +423 -0
  17. package/skills/slm-list-recent/SKILL.md +348 -0
  18. package/skills/slm-recall/SKILL.md +343 -0
  19. package/skills/slm-remember/SKILL.md +194 -0
  20. package/skills/slm-show-patterns/SKILL.md +224 -0
  21. package/skills/slm-status/SKILL.md +363 -0
  22. package/skills/slm-switch-profile/SKILL.md +442 -0
  23. package/src/superlocalmemory/cli/commands.py +219 -79
  24. package/src/superlocalmemory/cli/context_commands.py +192 -0
  25. package/src/superlocalmemory/cli/daemon.py +15 -1
  26. package/src/superlocalmemory/cli/db_migrate.py +80 -0
  27. package/src/superlocalmemory/cli/escape_hatch.py +220 -0
  28. package/src/superlocalmemory/cli/main.py +72 -1
  29. package/src/superlocalmemory/core/context_cache.py +397 -0
  30. package/src/superlocalmemory/core/embeddings.py +8 -2
  31. package/src/superlocalmemory/core/engine.py +38 -2
  32. package/src/superlocalmemory/core/engine_wiring.py +1 -1
  33. package/src/superlocalmemory/core/ram_lock.py +111 -0
  34. package/src/superlocalmemory/core/recall_pipeline.py +433 -3
  35. package/src/superlocalmemory/core/recall_worker.py +8 -3
  36. package/src/superlocalmemory/core/security_primitives.py +635 -0
  37. package/src/superlocalmemory/core/shadow_router.py +319 -0
  38. package/src/superlocalmemory/core/slm_disabled.py +87 -0
  39. package/src/superlocalmemory/core/slmignore.py +125 -0
  40. package/src/superlocalmemory/core/topic_signature.py +143 -0
  41. package/src/superlocalmemory/core/worker_pool.py +14 -3
  42. package/src/superlocalmemory/encoding/cognitive_consolidator.py +2 -2
  43. package/src/superlocalmemory/evolution/budget.py +321 -0
  44. package/src/superlocalmemory/evolution/llm_dispatch.py +508 -0
  45. package/src/superlocalmemory/evolution/skill_evolver.py +144 -94
  46. package/src/superlocalmemory/hooks/_outcome_common.py +506 -0
  47. package/src/superlocalmemory/hooks/adapter_base.py +317 -0
  48. package/src/superlocalmemory/hooks/antigravity_adapter.py +192 -0
  49. package/src/superlocalmemory/hooks/claude_code_hooks.py +33 -1
  50. package/src/superlocalmemory/hooks/context_payload.py +312 -0
  51. package/src/superlocalmemory/hooks/copilot_adapter.py +154 -0
  52. package/src/superlocalmemory/hooks/cross_platform_connector.py +90 -0
  53. package/src/superlocalmemory/hooks/cursor_adapter.py +195 -0
  54. package/src/superlocalmemory/hooks/hook_handlers.py +109 -8
  55. package/src/superlocalmemory/hooks/ide_connector.py +25 -2
  56. package/src/superlocalmemory/hooks/post_tool_async_hook.py +165 -0
  57. package/src/superlocalmemory/hooks/post_tool_outcome_hook.py +223 -0
  58. package/src/superlocalmemory/hooks/prewarm_auth.py +170 -0
  59. package/src/superlocalmemory/hooks/session_registry.py +186 -0
  60. package/src/superlocalmemory/hooks/stop_outcome_hook.py +134 -0
  61. package/src/superlocalmemory/hooks/sync_loop.py +114 -0
  62. package/src/superlocalmemory/hooks/user_prompt_hook.py +128 -0
  63. package/src/superlocalmemory/hooks/user_prompt_rehash_hook.py +202 -0
  64. package/src/superlocalmemory/infra/backup.py +3 -3
  65. package/src/superlocalmemory/infra/cloud_backup.py +2 -2
  66. package/src/superlocalmemory/infra/event_bus.py +2 -2
  67. package/src/superlocalmemory/infra/webhook_dispatcher.py +3 -3
  68. package/src/superlocalmemory/learning/arm_catalog.py +99 -0
  69. package/src/superlocalmemory/learning/bandit.py +526 -0
  70. package/src/superlocalmemory/learning/bandit_cache.py +133 -0
  71. package/src/superlocalmemory/learning/behavioral.py +53 -1
  72. package/src/superlocalmemory/learning/consolidation_cycle.py +381 -0
  73. package/src/superlocalmemory/learning/consolidation_worker.py +188 -520
  74. package/src/superlocalmemory/learning/database.py +256 -0
  75. package/src/superlocalmemory/learning/dedup_hnsw.py +413 -0
  76. package/src/superlocalmemory/learning/ensemble.py +300 -0
  77. package/src/superlocalmemory/learning/fact_outcome_joins.py +207 -0
  78. package/src/superlocalmemory/learning/forgetting_scheduler.py +55 -0
  79. package/src/superlocalmemory/learning/hnsw_dedup.py +69 -0
  80. package/src/superlocalmemory/learning/labeler.py +87 -0
  81. package/src/superlocalmemory/learning/legacy_migration.py +277 -0
  82. package/src/superlocalmemory/learning/memory_merge.py +160 -0
  83. package/src/superlocalmemory/learning/model_cache.py +269 -0
  84. package/src/superlocalmemory/learning/model_rollback.py +278 -0
  85. package/src/superlocalmemory/learning/outcome_queue.py +284 -0
  86. package/src/superlocalmemory/learning/pattern_miner.py +415 -0
  87. package/src/superlocalmemory/learning/pattern_miner_constants.py +47 -0
  88. package/src/superlocalmemory/learning/ranker.py +225 -81
  89. package/src/superlocalmemory/learning/ranker_common.py +163 -0
  90. package/src/superlocalmemory/learning/ranker_retrain_legacy.py +202 -0
  91. package/src/superlocalmemory/learning/ranker_retrain_online.py +411 -0
  92. package/src/superlocalmemory/learning/reward.py +777 -0
  93. package/src/superlocalmemory/learning/reward_archive.py +210 -0
  94. package/src/superlocalmemory/learning/reward_boost.py +201 -0
  95. package/src/superlocalmemory/learning/reward_proxy.py +326 -0
  96. package/src/superlocalmemory/learning/shadow_test.py +524 -0
  97. package/src/superlocalmemory/learning/signal_worker.py +270 -0
  98. package/src/superlocalmemory/learning/signals.py +314 -0
  99. package/src/superlocalmemory/learning/trigram_index.py +547 -0
  100. package/src/superlocalmemory/mcp/server.py +5 -5
  101. package/src/superlocalmemory/mcp/tools_context.py +183 -0
  102. package/src/superlocalmemory/mcp/tools_core.py +92 -27
  103. package/src/superlocalmemory/parameterization/soft_prompt_generator.py +13 -0
  104. package/src/superlocalmemory/retrieval/engine.py +52 -0
  105. package/src/superlocalmemory/retrieval/reranker.py +4 -2
  106. package/src/superlocalmemory/server/api.py +2 -2
  107. package/src/superlocalmemory/server/bandit_loops.py +140 -0
  108. package/src/superlocalmemory/server/middleware/__init__.py +11 -0
  109. package/src/superlocalmemory/server/middleware/security_headers.py +144 -0
  110. package/src/superlocalmemory/server/routes/backup.py +36 -13
  111. package/src/superlocalmemory/server/routes/behavioral.py +50 -19
  112. package/src/superlocalmemory/server/routes/brain.py +1234 -0
  113. package/src/superlocalmemory/server/routes/data_io.py +4 -4
  114. package/src/superlocalmemory/server/routes/events.py +2 -2
  115. package/src/superlocalmemory/server/routes/helpers.py +1 -1
  116. package/src/superlocalmemory/server/routes/learning.py +192 -7
  117. package/src/superlocalmemory/server/routes/memories.py +189 -1
  118. package/src/superlocalmemory/server/routes/prewarm.py +171 -0
  119. package/src/superlocalmemory/server/routes/profiles.py +3 -3
  120. package/src/superlocalmemory/server/routes/token.py +88 -0
  121. package/src/superlocalmemory/server/routes/ws.py +5 -5
  122. package/src/superlocalmemory/server/security_middleware.py +13 -7
  123. package/src/superlocalmemory/server/ui.py +2 -2
  124. package/src/superlocalmemory/server/unified_daemon.py +335 -3
  125. package/src/superlocalmemory/storage/migration_runner.py +545 -0
  126. package/src/superlocalmemory/storage/migrations/M001_add_signal_features_columns.py +67 -0
  127. package/src/superlocalmemory/storage/migrations/M002_model_state_history.py +132 -0
  128. package/src/superlocalmemory/storage/migrations/M003_migration_log.py +38 -0
  129. package/src/superlocalmemory/storage/migrations/M004_cross_platform_sync_log.py +46 -0
  130. package/src/superlocalmemory/storage/migrations/M005_bandit_tables.py +75 -0
  131. package/src/superlocalmemory/storage/migrations/M006_action_outcomes_reward.py +75 -0
  132. package/src/superlocalmemory/storage/migrations/M007_pending_outcomes.py +63 -0
  133. package/src/superlocalmemory/storage/migrations/M009_model_lineage.py +54 -0
  134. package/src/superlocalmemory/storage/migrations/M010_evolution_config.py +75 -0
  135. package/src/superlocalmemory/storage/migrations/M011_archive_and_merge.py +87 -0
  136. package/src/superlocalmemory/storage/migrations/M012_shadow_observations.py +72 -0
  137. package/src/superlocalmemory/storage/migrations/M013_bi_temporal_columns.py +55 -0
  138. package/src/superlocalmemory/storage/migrations/__init__.py +81 -0
  139. package/src/superlocalmemory/storage/models.py +4 -0
  140. package/src/superlocalmemory/ui/css/brain.css +409 -0
  141. package/src/superlocalmemory/ui/css/legacy-dashboard.css +645 -0
  142. package/src/superlocalmemory/ui/index.html +459 -1345
  143. package/src/superlocalmemory/ui/js/brain.js +1321 -0
  144. package/src/superlocalmemory/ui/js/clusters.js +123 -4
  145. package/src/superlocalmemory/ui/js/init.js +48 -39
  146. package/src/superlocalmemory/ui/js/memories.js +88 -2
  147. package/src/superlocalmemory/ui/js/modal.js +71 -1
  148. package/src/superlocalmemory/ui/js/ng-shell.js +101 -88
  149. package/src/superlocalmemory/ui/js/trust-dashboard.js +168 -25
  150. package/src/superlocalmemory/ui/vendor/bootstrap-icons/bootstrap-icons.css +2018 -0
  151. package/src/superlocalmemory/ui/vendor/bootstrap-icons/fonts/bootstrap-icons.woff +0 -0
  152. package/src/superlocalmemory/ui/vendor/bootstrap-icons/fonts/bootstrap-icons.woff2 +0 -0
  153. package/src/superlocalmemory/ui/vendor/bootstrap.bundle.min.js +7 -0
  154. package/src/superlocalmemory/ui/vendor/bootstrap.min.css +6 -0
  155. package/src/superlocalmemory/ui/vendor/d3.v7.min.js +2 -0
  156. package/src/superlocalmemory/ui/vendor/graphology-library.min.js +2 -0
  157. package/src/superlocalmemory/ui/vendor/graphology.umd.min.js +2 -0
  158. package/src/superlocalmemory/ui/vendor/inter-ui/inter-variable.min.css +8 -0
  159. package/src/superlocalmemory/ui/vendor/inter-ui/variable/InterVariable-Italic.woff2 +0 -0
  160. package/src/superlocalmemory/ui/vendor/inter-ui/variable/InterVariable.woff2 +0 -0
  161. package/src/superlocalmemory/ui/vendor/sigma.min.js +1 -0
  162. package/src/superlocalmemory/ui/js/behavioral.js +0 -447
  163. package/src/superlocalmemory/ui/js/graph-core.js +0 -447
  164. package/src/superlocalmemory/ui/js/graph-interactions.js +0 -351
  165. package/src/superlocalmemory/ui/js/learning.js +0 -435
  166. package/src/superlocalmemory/ui/js/patterns.js +0 -93
  167. package/src/superlocalmemory.egg-info/PKG-INFO +0 -647
  168. package/src/superlocalmemory.egg-info/SOURCES.txt +0 -335
  169. package/src/superlocalmemory.egg-info/dependency_links.txt +0 -1
  170. package/src/superlocalmemory.egg-info/entry_points.txt +0 -2
  171. package/src/superlocalmemory.egg-info/requires.txt +0 -58
  172. package/src/superlocalmemory.egg-info/top_level.txt +0 -1
@@ -0,0 +1,397 @@
1
+ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
2
+ # Licensed under AGPL-3.0-or-later - see LICENSE file
3
+ # Part of SuperLocalMemory v3.4.21 — LLD-01 §4.1
4
+
5
+ """Context cache — separate SQLite WAL DB, read-path <10 ms.
6
+
7
+ LLD reference: `.backup/active-brain/lld/LLD-01-context-cache-and-hot-path-hooks.md`
8
+ Section 4.1.
9
+
10
+ Two concerns in one module:
11
+ - Writer (``ContextCache``) — used by the daemon only. Owns pragmas,
12
+ schema bootstrap, install-binding row, LRU sweep.
13
+ - Reader (``read_entry_fast``) — used by the UserPromptSubmit hook.
14
+ Read-only SQLite URI, no pragmas, NEVER raises.
15
+
16
+ Hot-path contract (``read_entry_fast``):
17
+ - stdlib-only imports.
18
+ - Never raises: any exception → returns ``None`` (fail-open miss).
19
+ - Verifies install-binding HMAC to reject a DB at an env-var-hijacked
20
+ path.
21
+ - Applies TTL in SQL, not Python, to avoid fetching stale rows.
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ import hashlib
27
+ import hmac
28
+ import json
29
+ import os
30
+ import sqlite3
31
+ import time
32
+ from dataclasses import dataclass, field
33
+ from pathlib import Path
34
+
35
+ from superlocalmemory.core.security_primitives import (
36
+ PathTraversalError,
37
+ ensure_install_token,
38
+ redact_secrets,
39
+ safe_resolve,
40
+ )
41
+
42
+
43
+ CACHE_DB_DEFAULT: Path = Path.home() / ".superlocalmemory" / "active_brain_cache.db"
44
+ INSTALL_TOKEN_DEFAULT: Path = Path.home() / ".superlocalmemory" / ".install_token"
45
+
46
+ TTL_SECONDS: int = 120
47
+ CLEANUP_HORIZON_SECONDS: int = 600
48
+ MAX_BYTES: int = 50 * 1024 * 1024
49
+ MAX_CONTENT_CHARS: int = 4000
50
+ SCHEMA_VERSION: str = "3.4.21"
51
+
52
+ _HMAC_MATERIAL: bytes = b"active_brain_cache"
53
+ _HMAC_HEX_LEN: int = 32
54
+
55
+
56
+ @dataclass(frozen=True, slots=True)
57
+ class CacheEntry:
58
+ """Single cache row. ``content`` is expected to be pre-redacted."""
59
+
60
+ session_id: str
61
+ topic_sig: str
62
+ content: str
63
+ fact_ids: list[str] = field(default_factory=list)
64
+ provenance: str = "tool_observation"
65
+ computed_at: int = 0
66
+ byte_size: int = 0
67
+
68
+
69
+ # ---------------------------------------------------------------------------
70
+ # Internal helpers
71
+ # ---------------------------------------------------------------------------
72
+
73
+
74
+ def _expected_binding_hmac(token: str) -> str:
75
+ """Compute the HMAC-SHA256 first-32-hex value for the install token."""
76
+ return hmac.new(
77
+ token.encode("utf-8"), _HMAC_MATERIAL, hashlib.sha256,
78
+ ).hexdigest()[:_HMAC_HEX_LEN]
79
+
80
+
81
+ def _read_install_token(home: Path) -> str | None:
82
+ """Read the install token without creating it. Returns None if missing."""
83
+ token_path = home / ".install_token"
84
+ if not token_path.exists():
85
+ return None
86
+ try:
87
+ token = token_path.read_text(encoding="utf-8").strip()
88
+ except OSError: # pragma: no cover — disk-IO failure under contention
89
+ return None
90
+ return token or None
91
+
92
+
93
+ # ---------------------------------------------------------------------------
94
+ # Writer (daemon-side)
95
+ # ---------------------------------------------------------------------------
96
+
97
+
98
+ class ContextCache:
99
+ """Writer-side cache. One instance per daemon process.
100
+
101
+ Opens the DB with WAL + NORMAL sync + bounded mmap, bootstraps the
102
+ schema, and writes an install-bound HMAC into ``slm_meta`` so the
103
+ reader can reject a foreign DB pointed at via ``SLM_CACHE_DB``.
104
+ """
105
+
106
+ def __init__(
107
+ self,
108
+ db_path: Path | None = None,
109
+ home_dir: Path | None = None,
110
+ ) -> None:
111
+ self._home = home_dir or (Path.home() / ".superlocalmemory")
112
+ self._home.mkdir(parents=True, exist_ok=True)
113
+
114
+ raw = db_path or (self._home / "active_brain_cache.db")
115
+ self._db_path = safe_resolve(self._home, Path(raw).name) \
116
+ if Path(raw).parent == self._home else \
117
+ safe_resolve(self._home, raw)
118
+
119
+ self._write_conn = self._open_writer()
120
+ self._bootstrap_schema_and_meta()
121
+
122
+ # -- Open / bootstrap ---------------------------------------------------
123
+
124
+ def _open_writer(self) -> sqlite3.Connection:
125
+ # Pre-create the file with 0600 if it's missing, so sqlite inherits
126
+ # the restrictive mode instead of umask default.
127
+ if not self._db_path.exists():
128
+ flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL
129
+ if hasattr(os, "O_NOFOLLOW"):
130
+ flags |= os.O_NOFOLLOW
131
+ try:
132
+ fd = os.open(str(self._db_path), flags, 0o600)
133
+ os.close(fd)
134
+ except FileExistsError: # pragma: no cover — race on concurrent start
135
+ pass
136
+ # On POSIX, enforce mode in case an earlier process created it.
137
+ if os.name != "nt":
138
+ try:
139
+ os.chmod(self._db_path, 0o600)
140
+ except OSError: # pragma: no cover — remote FS without chmod
141
+ pass
142
+
143
+ conn = sqlite3.connect(
144
+ str(self._db_path), isolation_level=None, timeout=5.0,
145
+ )
146
+ conn.execute("PRAGMA journal_mode=WAL")
147
+ conn.execute("PRAGMA synchronous=NORMAL")
148
+ conn.execute("PRAGMA temp_store=MEMORY")
149
+ conn.execute("PRAGMA cache_size=-32768")
150
+ conn.execute("PRAGMA busy_timeout=500")
151
+ # 64 MB writer mmap — reader opens with defaults to keep budget tight.
152
+ try:
153
+ conn.execute("PRAGMA mmap_size=67108864")
154
+ except sqlite3.Error: # pragma: no cover — some builds disable mmap
155
+ pass
156
+ return conn
157
+
158
+ def _bootstrap_schema_and_meta(self) -> None:
159
+ self._write_conn.executescript(
160
+ """
161
+ CREATE TABLE IF NOT EXISTS context_entries (
162
+ session_id TEXT NOT NULL,
163
+ topic_sig TEXT NOT NULL,
164
+ content TEXT NOT NULL,
165
+ fact_ids TEXT NOT NULL,
166
+ provenance TEXT NOT NULL DEFAULT 'tool_observation',
167
+ computed_at INTEGER NOT NULL,
168
+ byte_size INTEGER NOT NULL,
169
+ PRIMARY KEY (session_id, topic_sig)
170
+ ) WITHOUT ROWID;
171
+
172
+ CREATE INDEX IF NOT EXISTS idx_ctx_session_time
173
+ ON context_entries(session_id, computed_at);
174
+ CREATE INDEX IF NOT EXISTS idx_ctx_time
175
+ ON context_entries(computed_at);
176
+
177
+ CREATE TABLE IF NOT EXISTS slm_meta (
178
+ key TEXT PRIMARY KEY,
179
+ value TEXT NOT NULL,
180
+ created_at INTEGER NOT NULL
181
+ );
182
+ """
183
+ )
184
+ token = ensure_install_token()
185
+ now = int(time.time())
186
+ self._write_conn.execute(
187
+ "INSERT OR IGNORE INTO slm_meta (key, value, created_at) "
188
+ "VALUES (?, ?, ?)",
189
+ ("install_token_hmac", _expected_binding_hmac(token), now),
190
+ )
191
+ self._write_conn.execute(
192
+ "INSERT OR IGNORE INTO slm_meta (key, value, created_at) "
193
+ "VALUES (?, ?, ?)",
194
+ ("schema_version", SCHEMA_VERSION, now),
195
+ )
196
+
197
+ # -- Write path ---------------------------------------------------------
198
+
199
+ def upsert(self, entry: CacheEntry) -> None:
200
+ """Insert-or-replace a cache row.
201
+
202
+ Content is redacted (belt-and-suspenders — caller should have done
203
+ this already per LLD-07 §6.3) and truncated. Byte size is computed
204
+ here, not trusted from caller, so LRU accounting stays accurate.
205
+ Does NOT run LRU sweep inline (PERF-01-07) — that's a background
206
+ task on the daemon.
207
+ """
208
+ content = redact_secrets(entry.content)[:MAX_CONTENT_CHARS]
209
+ fact_ids_json = json.dumps(list(entry.fact_ids))
210
+ byte_size = (
211
+ len(content.encode("utf-8"))
212
+ + len(fact_ids_json)
213
+ + len(entry.session_id)
214
+ + len(entry.topic_sig)
215
+ )
216
+ computed_at = entry.computed_at or int(time.time())
217
+ self._write_conn.execute(
218
+ """
219
+ INSERT OR REPLACE INTO context_entries
220
+ (session_id, topic_sig, content, fact_ids,
221
+ provenance, computed_at, byte_size)
222
+ VALUES (?, ?, ?, ?, ?, ?, ?)
223
+ """,
224
+ (entry.session_id, entry.topic_sig, content, fact_ids_json,
225
+ entry.provenance, computed_at, byte_size),
226
+ )
227
+
228
+ # -- Cleanup ------------------------------------------------------------
229
+
230
+ def cleanup_session(
231
+ self, session_id: str, *, older_than: int = CLEANUP_HORIZON_SECONDS,
232
+ ) -> int:
233
+ """Delete rows for ``session_id`` older than ``older_than`` seconds."""
234
+ cutoff = int(time.time()) - older_than
235
+ cur = self._write_conn.execute(
236
+ "DELETE FROM context_entries "
237
+ "WHERE session_id=? AND computed_at < ?",
238
+ (session_id, cutoff),
239
+ )
240
+ return cur.rowcount
241
+
242
+ def cleanup_global_lru(self) -> int:
243
+ """Background sweep — runs every 60s in the daemon.
244
+
245
+ Two passes:
246
+ 1. Time-based — delete rows older than ``CLEANUP_HORIZON_SECONDS``.
247
+ 2. Byte-based — if total size still exceeds ``MAX_BYTES``, delete
248
+ oldest rows (by ``computed_at``) until total is <= 90% of cap.
249
+ Returns total deletions.
250
+ """
251
+ cutoff = int(time.time()) - CLEANUP_HORIZON_SECONDS
252
+ cur = self._write_conn.execute(
253
+ "DELETE FROM context_entries WHERE computed_at < ?", (cutoff,),
254
+ )
255
+ deleted = cur.rowcount
256
+
257
+ total = self._write_conn.execute(
258
+ "SELECT COALESCE(SUM(byte_size), 0) FROM context_entries",
259
+ ).fetchone()[0]
260
+ if total <= MAX_BYTES:
261
+ return deleted
262
+
263
+ target = int(MAX_BYTES * 0.9)
264
+ while total > target:
265
+ rows = self._write_conn.execute(
266
+ "SELECT session_id, topic_sig, byte_size "
267
+ "FROM context_entries "
268
+ "ORDER BY computed_at ASC LIMIT 100",
269
+ ).fetchall()
270
+ if not rows: # pragma: no cover — reached only if table empties mid-sweep
271
+ break
272
+ for sess, sig, size in rows:
273
+ self._write_conn.execute(
274
+ "DELETE FROM context_entries "
275
+ "WHERE session_id=? AND topic_sig=?",
276
+ (sess, sig),
277
+ )
278
+ deleted += 1
279
+ total -= size
280
+ if total <= target:
281
+ break
282
+ return deleted
283
+
284
+ def close(self) -> None:
285
+ try:
286
+ self._write_conn.close()
287
+ except sqlite3.Error: # pragma: no cover — defensive
288
+ pass
289
+
290
+
291
+ # ---------------------------------------------------------------------------
292
+ # Reader (hook-side hot path — NEVER raises)
293
+ # ---------------------------------------------------------------------------
294
+
295
+
296
+ def read_entry_fast(
297
+ session_id: str,
298
+ topic_sig: str,
299
+ *,
300
+ db_path: Path | None = None,
301
+ home_dir: Path | None = None,
302
+ ) -> CacheEntry | None:
303
+ """Hot-path reader used by the UserPromptSubmit hook.
304
+
305
+ Contract (LLD-01 §4.1):
306
+ - NEVER raises. Any exception → returns ``None``.
307
+ - Returns ``None`` on miss, stale (TTL exceeded), missing DB, path
308
+ traversal attempt, or failed install-binding check.
309
+ - Opens the SQLite file read-only via ``?mode=ro`` URI.
310
+ - stdlib-only — no heavy imports, no daemon HTTP call.
311
+ """
312
+ try:
313
+ home = home_dir or (Path.home() / ".superlocalmemory")
314
+ if not home.exists():
315
+ return None
316
+
317
+ requested = db_path or Path(
318
+ os.environ.get("SLM_CACHE_DB") or (home / "active_brain_cache.db"),
319
+ )
320
+ try:
321
+ resolved = safe_resolve(home, Path(requested).resolve())
322
+ except (PathTraversalError, OSError, ValueError):
323
+ return None
324
+
325
+ if not resolved.exists():
326
+ return None
327
+
328
+ token = _read_install_token(home)
329
+ if token is None:
330
+ return None
331
+
332
+ # Read-only URI connection. Short timeout so busy WAL doesn't stall
333
+ # the hot path.
334
+ conn = sqlite3.connect(
335
+ f"file:{resolved}?mode=ro", uri=True, timeout=0.5,
336
+ )
337
+ try:
338
+ # Verify install binding first — cheap SELECT.
339
+ row = conn.execute(
340
+ "SELECT value FROM slm_meta WHERE key='install_token_hmac'",
341
+ ).fetchone()
342
+ if row is None:
343
+ return None
344
+ expected = _expected_binding_hmac(token)
345
+ if not hmac.compare_digest(row[0], expected):
346
+ return None
347
+
348
+ now = int(time.time())
349
+ row = conn.execute(
350
+ """
351
+ SELECT content, fact_ids, provenance, computed_at, byte_size
352
+ FROM context_entries
353
+ WHERE session_id=? AND topic_sig=?
354
+ AND computed_at > ?
355
+ """,
356
+ (session_id, topic_sig, now - TTL_SECONDS),
357
+ ).fetchone()
358
+ finally:
359
+ try:
360
+ conn.close()
361
+ except sqlite3.Error: # pragma: no cover — defensive
362
+ pass
363
+
364
+ if row is None:
365
+ return None
366
+
367
+ try:
368
+ fact_ids = json.loads(row[1])
369
+ if not isinstance(fact_ids, list):
370
+ fact_ids = []
371
+ except (ValueError, TypeError):
372
+ fact_ids = []
373
+
374
+ return CacheEntry(
375
+ session_id=session_id,
376
+ topic_sig=topic_sig,
377
+ content=row[0],
378
+ fact_ids=fact_ids,
379
+ provenance=row[2],
380
+ computed_at=int(row[3]),
381
+ byte_size=int(row[4]),
382
+ )
383
+ except Exception: # pragma: no cover — last-resort fail-open
384
+ return None
385
+
386
+
387
+ __all__ = (
388
+ "CACHE_DB_DEFAULT",
389
+ "CLEANUP_HORIZON_SECONDS",
390
+ "CacheEntry",
391
+ "ContextCache",
392
+ "MAX_BYTES",
393
+ "MAX_CONTENT_CHARS",
394
+ "SCHEMA_VERSION",
395
+ "TTL_SECONDS",
396
+ "read_entry_fast",
397
+ )
@@ -140,8 +140,14 @@ def release_embedding_lock() -> None:
140
140
  _embedding_lock_fd = None
141
141
 
142
142
 
143
- _IDLE_TIMEOUT_SECONDS = 120 # 2 minutes — kill worker after idle
144
- # V3.3.12: Configurable via SLM_EMBED_IDLE_TIMEOUT env var (seconds)
143
+ _IDLE_TIMEOUT_SECONDS = 1800 # 30 minutes — keep model warm across bursty use.
144
+ # V3.3.12: Configurable via SLM_EMBED_IDLE_TIMEOUT env var (seconds).
145
+ # V3.4.19: Bumped from 120 → 1800 to eliminate the 30-60s cold-start pain
146
+ # when the embedding worker was killed too aggressively. Safety: the
147
+ # per-embed RSS self-check (SLM_EMBED_WORKER_RSS_LIMIT_MB, 4GB default) and
148
+ # the daemon memory watchdog (unified_daemon.py, 4GB/60s) still cap any
149
+ # runaway. To restore the old aggressive policy without redeploying, set
150
+ # ``SLM_EMBED_IDLE_TIMEOUT=120`` and ``slm restart``.
145
151
  _IDLE_TIMEOUT_SECONDS = int(os.environ.get("SLM_EMBED_IDLE_TIMEOUT", _IDLE_TIMEOUT_SECONDS))
146
152
  # V3.3.21: Configurable response timeout — 180s default, but batch ingestion
147
153
  # (2-turn chunks across 10 conversations) needs 600s+ to survive cold-start
@@ -346,14 +346,23 @@ class MemoryEngine:
346
346
  self, query: str, profile_id: str | None = None,
347
347
  mode: Mode | None = None, limit: int = 20,
348
348
  agent_id: str = "unknown",
349
+ session_id: str | None = None,
349
350
  ) -> RecallResponse:
350
- """Recall relevant facts for a query."""
351
+ """Recall relevant facts for a query.
352
+
353
+ S9-DASH-02: when ``session_id`` is provided, the recall is
354
+ non-blockingly enqueued to the outcome queue so downstream
355
+ hooks (PostToolUse, Stop) can attach engagement signals.
356
+ Zero additional latency on the hot path — enqueue is a
357
+ ``put_nowait`` and the actual ``pending_outcomes`` INSERT runs
358
+ on a background worker.
359
+ """
351
360
  self._ensure_init()
352
361
 
353
362
  pid = profile_id or self._profile_id
354
363
 
355
364
  from superlocalmemory.core.recall_pipeline import run_recall
356
- return run_recall(
365
+ response = run_recall(
357
366
  query, pid, mode=mode, limit=limit, agent_id=agent_id,
358
367
  config=self._config,
359
368
  retrieval_engine=self._retrieval_engine,
@@ -365,6 +374,33 @@ class MemoryEngine:
365
374
  auto_linker=self._auto_linker,
366
375
  )
367
376
 
377
+ # S9-DASH-02: enqueue for pending_outcomes. Non-blocking; errors
378
+ # swallowed because signal capture is never load-bearing on
379
+ # recall correctness (LLD-02 §4.9, LLD-08 §4.1).
380
+ if session_id:
381
+ try:
382
+ from superlocalmemory.learning.outcome_queue import (
383
+ RecallEvent, enqueue_recall,
384
+ )
385
+ fact_ids = tuple(
386
+ getattr(r.fact, "fact_id", "") or ""
387
+ for r in getattr(response, "results", [])
388
+ if getattr(r, "fact", None) is not None
389
+ )
390
+ fact_ids = tuple(f for f in fact_ids if f)
391
+ if fact_ids:
392
+ enqueue_recall(RecallEvent(
393
+ session_id=session_id,
394
+ profile_id=pid,
395
+ query=query,
396
+ fact_ids=fact_ids,
397
+ query_id=getattr(response, "query_id", "") or "",
398
+ ))
399
+ except Exception:
400
+ pass
401
+
402
+ return response
403
+
368
404
  # -- Session operations -------------------------------------------------
369
405
 
370
406
  def create_speaker_entities(
@@ -476,7 +476,7 @@ def _init_quantization_aware_search(
476
476
  quantized_store=q_store,
477
477
  config=config.quantization,
478
478
  )
479
- logger.info("QuantizationAwareSearch initialized (TurboQuant 3-tier search)")
479
+ logger.info("QuantizationAwareSearch initialized (3-tier quantization pipeline)")
480
480
  return qas
481
481
  except Exception as exc:
482
482
  logger.debug("QuantizationAwareSearch init failed (non-fatal): %s", exc)
@@ -0,0 +1,111 @@
1
+ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
2
+ # Licensed under AGPL-3.0-or-later - see LICENSE file
3
+ # Part of SuperLocalMemory v3.4.21 — LLD-00 §7 + P0.5
4
+
5
+ """Process-wide RAM semaphore for heavyweight consolidation subsystems.
6
+
7
+ LLD-00 §7: hnswlib index builds (LLD-12) and trigram full rebuilds
8
+ (LLD-13) both spike RAM into the hundreds of MB. Running them
9
+ concurrently in the same consolidation tick can blow past the I2 RAM
10
+ ceiling on Light/Minimal profiles. This semaphore serialises them.
11
+
12
+ Design notes:
13
+ - One flock per process pair (exclusive, non-blocking with poll loop).
14
+ - Fast-fail if psutil reports less than ``MIN_FREE_MB + required_mb``
15
+ available before we even try — better to defer than thrash.
16
+ - Lock file lives at ``~/.superlocalmemory/ram_lock.sem`` by default,
17
+ tests may monkeypatch ``RAM_LOCK_PATH``.
18
+ - POSIX only; Windows build is deferred per LLD-00 §7 and MASTER-PLAN H-01.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import fcntl
24
+ import os
25
+ import time
26
+ from contextlib import contextmanager
27
+ from pathlib import Path
28
+ from typing import Iterator
29
+
30
+ import psutil
31
+
32
+
33
+ RAM_LOCK_PATH: Path = Path.home() / ".superlocalmemory" / "ram_lock.sem"
34
+ MIN_FREE_MB: int = 400
35
+
36
+
37
+ @contextmanager
38
+ def ram_reservation(
39
+ name: str,
40
+ *,
41
+ timeout_s: float = 60.0,
42
+ required_mb: int = 200,
43
+ ) -> Iterator[None]:
44
+ """Process-wide RAM semaphore. Acquire before heavy subsystems.
45
+
46
+ Usage::
47
+
48
+ with ram_reservation('hnswlib', required_mb=200):
49
+ build_index(...)
50
+
51
+ Guarantees:
52
+
53
+ - Fast-fails if ``psutil.virtual_memory().available < (MIN_FREE_MB +
54
+ required_mb) * 1024 * 1024``. No body execution in that case.
55
+ - Acquires the exclusive flock on ``RAM_LOCK_PATH`` with a polling
56
+ wait. Raises ``RuntimeError`` after ``timeout_s``.
57
+ - Releases the lock on normal exit AND on exception propagation.
58
+ - Writes a short audit line ``<pid>:<name>\\n`` on acquire so
59
+ an operator can see which subsystem holds it.
60
+ """
61
+ if not isinstance(name, str) or not name:
62
+ raise ValueError("name must be a non-empty string")
63
+ if not isinstance(required_mb, int) or required_mb < 0:
64
+ raise ValueError(f"required_mb must be non-negative int, got {required_mb!r}")
65
+
66
+ vm = psutil.virtual_memory()
67
+ free_mb = vm.available / (1024 * 1024)
68
+ floor_mb = MIN_FREE_MB + required_mb
69
+ if free_mb < floor_mb:
70
+ raise RuntimeError(
71
+ f"ram_reservation({name}): free {free_mb:.0f}MB < required "
72
+ f"{floor_mb}MB"
73
+ )
74
+
75
+ RAM_LOCK_PATH.parent.mkdir(parents=True, exist_ok=True)
76
+ # SEC-M6 — tighten the parent dir so the audit marker (``{pid}:{name}``)
77
+ # is not readable by other UIDs on shared hosts. Idempotent; POSIX-only.
78
+ try:
79
+ if os.name == "posix":
80
+ os.chmod(RAM_LOCK_PATH.parent, 0o700)
81
+ except OSError: # pragma: no cover — perms race
82
+ pass
83
+ fd = os.open(str(RAM_LOCK_PATH), os.O_CREAT | os.O_RDWR, 0o600)
84
+ try:
85
+ deadline = time.time() + timeout_s
86
+ while True:
87
+ try:
88
+ fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
89
+ break
90
+ except BlockingIOError:
91
+ if time.time() >= deadline:
92
+ raise RuntimeError(
93
+ f"ram_reservation({name}) timeout after {timeout_s:.1f}s"
94
+ )
95
+ time.sleep(0.05)
96
+ try:
97
+ # Truncate + write a fresh audit marker.
98
+ os.ftruncate(fd, 0)
99
+ os.lseek(fd, 0, os.SEEK_SET)
100
+ os.write(fd, f"{os.getpid()}:{name}\n".encode("utf-8"))
101
+ except OSError: # pragma: no cover — marker write is best-effort
102
+ pass
103
+ yield
104
+ finally:
105
+ try:
106
+ fcntl.flock(fd, fcntl.LOCK_UN)
107
+ finally:
108
+ os.close(fd)
109
+
110
+
111
+ __all__ = ("RAM_LOCK_PATH", "MIN_FREE_MB", "ram_reservation")