superlocalmemory 3.4.19 → 3.4.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -0
- package/README.md +42 -34
- package/bin/slm +11 -0
- package/bin/slm.bat +12 -0
- package/package.json +4 -3
- package/pyproject.toml +4 -3
- package/scripts/build-slm-hook.ps1 +40 -0
- package/scripts/build-slm-hook.sh +45 -0
- package/scripts/build_entry.py +452 -0
- package/scripts/ci/stage5b_gate.sh +50 -0
- package/scripts/postinstall/validation.js +187 -0
- package/scripts/postinstall-interactive.js +756 -0
- package/scripts/postinstall_binary.js +287 -0
- package/scripts/release_manifest.py +273 -0
- package/scripts/slm-hook.spec +56 -0
- package/skills/slm-build-graph/SKILL.md +423 -0
- package/skills/slm-list-recent/SKILL.md +348 -0
- package/skills/slm-recall/SKILL.md +343 -0
- package/skills/slm-remember/SKILL.md +194 -0
- package/skills/slm-show-patterns/SKILL.md +224 -0
- package/skills/slm-status/SKILL.md +363 -0
- package/skills/slm-switch-profile/SKILL.md +442 -0
- package/src/superlocalmemory/cli/commands.py +254 -79
- package/src/superlocalmemory/cli/context_commands.py +192 -0
- package/src/superlocalmemory/cli/daemon.py +15 -1
- package/src/superlocalmemory/cli/db_migrate.py +80 -0
- package/src/superlocalmemory/cli/escape_hatch.py +220 -0
- package/src/superlocalmemory/cli/main.py +72 -1
- package/src/superlocalmemory/core/context_cache.py +397 -0
- package/src/superlocalmemory/core/engine.py +38 -2
- package/src/superlocalmemory/core/engine_wiring.py +1 -1
- package/src/superlocalmemory/core/ram_lock.py +111 -0
- package/src/superlocalmemory/core/recall_pipeline.py +433 -3
- package/src/superlocalmemory/core/recall_worker.py +8 -3
- package/src/superlocalmemory/core/security_primitives.py +635 -0
- package/src/superlocalmemory/core/shadow_router.py +319 -0
- package/src/superlocalmemory/core/slm_disabled.py +87 -0
- package/src/superlocalmemory/core/slmignore.py +125 -0
- package/src/superlocalmemory/core/topic_signature.py +143 -0
- package/src/superlocalmemory/core/worker_pool.py +14 -3
- package/src/superlocalmemory/encoding/cognitive_consolidator.py +2 -2
- package/src/superlocalmemory/evolution/budget.py +321 -0
- package/src/superlocalmemory/evolution/llm_dispatch.py +508 -0
- package/src/superlocalmemory/evolution/skill_evolver.py +144 -94
- package/src/superlocalmemory/hooks/_outcome_common.py +506 -0
- package/src/superlocalmemory/hooks/adapter_base.py +317 -0
- package/src/superlocalmemory/hooks/antigravity_adapter.py +192 -0
- package/src/superlocalmemory/hooks/claude_code_hooks.py +33 -1
- package/src/superlocalmemory/hooks/context_payload.py +312 -0
- package/src/superlocalmemory/hooks/copilot_adapter.py +154 -0
- package/src/superlocalmemory/hooks/cross_platform_connector.py +90 -0
- package/src/superlocalmemory/hooks/cursor_adapter.py +195 -0
- package/src/superlocalmemory/hooks/hook_handlers.py +109 -8
- package/src/superlocalmemory/hooks/ide_connector.py +25 -2
- package/src/superlocalmemory/hooks/post_tool_async_hook.py +165 -0
- package/src/superlocalmemory/hooks/post_tool_outcome_hook.py +223 -0
- package/src/superlocalmemory/hooks/prewarm_auth.py +170 -0
- package/src/superlocalmemory/hooks/session_registry.py +186 -0
- package/src/superlocalmemory/hooks/stop_outcome_hook.py +134 -0
- package/src/superlocalmemory/hooks/sync_loop.py +114 -0
- package/src/superlocalmemory/hooks/user_prompt_hook.py +128 -0
- package/src/superlocalmemory/hooks/user_prompt_rehash_hook.py +202 -0
- package/src/superlocalmemory/infra/backup.py +3 -3
- package/src/superlocalmemory/infra/cloud_backup.py +2 -2
- package/src/superlocalmemory/infra/event_bus.py +2 -2
- package/src/superlocalmemory/infra/webhook_dispatcher.py +3 -3
- package/src/superlocalmemory/learning/arm_catalog.py +99 -0
- package/src/superlocalmemory/learning/bandit.py +526 -0
- package/src/superlocalmemory/learning/bandit_cache.py +133 -0
- package/src/superlocalmemory/learning/behavioral.py +53 -1
- package/src/superlocalmemory/learning/consolidation_cycle.py +381 -0
- package/src/superlocalmemory/learning/consolidation_worker.py +188 -520
- package/src/superlocalmemory/learning/database.py +256 -0
- package/src/superlocalmemory/learning/dedup_hnsw.py +413 -0
- package/src/superlocalmemory/learning/ensemble.py +300 -0
- package/src/superlocalmemory/learning/fact_outcome_joins.py +207 -0
- package/src/superlocalmemory/learning/forgetting_scheduler.py +55 -0
- package/src/superlocalmemory/learning/hnsw_dedup.py +69 -0
- package/src/superlocalmemory/learning/labeler.py +87 -0
- package/src/superlocalmemory/learning/legacy_migration.py +277 -0
- package/src/superlocalmemory/learning/memory_merge.py +160 -0
- package/src/superlocalmemory/learning/model_cache.py +269 -0
- package/src/superlocalmemory/learning/model_rollback.py +278 -0
- package/src/superlocalmemory/learning/outcome_queue.py +284 -0
- package/src/superlocalmemory/learning/pattern_miner.py +415 -0
- package/src/superlocalmemory/learning/pattern_miner_constants.py +47 -0
- package/src/superlocalmemory/learning/ranker.py +225 -81
- package/src/superlocalmemory/learning/ranker_common.py +163 -0
- package/src/superlocalmemory/learning/ranker_retrain_legacy.py +202 -0
- package/src/superlocalmemory/learning/ranker_retrain_online.py +411 -0
- package/src/superlocalmemory/learning/reward.py +777 -0
- package/src/superlocalmemory/learning/reward_archive.py +210 -0
- package/src/superlocalmemory/learning/reward_boost.py +201 -0
- package/src/superlocalmemory/learning/reward_proxy.py +326 -0
- package/src/superlocalmemory/learning/shadow_test.py +524 -0
- package/src/superlocalmemory/learning/signal_worker.py +270 -0
- package/src/superlocalmemory/learning/signals.py +314 -0
- package/src/superlocalmemory/learning/trigram_index.py +547 -0
- package/src/superlocalmemory/mcp/server.py +5 -5
- package/src/superlocalmemory/mcp/tools_context.py +183 -0
- package/src/superlocalmemory/mcp/tools_core.py +92 -27
- package/src/superlocalmemory/parameterization/soft_prompt_generator.py +13 -0
- package/src/superlocalmemory/retrieval/engine.py +52 -0
- package/src/superlocalmemory/server/api.py +2 -2
- package/src/superlocalmemory/server/bandit_loops.py +140 -0
- package/src/superlocalmemory/server/middleware/__init__.py +11 -0
- package/src/superlocalmemory/server/middleware/security_headers.py +144 -0
- package/src/superlocalmemory/server/routes/backup.py +36 -13
- package/src/superlocalmemory/server/routes/behavioral.py +50 -19
- package/src/superlocalmemory/server/routes/brain.py +1234 -0
- package/src/superlocalmemory/server/routes/data_io.py +4 -4
- package/src/superlocalmemory/server/routes/events.py +2 -2
- package/src/superlocalmemory/server/routes/helpers.py +1 -1
- package/src/superlocalmemory/server/routes/learning.py +192 -7
- package/src/superlocalmemory/server/routes/memories.py +189 -1
- package/src/superlocalmemory/server/routes/prewarm.py +171 -0
- package/src/superlocalmemory/server/routes/profiles.py +3 -3
- package/src/superlocalmemory/server/routes/token.py +88 -0
- package/src/superlocalmemory/server/routes/ws.py +5 -5
- package/src/superlocalmemory/server/security_middleware.py +13 -7
- package/src/superlocalmemory/server/ui.py +2 -2
- package/src/superlocalmemory/server/unified_daemon.py +335 -3
- package/src/superlocalmemory/skills/slm-build-graph/SKILL.md +423 -0
- package/src/superlocalmemory/skills/slm-list-recent/SKILL.md +348 -0
- package/src/superlocalmemory/skills/slm-recall/SKILL.md +343 -0
- package/src/superlocalmemory/skills/slm-remember/SKILL.md +194 -0
- package/src/superlocalmemory/skills/slm-show-patterns/SKILL.md +224 -0
- package/src/superlocalmemory/skills/slm-status/SKILL.md +363 -0
- package/src/superlocalmemory/skills/slm-switch-profile/SKILL.md +442 -0
- package/src/superlocalmemory/storage/migration_runner.py +545 -0
- package/src/superlocalmemory/storage/migrations/M001_add_signal_features_columns.py +67 -0
- package/src/superlocalmemory/storage/migrations/M002_model_state_history.py +132 -0
- package/src/superlocalmemory/storage/migrations/M003_migration_log.py +38 -0
- package/src/superlocalmemory/storage/migrations/M004_cross_platform_sync_log.py +46 -0
- package/src/superlocalmemory/storage/migrations/M005_bandit_tables.py +75 -0
- package/src/superlocalmemory/storage/migrations/M006_action_outcomes_reward.py +75 -0
- package/src/superlocalmemory/storage/migrations/M007_pending_outcomes.py +63 -0
- package/src/superlocalmemory/storage/migrations/M009_model_lineage.py +54 -0
- package/src/superlocalmemory/storage/migrations/M010_evolution_config.py +75 -0
- package/src/superlocalmemory/storage/migrations/M011_archive_and_merge.py +87 -0
- package/src/superlocalmemory/storage/migrations/M012_shadow_observations.py +72 -0
- package/src/superlocalmemory/storage/migrations/M013_bi_temporal_columns.py +55 -0
- package/src/superlocalmemory/storage/migrations/__init__.py +81 -0
- package/src/superlocalmemory/storage/models.py +4 -0
- package/src/superlocalmemory/ui/css/brain.css +409 -0
- package/src/superlocalmemory/ui/css/legacy-dashboard.css +645 -0
- package/src/superlocalmemory/ui/index.html +459 -1345
- package/src/superlocalmemory/ui/js/brain.js +1321 -0
- package/src/superlocalmemory/ui/js/clusters.js +123 -4
- package/src/superlocalmemory/ui/js/init.js +48 -39
- package/src/superlocalmemory/ui/js/memories.js +88 -2
- package/src/superlocalmemory/ui/js/modal.js +71 -1
- package/src/superlocalmemory/ui/js/ng-shell.js +101 -88
- package/src/superlocalmemory/ui/js/trust-dashboard.js +168 -25
- package/src/superlocalmemory/ui/vendor/bootstrap-icons/bootstrap-icons.css +2018 -0
- package/src/superlocalmemory/ui/vendor/bootstrap-icons/fonts/bootstrap-icons.woff +0 -0
- package/src/superlocalmemory/ui/vendor/bootstrap-icons/fonts/bootstrap-icons.woff2 +0 -0
- package/src/superlocalmemory/ui/vendor/bootstrap.bundle.min.js +7 -0
- package/src/superlocalmemory/ui/vendor/bootstrap.min.css +6 -0
- package/src/superlocalmemory/ui/vendor/d3.v7.min.js +2 -0
- package/src/superlocalmemory/ui/vendor/graphology-library.min.js +2 -0
- package/src/superlocalmemory/ui/vendor/graphology.umd.min.js +2 -0
- package/src/superlocalmemory/ui/vendor/inter-ui/inter-variable.min.css +8 -0
- package/src/superlocalmemory/ui/vendor/inter-ui/variable/InterVariable-Italic.woff2 +0 -0
- package/src/superlocalmemory/ui/vendor/inter-ui/variable/InterVariable.woff2 +0 -0
- package/src/superlocalmemory/ui/vendor/sigma.min.js +1 -0
- package/src/superlocalmemory/ui/js/behavioral.js +0 -447
- package/src/superlocalmemory/ui/js/graph-core.js +0 -447
- package/src/superlocalmemory/ui/js/graph-interactions.js +0 -351
- package/src/superlocalmemory/ui/js/learning.js +0 -435
- package/src/superlocalmemory/ui/js/patterns.js +0 -93
- package/src/superlocalmemory.egg-info/PKG-INFO +0 -647
- package/src/superlocalmemory.egg-info/SOURCES.txt +0 -335
- package/src/superlocalmemory.egg-info/dependency_links.txt +0 -1
- package/src/superlocalmemory.egg-info/entry_points.txt +0 -2
- package/src/superlocalmemory.egg-info/requires.txt +0 -58
- 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.22 — 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.22"
|
|
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
|
+
)
|
|
@@ -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
|
-
|
|
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 (
|
|
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.22 — 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")
|