Big News: Socket raises $60M Series C at a $1B valuation to secure software supply chains for AI-driven development.Announcement
Sign In

superlocalmemory

Package Overview
Dependencies
Maintainers
1
Versions
172
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

superlocalmemory - npm Package Compare versions

Comparing version
3.6.8
to
3.6.9
+1
-1
package.json
{
"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": [

[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