superlocalmemory
Advanced tools
+1
-1
| { | ||
| "name": "superlocalmemory", | ||
| "version": "3.6.8", | ||
| "version": "3.6.9", | ||
| "description": "Information-geometric agent memory with mathematical guarantees. 4-channel retrieval, Fisher-Rao similarity, zero-LLM mode, EU AI Act compliant. Works with Claude, Cursor, Windsurf, and 17+ AI tools.", | ||
@@ -5,0 +5,0 @@ "keywords": [ |
+1
-1
| [project] | ||
| name = "superlocalmemory" | ||
| version = "3.6.8" | ||
| version = "3.6.9" | ||
| description = "Information-geometric agent memory with mathematical guarantees" | ||
@@ -5,0 +5,0 @@ readme = "README.md" |
| Metadata-Version: 2.4 | ||
| Name: superlocalmemory | ||
| Version: 3.6.8 | ||
| Version: 3.6.9 | ||
| Summary: Information-geometric agent memory with mathematical guarantees | ||
@@ -5,0 +5,0 @@ Author-email: Varun Pratap Bhardwaj <admin@superlocalmemory.com> |
@@ -1,3 +0,7 @@ | ||
| """SuperLocalMemory — information-geometric agent memory.""" | ||
| """SuperLocalMemory — information-geometric agent memory. | ||
| v3.6.9: all 7 retrieval layers at full quality (Hopfield@1000, entity_graph@100, | ||
| SA neighbor-cache fix, fast=True deprecated). See CHANGELOG.md. | ||
| """ | ||
| import os | ||
@@ -31,3 +35,3 @@ | ||
| __version__ = "3.6.8" | ||
| __version__ = "3.6.9" | ||
@@ -34,0 +38,0 @@ _REQUIRED_VERSIONS = { |
@@ -37,3 +37,6 @@ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar | ||
| _DEFAULT_PORT = 8765 # v3.4.3: unified daemon on 8765 (was 8767) | ||
| try: | ||
| _DEFAULT_PORT = int(os.environ.get("SLM_DAEMON_PORT", "") or 8765) | ||
| except ValueError: | ||
| _DEFAULT_PORT = 8765 | ||
| _LEGACY_PORT = 8767 # backward-compat redirect | ||
@@ -164,3 +167,9 @@ _DEFAULT_IDLE_TIMEOUT = 0 # v3.4.3: 24/7 default (was 1800) | ||
| import subprocess | ||
| cmd = [sys.executable, "-m", "superlocalmemory.server.unified_daemon", "--start"] | ||
| # v3.6.9 (#33): pass SLM_DAEMON_PORT as explicit --port= so the daemon | ||
| # binds the right port even when the env var reaches the subprocess. | ||
| _target_port = _DEFAULT_PORT | ||
| cmd = [ | ||
| sys.executable, "-m", "superlocalmemory.server.unified_daemon", | ||
| "--start", f"--port={_target_port}", | ||
| ] | ||
| log_dir = Path.home() / ".superlocalmemory" / "logs" | ||
@@ -192,3 +201,3 @@ log_dir.mkdir(parents=True, exist_ok=True) | ||
| _PID_FILE.write_text(str(proc.pid)) | ||
| _PORT_FILE.write_text(str(_DEFAULT_PORT)) | ||
| _PORT_FILE.write_text(str(_target_port)) | ||
@@ -243,2 +252,15 @@ return _wait_for_daemon(timeout=60) | ||
| # v3.6.9 (#36): TCP-level check catches a systemd-started daemon that | ||
| # has bound the port but hasn't written a PID file yet (e.g. different | ||
| # HOME for the service user vs. the SSH user). If the port is already | ||
| # bound, don't start a second daemon — wait for HTTP readiness instead. | ||
| try: | ||
| import socket as _socket | ||
| with _socket.socket(_socket.AF_INET, _socket.SOCK_STREAM) as _s: | ||
| _s.settimeout(1) | ||
| if _s.connect_ex(("127.0.0.1", _DEFAULT_PORT)) == 0: | ||
| return _wait_for_daemon(timeout=30) | ||
| except Exception: | ||
| pass | ||
| # Start unified daemon in background — delegated to helper so the | ||
@@ -245,0 +267,0 @@ # same logic can be reused by callers that already hold the lock. |
@@ -658,2 +658,21 @@ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar | ||
| # --------------------------------------------------------------------------- | ||
| # Health Config (v3.6.9 BUG-A) | ||
| # --------------------------------------------------------------------------- | ||
| @dataclass | ||
| class HealthConfig: | ||
| """Health-monitor tuning knobs. | ||
| All values have safe defaults so an empty ``"health": {}`` JSON section | ||
| silently inherits them. The ``global_rss_budget_mb`` default is computed | ||
| at runtime (40% of physical RAM, floor 2500 MB) so low-RAM boxes keep the | ||
| old behaviour while large machines are never accidentally throttled. | ||
| """ | ||
| global_rss_budget_mb: int = 0 # 0 = compute at runtime (40% RAM, floor 2500) | ||
| heartbeat_timeout_sec: int = 60 | ||
| health_check_interval_sec: int = 15 | ||
| enable_structured_logging: bool = True | ||
| # --------------------------------------------------------------------------- | ||
| # Master Config | ||
@@ -702,2 +721,3 @@ # --------------------------------------------------------------------------- | ||
| evolution: EvolutionConfig = field(default_factory=EvolutionConfig) | ||
| health: HealthConfig = field(default_factory=HealthConfig) | ||
@@ -794,2 +814,10 @@ # v3.4.3: Daemon configuration | ||
| # V3.6.9: Health monitor config (BUG-A — previously silently ignored) | ||
| hlth = data.get("health", {}) | ||
| if hlth: | ||
| config.health = HealthConfig(**{ | ||
| k: v for k, v in hlth.items() | ||
| if k in HealthConfig.__dataclass_fields__ | ||
| }) | ||
| # V3.4.65: Injection config (additive — defaults if missing from JSON) | ||
@@ -796,0 +824,0 @@ inj = data.get("injection", {}) or {} |
@@ -455,3 +455,5 @@ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar | ||
| profile_id=self._profile_id, content=content, | ||
| session_date=now[:10], metadata=metadata or {}, | ||
| session_date=now[:10], | ||
| session_id=(metadata or {}).get("session_id", ""), | ||
| metadata=metadata or {}, | ||
| ) | ||
@@ -524,5 +526,6 @@ self._db.store_memory(record) | ||
| V3.4.40 (2026-05-09): ``fast=True`` skips the SpreadingActivation | ||
| 5th channel for sub-second response. The other 4 channels still | ||
| run. Use when recall must complete before another tool call (e.g. | ||
| agent recall before WebSearch). | ||
| channel. Deprecated in v3.6.9 — SA now completes in ~36ms after the | ||
| neighbor-cache fix; fast=True is slower than fast=False and reduces | ||
| recall quality. The parameter is accepted for backward compatibility | ||
| but is silently treated as False. | ||
| """ | ||
@@ -532,2 +535,10 @@ self._require_full("recall") | ||
| if fast: | ||
| logger.warning( | ||
| "fast=True is deprecated (v3.6.9): SpreadingActivation now " | ||
| "completes in ~36ms; fast mode is slower and reduces quality. " | ||
| "Pass fast=False (the default) to silence this warning." | ||
| ) | ||
| fast = False | ||
| pid = profile_id or self._profile_id | ||
@@ -534,0 +545,0 @@ |
@@ -133,6 +133,9 @@ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar | ||
| ) | ||
| # Workers that are LOAD-BEARING for recall quality — never kill these | ||
| # first; prefer killing the reranker (gracefully degrades) or GC instead. | ||
| _EMBEDDING_IDENTIFIER = "superlocalmemory.core.embedding_worker" | ||
| def __init__( | ||
| self, | ||
| global_rss_budget_mb: int = 2500, | ||
| global_rss_budget_mb: int = 0, | ||
| heartbeat_timeout_sec: int = 60, | ||
@@ -142,2 +145,11 @@ check_interval_sec: int = 15, | ||
| ): | ||
| # Compute RAM-scaled default when 0 is passed (or when the caller | ||
| # explicitly passes 0 meaning "auto"). Floor at 2500 so low-RAM boxes | ||
| # keep the old conservative behaviour. | ||
| if global_rss_budget_mb <= 0: | ||
| if PSUTIL_AVAILABLE: | ||
| phys_mb = psutil.virtual_memory().total // (1024 * 1024) | ||
| global_rss_budget_mb = max(2500, int(phys_mb * 0.40)) | ||
| else: | ||
| global_rss_budget_mb = 8000 # safe fallback when psutil absent | ||
| self._budget_mb = global_rss_budget_mb | ||
@@ -212,3 +224,3 @@ self._heartbeat_timeout = heartbeat_timeout_sec | ||
| "rss_mb": round(rss_mb, 1), | ||
| "cmdline": cmdline[:80], | ||
| "cmdline": cmdline[:200], | ||
| }) | ||
@@ -231,8 +243,18 @@ except (psutil.NoSuchProcess, psutil.AccessDenied): | ||
| # RSS budget enforcement | ||
| # RSS budget enforcement — spare the embedding worker (load-bearing for | ||
| # recall quality). Kill the reranker first (degrades gracefully); only | ||
| # fall back to the embedder if it is the only worker remaining. | ||
| if total_rss_mb > self._budget_mb and slm_workers: | ||
| heaviest = max(slm_workers, key=lambda w: w["rss_mb"]) | ||
| non_embedder = [ | ||
| w for w in slm_workers | ||
| if self._EMBEDDING_IDENTIFIER not in w["cmdline"] | ||
| ] | ||
| candidate = ( | ||
| max(non_embedder, key=lambda w: w["rss_mb"]) | ||
| if non_embedder | ||
| else max(slm_workers, key=lambda w: w["rss_mb"]) | ||
| ) | ||
| logger.warning( | ||
| "RSS budget exceeded (%.0fMB > %dMB). Killing heaviest worker PID %d (%.0fMB)", | ||
| total_rss_mb, self._budget_mb, heaviest["pid"], heaviest["rss_mb"], | ||
| "RSS budget exceeded (%.0fMB > %dMB). Killing worker PID %d (%.0fMB)", | ||
| total_rss_mb, self._budget_mb, candidate["pid"], candidate["rss_mb"], | ||
| ) | ||
@@ -242,8 +264,9 @@ log_structured( | ||
| operation="rss_budget_kill", | ||
| killed_pid=heaviest["pid"], | ||
| killed_rss_mb=heaviest["rss_mb"], | ||
| killed_pid=candidate["pid"], | ||
| killed_rss_mb=candidate["rss_mb"], | ||
| total_rss_mb=round(total_rss_mb, 1), | ||
| spared_embedder=bool(non_embedder), | ||
| ) | ||
| try: | ||
| psutil.Process(heaviest["pid"]).terminate() | ||
| psutil.Process(candidate["pid"]).terminate() | ||
| except psutil.NoSuchProcess: | ||
@@ -250,0 +273,0 @@ pass |
@@ -19,5 +19,8 @@ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar | ||
| import asyncio | ||
| import datetime | ||
| import logging | ||
| import os | ||
| import sqlite3 | ||
| import uuid | ||
| from pathlib import Path | ||
@@ -217,3 +220,9 @@ from typing import Callable | ||
| try: | ||
| response = pool_recall(search_query, limit=max_results, fast=False) | ||
| # v3.6.9-audit: pool_recall uses blocking urllib under the hood | ||
| # (DaemonPoolProxy.recall → urllib.urlopen). Must run in a | ||
| # thread so the async MCP event loop is not stalled — same | ||
| # fix class as #34 mesh tools deadlock. | ||
| response = await asyncio.to_thread( | ||
| pool_recall, search_query, limit=max_results, fast=False, | ||
| ) | ||
| except (PoolError, Exception) as exc: | ||
@@ -354,2 +363,9 @@ logger.warning( | ||
| # v3.6.9 (#35): generate a stable session_id so clients can pass it | ||
| # to remember() and close_session() for proper session aggregation. | ||
| session_id = ( | ||
| f"slm-{datetime.datetime.now(datetime.timezone.utc):%Y%m%d}" | ||
| f"-{uuid.uuid4().hex[:8]}" | ||
| ) | ||
| # Register agent + emit event (v3.4.39: SLM_AGENT_ID env support) | ||
@@ -366,2 +382,3 @@ agent_id = _get_agent_id() | ||
| "success": True, | ||
| "session_id": session_id, | ||
| "context": context, | ||
@@ -437,4 +454,7 @@ "memories": memories[:max_results], | ||
| # Auto-store via engine | ||
| stored = auto.capture( | ||
| # Auto-store via engine. | ||
| # pool_store uses blocking urllib (DaemonPoolProxy) — run in | ||
| # thread so the MCP event loop stays unblocked (#34 class). | ||
| stored = await asyncio.to_thread( | ||
| auto.capture, | ||
| content, | ||
@@ -533,4 +553,20 @@ category=decision.category, | ||
| sid = session_id or getattr(engine, '_last_session_id', '') | ||
| # v3.6.9 (#35): _last_session_id was never assigned — fall back to | ||
| # querying the DB for the most recent session_id instead of silently | ||
| # returning summary_events_created: 0. | ||
| if not sid: | ||
| return {"success": False, "error": "No session_id provided"} | ||
| try: | ||
| db = getattr(engine, '_db', None) or getattr(engine, 'db', None) | ||
| if db and hasattr(db, 'execute'): | ||
| rows = db.execute( | ||
| "SELECT session_id FROM memories " | ||
| "WHERE session_id != '' ORDER BY created_at DESC LIMIT 1", | ||
| () | ||
| ) | ||
| if rows: | ||
| sid = str(rows[0][0]) | ||
| except Exception: | ||
| pass | ||
| if not sid: | ||
| return {"success": False, "error": "No session_id provided or found"} | ||
| count = engine.close_session(sid) | ||
@@ -537,0 +573,0 @@ return { |
@@ -125,5 +125,9 @@ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar | ||
| try: | ||
| import asyncio as _asyncio | ||
| from superlocalmemory.cli.daemon import daemon_request, is_daemon_running | ||
| if is_daemon_running(): | ||
| resp = daemon_request("POST", "/remember", { | ||
| # is_daemon_running() and daemon_request() both use blocking urllib | ||
| # against the same uvicorn server — run in threads so the MCP | ||
| # event loop stays unblocked (#34 class bug). | ||
| if await _asyncio.to_thread(is_daemon_running): | ||
| resp = await _asyncio.to_thread(daemon_request, "POST", "/remember", { | ||
| "content": content, "tags": tags, "metadata": meta, | ||
@@ -130,0 +134,0 @@ }) |
@@ -19,2 +19,3 @@ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar | ||
| import asyncio | ||
| import json | ||
@@ -145,9 +146,9 @@ import logging | ||
| _ensure_registered() | ||
| await asyncio.to_thread(_ensure_registered) | ||
| # Update summary | ||
| result = _mesh_request("POST", "/summary", { | ||
| "peer_id": _PEER_ID, | ||
| "summary": _SESSION_SUMMARY, | ||
| }) | ||
| result = await asyncio.to_thread( | ||
| _mesh_request, "POST", "/summary", | ||
| {"peer_id": _PEER_ID, "summary": _SESSION_SUMMARY}, | ||
| ) | ||
@@ -158,4 +159,4 @@ return { | ||
| "project_path": _PROJECT_PATH, | ||
| "registered": True, | ||
| "heartbeat_active": _HEARTBEAT_THREAD is not None, | ||
| "registered": _REGISTERED, | ||
| "heartbeat_active": _HEARTBEAT_THREAD is not None and _HEARTBEAT_THREAD.is_alive(), | ||
| "broker_response": result, | ||
@@ -171,4 +172,4 @@ } | ||
| """ | ||
| _ensure_registered() | ||
| result = _mesh_request("GET", "/peers") | ||
| await asyncio.to_thread(_ensure_registered) | ||
| result = await asyncio.to_thread(_mesh_request, "GET", "/peers") | ||
| peers = (result or {}).get("peers", []) | ||
@@ -192,8 +193,7 @@ return { | ||
| """ | ||
| _ensure_registered() | ||
| result = _mesh_request("POST", "/send", { | ||
| "from_peer": _PEER_ID, | ||
| "to_peer": to, | ||
| "content": message, | ||
| }) | ||
| await asyncio.to_thread(_ensure_registered) | ||
| result = await asyncio.to_thread( | ||
| _mesh_request, "POST", "/send", | ||
| {"from_peer": _PEER_ID, "to_peer": to, "content": message}, | ||
| ) | ||
| return result or {"error": "Failed to send message"} | ||
@@ -209,6 +209,6 @@ | ||
| """ | ||
| _ensure_registered() | ||
| await asyncio.to_thread(_ensure_registered) | ||
| project = _PROJECT_PATH or _detect_project_path() | ||
| messages = _mesh_request( | ||
| "GET", f"/inbox/{_PEER_ID}?project_path={project}", | ||
| messages = await asyncio.to_thread( | ||
| _mesh_request, "GET", f"/inbox/{_PEER_ID}?project_path={project}", | ||
| ) | ||
@@ -219,5 +219,6 @@ msg_list = (messages or {}).get("messages", []) | ||
| if unread_ids: | ||
| _mesh_request("POST", f"/inbox/{_PEER_ID}/read", { | ||
| "message_ids": unread_ids, | ||
| }) | ||
| await asyncio.to_thread( | ||
| _mesh_request, "POST", f"/inbox/{_PEER_ID}/read", | ||
| {"message_ids": unread_ids}, | ||
| ) | ||
| return { | ||
@@ -241,17 +242,16 @@ "messages": msg_list, | ||
| """ | ||
| _ensure_registered() | ||
| await asyncio.to_thread(_ensure_registered) | ||
| if action == "set" and key: | ||
| result = _mesh_request("POST", "/state", { | ||
| "key": key, | ||
| "value": value, | ||
| "set_by": _PEER_ID, | ||
| }) | ||
| result = await asyncio.to_thread( | ||
| _mesh_request, "POST", "/state", | ||
| {"key": key, "value": value, "set_by": _PEER_ID}, | ||
| ) | ||
| return result or {"error": "Failed to set state"} | ||
| if key: | ||
| result = _mesh_request("GET", f"/state/{key}") | ||
| result = await asyncio.to_thread(_mesh_request, "GET", f"/state/{key}") | ||
| return result or {"key": key, "value": None} | ||
| result = _mesh_request("GET", "/state") | ||
| result = await asyncio.to_thread(_mesh_request, "GET", "/state") | ||
| return result or {"state": {}} | ||
@@ -272,8 +272,7 @@ | ||
| """ | ||
| _ensure_registered() | ||
| result = _mesh_request("POST", "/lock", { | ||
| "file_path": file_path, | ||
| "action": action, | ||
| "locked_by": _PEER_ID, | ||
| }) | ||
| await asyncio.to_thread(_ensure_registered) | ||
| result = await asyncio.to_thread( | ||
| _mesh_request, "POST", "/lock", | ||
| {"file_path": file_path, "action": action, "locked_by": _PEER_ID}, | ||
| ) | ||
| return result or {"error": "Lock operation failed"} | ||
@@ -287,3 +286,3 @@ | ||
| """ | ||
| result = _mesh_request("GET", "/events") | ||
| result = await asyncio.to_thread(_mesh_request, "GET", "/events") | ||
| return result or {"events": []} | ||
@@ -297,6 +296,6 @@ | ||
| """ | ||
| result = _mesh_request("GET", "/status") | ||
| result = await asyncio.to_thread(_mesh_request, "GET", "/status") | ||
| if result: | ||
| result["my_peer_id"] = _PEER_ID | ||
| result["heartbeat_active"] = _HEARTBEAT_THREAD is not None | ||
| result["heartbeat_active"] = _HEARTBEAT_THREAD is not None and _HEARTBEAT_THREAD.is_alive() | ||
| return result or { | ||
@@ -303,0 +302,0 @@ "broker_up": False, |
@@ -173,4 +173,7 @@ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar | ||
| # Precompute out-degrees for fan effect | ||
| # Cache neighbor lookups and out-degrees across iterations — same node | ||
| # often survives multiple rounds via self-retention (delta=0.5); | ||
| # caching here cuts ~80% of SQL queries vs per-iteration re-query. | ||
| degree_cache: dict[str, int] = {} | ||
| neighbor_cache: dict[str, list] = {} | ||
@@ -185,4 +188,6 @@ # Steps 2-4, repeated T times | ||
| # Get neighbors from BOTH tables (Rule 13) | ||
| neighbors = self._get_unified_neighbors(node_id, profile_id) | ||
| # Get neighbors from BOTH tables (Rule 13) — cached per node | ||
| if node_id not in neighbor_cache: | ||
| neighbor_cache[node_id] = self._get_unified_neighbors(node_id, profile_id) | ||
| neighbors = neighbor_cache[node_id] | ||
@@ -189,0 +194,0 @@ # Out-degree for fan effect normalization |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
Found 5 instances in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
Found 4 instances in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
7777764
0.15%118888
0.13%48
2.13%