🚀 Socket Launch Week Day 5:Introducing Repository Access Permissions and Custom Roles.Learn more
Sign In

superlocalmemory

Package Overview
Dependencies
Maintainers
1
Versions
183
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.16
to
3.6.17
+102
src/superlocalmemory/hooks/memory_protocol.py
# Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
# Licensed under AGPL-3.0-or-later - see LICENSE file
# Part of SuperLocalMemory V3 | https://qualixar.com | https://varunpratap.com
"""Shared utilities for marker-bounded writes into agent instruction files.
Adapters that inject SLM content into IDE/agent instruction files (e.g.
``.github/copilot-instructions.md``) use the constants and helpers here to
demarcate the SLM-managed section so user-curated content outside the
markers is preserved on every sync.
Marker contract
---------------
SLM wraps its content in a pair of HTML comments::
<!-- SLM-START -->
... managed content ...
<!-- SLM-END -->
``strip_slm_block`` removes all such pairs idempotently; adapters call it
before re-writing so a fresh block replaces the old one in place.
"""
from __future__ import annotations
import logging
logger = logging.getLogger(__name__)
#: Opening marker for the SLM-managed section.
SLM_MARKER_START = "<!-- SLM-START -->"
#: Closing marker for the SLM-managed section.
SLM_MARKER_END = "<!-- SLM-END -->"
def strip_slm_block(text: str) -> str:
"""Remove all SLM-managed sections from *text*.
Idempotent — returns *text* unchanged when no markers are present.
Strips every ``SLM-START``/``SLM-END`` pair to handle files that
accumulated duplicates from a previous bug or a competing writer.
If a ``SLM-START`` marker has no matching ``SLM-END``, the file is
returned unchanged to avoid eating user content; the caller should
treat this as an orphaned-marker error and skip the write.
"""
out = text
while True:
start_idx = out.find(SLM_MARKER_START)
if start_idx == -1:
return out
end_idx = out.find(SLM_MARKER_END, start_idx)
if end_idx == -1:
logger.warning(
"memory_protocol: %s found but %s missing; leaving file unchanged",
SLM_MARKER_START,
SLM_MARKER_END,
)
return text
cut_end = end_idx + len(SLM_MARKER_END)
if cut_end < len(out) and out[cut_end] == "\n":
cut_end += 1
# Pull back up to two leading newlines added as a boundary separator.
cut_start = start_idx
while cut_start > 0 and out[cut_start - 1] == "\n":
cut_start -= 1
if start_idx - cut_start >= 2:
break
out = out[:cut_start] + out[cut_end:]
def memory_protocol_markdown() -> str:
"""Return the agent-facing Markdown memory protocol block.
Embedded verbatim into Markdown instruction files such as
``.github/copilot-instructions.md``. Trailing newline included so
callers can concatenate without worrying about boundary whitespace.
"""
return (
"## Memory protocol\n"
"SLM tools are available via the `slm-hub` MCP gateway. Use them to "
"make this brain context grow across sessions.\n\n"
"- **At the start of work on an unfamiliar area**, call "
"`hub__call_tool` with `tool=\"slm__recall\"` and "
"`arguments={\"query\": \"<topic>\"}` to surface prior decisions "
"and patterns.\n"
"- **At the end of a substantial task** (a fix, a decision, a "
"non-trivial change, a session conclusion), call `hub__call_tool` "
"with `tool=\"slm__remember\"` and `arguments={\"content\": "
"\"<one-paragraph summary of what was decided / changed / "
"learned>\", \"tags\": \"<comma-separated kebab-case keywords>\"}`.\n"
"- A \"substantial task\" is anything you would write a commit "
"message or handoff note about — not every tool call.\n"
)
__all__ = (
"SLM_MARKER_START",
"SLM_MARKER_END",
"strip_slm_block",
"memory_protocol_markdown",
)
+1
-1
{
"name": "superlocalmemory",
"version": "3.6.16",
"version": "3.6.17",
"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": [

@@ -44,2 +44,2 @@ ---

SuperLocalMemory v3.6.16 · Qualixar · AGPL-3.0-or-later
SuperLocalMemory v3.6.17 · Qualixar · AGPL-3.0-or-later

@@ -38,2 +38,2 @@ ---

SuperLocalMemory v3.6.16 · Qualixar · AGPL-3.0-or-later
SuperLocalMemory v3.6.17 · Qualixar · AGPL-3.0-or-later

@@ -22,2 +22,2 @@ ---

SuperLocalMemory v3.6.16 · Qualixar · AGPL-3.0-or-later
SuperLocalMemory v3.6.17 · Qualixar · AGPL-3.0-or-later

@@ -16,2 +16,2 @@ ---

SuperLocalMemory v3.6.16 · Qualixar · AGPL-3.0-or-later
SuperLocalMemory v3.6.17 · Qualixar · AGPL-3.0-or-later

@@ -16,2 +16,2 @@ ---

SuperLocalMemory v3.6.16 · Qualixar · AGPL-3.0-or-later
SuperLocalMemory v3.6.17 · Qualixar · AGPL-3.0-or-later

@@ -15,2 +15,2 @@ ---

SuperLocalMemory v3.6.16 · Qualixar · AGPL-3.0-or-later
SuperLocalMemory v3.6.17 · Qualixar · AGPL-3.0-or-later
{
"version": "3.6.16",
"version": "3.6.17",
"pluginName": "superlocalmemory",

@@ -4,0 +4,0 @@ "displayName": "SuperLocalMemory",

@@ -1,1 +0,1 @@

superlocalmemory==3.6.16
superlocalmemory==3.6.17

@@ -91,2 +91,2 @@ # SuperLocalMemory — Agent Rules

SuperLocalMemory v3.6.16 · Qualixar · AGPL-3.0-or-later
SuperLocalMemory v3.6.17 · Qualixar · AGPL-3.0-or-later

@@ -140,2 +140,2 @@ ---

SuperLocalMemory v3.6.16 · Qualixar · AGPL-3.0-or-later
SuperLocalMemory v3.6.17 · Qualixar · AGPL-3.0-or-later

@@ -143,2 +143,2 @@ ---

SuperLocalMemory v3.6.16 · Qualixar · AGPL-3.0-or-later
SuperLocalMemory v3.6.17 · Qualixar · AGPL-3.0-or-later

@@ -300,2 +300,2 @@ ---

SuperLocalMemory v3.6.16 · Qualixar · AGPL-3.0-or-later
SuperLocalMemory v3.6.17 · Qualixar · AGPL-3.0-or-later

@@ -204,2 +204,2 @@ ---

*SuperLocalMemory v3.6.16 · Qualixar · AGPL-3.0-or-later*
*SuperLocalMemory v3.6.17 · Qualixar · AGPL-3.0-or-later*

@@ -194,2 +194,2 @@ ---

*SuperLocalMemory v3.6.16 · Qualixar · AGPL-3.0-or-later*
*SuperLocalMemory v3.6.17 · Qualixar · AGPL-3.0-or-later*

@@ -207,2 +207,2 @@ ---

*SuperLocalMemory v3.6.16 · Qualixar · AGPL-3.0-or-later*
*SuperLocalMemory v3.6.17 · Qualixar · AGPL-3.0-or-later*

@@ -149,2 +149,2 @@ ---

SuperLocalMemory v3.6.16 · Qualixar · AGPL-3.0-or-later
SuperLocalMemory v3.6.17 · Qualixar · AGPL-3.0-or-later

@@ -19,3 +19,3 @@ {

"repository": "https://github.com/qualixar/superlocalmemory",
"version": "3.6.16"
"version": "3.6.17"
}

@@ -44,2 +44,2 @@ ---

SuperLocalMemory v3.6.16 · Qualixar · AGPL-3.0-or-later
SuperLocalMemory v3.6.17 · Qualixar · AGPL-3.0-or-later

@@ -38,2 +38,2 @@ ---

SuperLocalMemory v3.6.16 · Qualixar · AGPL-3.0-or-later
SuperLocalMemory v3.6.17 · Qualixar · AGPL-3.0-or-later

@@ -1,2 +0,2 @@

<!-- BEGIN SuperLocalMemory v3.6.16 -->
<!-- BEGIN SuperLocalMemory v3.6.17 -->

@@ -42,4 +42,4 @@ ## SuperLocalMemory (SLM) — Agent Rules

<!-- END SuperLocalMemory v3.6.16 -->
<!-- END SuperLocalMemory v3.6.17 -->
SuperLocalMemory v3.6.16 · Qualixar · AGPL-3.0-or-later
SuperLocalMemory v3.6.17 · Qualixar · AGPL-3.0-or-later

@@ -1,1 +0,1 @@

superlocalmemory==3.6.16
superlocalmemory==3.6.17

@@ -140,2 +140,2 @@ ---

SuperLocalMemory v3.6.16 · Qualixar · AGPL-3.0-or-later
SuperLocalMemory v3.6.17 · Qualixar · AGPL-3.0-or-later

@@ -143,2 +143,2 @@ ---

SuperLocalMemory v3.6.16 · Qualixar · AGPL-3.0-or-later
SuperLocalMemory v3.6.17 · Qualixar · AGPL-3.0-or-later

@@ -300,2 +300,2 @@ ---

SuperLocalMemory v3.6.16 · Qualixar · AGPL-3.0-or-later
SuperLocalMemory v3.6.17 · Qualixar · AGPL-3.0-or-later

@@ -204,2 +204,2 @@ ---

*SuperLocalMemory v3.6.16 · Qualixar · AGPL-3.0-or-later*
*SuperLocalMemory v3.6.17 · Qualixar · AGPL-3.0-or-later*

@@ -194,2 +194,2 @@ ---

*SuperLocalMemory v3.6.16 · Qualixar · AGPL-3.0-or-later*
*SuperLocalMemory v3.6.17 · Qualixar · AGPL-3.0-or-later*

@@ -207,2 +207,2 @@ ---

*SuperLocalMemory v3.6.16 · Qualixar · AGPL-3.0-or-later*
*SuperLocalMemory v3.6.17 · Qualixar · AGPL-3.0-or-later*

@@ -149,2 +149,2 @@ ---

SuperLocalMemory v3.6.16 · Qualixar · AGPL-3.0-or-later
SuperLocalMemory v3.6.17 · Qualixar · AGPL-3.0-or-later
[project]
name = "superlocalmemory"
version = "3.6.16"
version = "3.6.17"
description = "Information-geometric agent memory with mathematical guarantees"

@@ -5,0 +5,0 @@ readme = "README.md"

@@ -5,6 +5,6 @@ <p align="center">

<h1 align="center">SuperLocalMemory V3.6.16</h1>
<h1 align="center">SuperLocalMemory V3.6.17</h1>
<p align="center"><strong>Cache. Compress. Remember. Three surfaces — proxy, MCP tools, or skill. Every setup covered.</strong><br/>
<em>To the best of our knowledge, the only zero-cloud agent memory that beats Mem0's zero-LLM score on LoCoMo. Mode A: 74.8% vs Mem0 64.2% — no GPU, no API key, on CPU.</em></p>
<p align="center"><code>v3.6.16</code> — <strong>Plugin-native. Profile-aware. Distributed-ready.</strong><br/>
<p align="center"><code>v3.6.17</code> — <strong>Plugin-native. Profile-aware. Distributed-ready.</strong><br/>
Proxy: <code>slm wrap claude</code> &nbsp;·&nbsp; MCP: add <code>slm_compress</code> to your config &nbsp;·&nbsp; Skill: zero-config</p>

@@ -311,2 +311,3 @@ <p align="center"><strong>3 published research papers</strong> (arXiv preprints + Zenodo-archived) · <a href="https://arxiv.org/abs/2603.02240">arXiv:2603.02240</a> · <a href="https://arxiv.org/abs/2603.14588">arXiv:2603.14588</a> · <a href="https://arxiv.org/abs/2604.04514">arXiv:2604.04514</a></p>

|---|---|---|
| **v3.6.17** | Community | 8 contributor PRs (observability events, marker-bounded adapter writes, daemon port discovery, anthropic `api_base`, OpenMP workers, atomic-write rehash, `_jl` sentinel, LFS pointer); dashboard-feedback fix (#53/#59); env-tunable SQLite knobs + idle backoff; remote LLM test-probe (#40) |
| **v3.6.16** | Docs | Corrected Claude Code plugin install — adds the required `/plugin marketplace add` step; clarifies plugin vs pip/npm delivery |

@@ -313,0 +314,0 @@ | **v3.6.15** | Multi-scope | **Opt-in [shared memory](docs/shared-memory.md)** (personal/shared/global, off by default), default-deny scope at every read path, recall scope-race fix, contributor PRs #42/#43/#44, fixes #46–#49 |

@@ -33,3 +33,3 @@ #!/usr/bin/env node

// ---------------------------------------------------------------------------
const VERSION = '3.6.16';
const VERSION = '3.6.17';
const MANIFEST_REL = 'plugin-src/manifest.json';

@@ -36,0 +36,0 @@ const GENERATED_BANNER = `# _GENERATED — DO NOT HAND-EDIT

Metadata-Version: 2.4
Name: superlocalmemory
Version: 3.6.16
Version: 3.6.17
Summary: Information-geometric agent memory with mathematical guarantees

@@ -99,6 +99,6 @@ Author-email: Varun Pratap Bhardwaj <admin@superlocalmemory.com>

<h1 align="center">SuperLocalMemory V3.6.16</h1>
<h1 align="center">SuperLocalMemory V3.6.17</h1>
<p align="center"><strong>Cache. Compress. Remember. Three surfaces — proxy, MCP tools, or skill. Every setup covered.</strong><br/>
<em>To the best of our knowledge, the only zero-cloud agent memory that beats Mem0's zero-LLM score on LoCoMo. Mode A: 74.8% vs Mem0 64.2% — no GPU, no API key, on CPU.</em></p>
<p align="center"><code>v3.6.16</code> — <strong>Plugin-native. Profile-aware. Distributed-ready.</strong><br/>
<p align="center"><code>v3.6.17</code> — <strong>Plugin-native. Profile-aware. Distributed-ready.</strong><br/>
Proxy: <code>slm wrap claude</code> &nbsp;·&nbsp; MCP: add <code>slm_compress</code> to your config &nbsp;·&nbsp; Skill: zero-config</p>

@@ -405,2 +405,3 @@ <p align="center"><strong>3 published research papers</strong> (arXiv preprints + Zenodo-archived) · <a href="https://arxiv.org/abs/2603.02240">arXiv:2603.02240</a> · <a href="https://arxiv.org/abs/2603.14588">arXiv:2603.14588</a> · <a href="https://arxiv.org/abs/2604.04514">arXiv:2604.04514</a></p>

|---|---|---|
| **v3.6.17** | Community | 8 contributor PRs (observability events, marker-bounded adapter writes, daemon port discovery, anthropic `api_base`, OpenMP workers, atomic-write rehash, `_jl` sentinel, LFS pointer); dashboard-feedback fix (#53/#59); env-tunable SQLite knobs + idle backoff; remote LLM test-probe (#40) |
| **v3.6.16** | Docs | Corrected Claude Code plugin install — adds the required `/plugin marketplace add` step; clarifies plugin vs pip/npm delivery |

@@ -407,0 +408,0 @@ | **v3.6.15** | Multi-scope | **Opt-in [shared memory](docs/shared-memory.md)** (personal/shared/global, off by default), default-deny scope at every read path, recall scope-race fix, contributor PRs #42/#43/#44, fixes #46–#49 |

@@ -180,2 +180,3 @@ AUTHORS.md

src/superlocalmemory/hooks/ide_connector.py
src/superlocalmemory/hooks/memory_protocol.py
src/superlocalmemory/hooks/portable_kit.py

@@ -182,0 +183,0 @@ src/superlocalmemory/hooks/post_tool_async_hook.py

@@ -35,3 +35,3 @@ """SuperLocalMemory — information-geometric agent memory.

__version__ = "3.6.16"
__version__ = "3.6.17"

@@ -38,0 +38,0 @@ _REQUIRED_VERSIONS = {

@@ -476,2 +476,7 @@ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar

"ORT_DISABLE_COREML": "1",
# Restore parallel OpenMP. The package caps OMP_NUM_THREADS
# globally to avoid a torch+lightgbm libomp SIGSEGV in the
# main process. This worker loads torch but never lightgbm,
# so there is no collision risk and full parallelism is safe.
"OMP_NUM_THREADS": str(os.cpu_count() or 4),
}

@@ -478,0 +483,0 @@ from superlocalmemory.core.platform_utils import popen_platform_kwargs

@@ -114,2 +114,4 @@ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar

var_arr = np.asarray(fisher_variance, dtype=np.float64)
if var_arr.size == 0:
return CouplingState()

@@ -207,2 +209,4 @@ # Step 1: Fisher confidence from variance

var_arr = np.asarray(fisher_variance, dtype=np.float64)
if var_arr.size == 0:
return self._base_temp
avg_var = float(np.mean(np.clip(var_arr, 1e-8, None)))

@@ -209,0 +213,0 @@ fisher_conf = min(1.0, 1.0 / (1.0 + avg_var) + min(access_count * 0.02, 0.2))

@@ -220,5 +220,12 @@ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar

if prev == new_hash and resolved_path.exists():
# Durable skip — no write, no new sync-log row (the prior row still
# reflects on-disk truth).
return WriteResult(wrote=False, bytes_written=0, content_sha256=new_hash)
# Durable skip only if the on-disk content also matches the new hash.
# The sync-log row alone is not authoritative: the file may have been
# mutated out-of-band (e.g. ``git restore``, manual edit) since the
# last sync. Re-hash the file and re-write if it diverges.
try:
disk_hash = hashlib.sha256(resolved_path.read_bytes()).hexdigest()
except OSError:
disk_hash = None
if disk_hash == new_hash:
return WriteResult(wrote=False, bytes_written=0, content_sha256=new_hash)

@@ -225,0 +232,0 @@ resolved_path.parent.mkdir(parents=True, exist_ok=True)

@@ -11,5 +11,11 @@ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar

v3.4.23 fix: the SLM-managed content is wrapped in
``<!-- SLM-START -->`` / ``<!-- SLM-END -->`` markers and merged into the
host file rather than overwriting it. ``.github/copilot-instructions.md``
is typically a curated, project-specific document; destructive rewrites
deleted the user's prose.
Hard rules covered here:
- A1 / A2 / A3 / A7: via ``adapter_base.atomic_write``.
- A4: soft 2 KB + hard 4 KB cap enforcement.
- A4: soft 2 KB + hard 4 KB cap enforcement on the SLM section.
"""

@@ -43,2 +49,8 @@

)
from superlocalmemory.hooks.memory_protocol import (
SLM_MARKER_END,
SLM_MARKER_START,
memory_protocol_markdown,
strip_slm_block as _strip_existing_block,
)

@@ -57,3 +69,3 @@ logger = logging.getLogger(__name__)

"- Do not modify files under `.slm/`\n"
"- Do not commit `*.slm-cache.db`\n"
"- Do not commit `*.slm-cache.db`\n\n"
)

@@ -63,11 +75,28 @@

def render_copilot(payload: ContextPayload) -> bytes:
return _BODY_TEMPLATE.format(
# Two-stage assembly: format the dynamic header (which contains {}
# placeholders), then concatenate the static memory-protocol block
# verbatim. The memory-protocol block legitimately contains literal
# braces (JSON-shaped argument examples for the agent) which must not
# be interpreted as format fields.
header = _BODY_TEMPLATE.format(
version=payload.version,
topics=format_topics(payload),
entities=format_entities(payload),
).encode("utf-8")
)
return (header + memory_protocol_markdown()).encode("utf-8")
def _wrap_managed(rendered: bytes) -> str:
"""Wrap rendered SLM content in ``<!-- SLM-START -->`` markers."""
return (
f"{SLM_MARKER_START}\n"
"<!-- Managed by SuperLocalMemory. Edits between SLM-START and "
"SLM-END will be overwritten. -->\n\n"
f"{rendered.decode('utf-8')}\n"
f"{SLM_MARKER_END}\n"
)
class CopilotAdapter:
"""Project-scope Copilot adapter."""
"""Project-scope Copilot adapter (marker-bounded merge)."""

@@ -131,4 +160,35 @@ def __init__(

# Marker-bounded merge — preserve any user-curated content in the
# host file. Strip any prior SLM block(s) and re-append a fresh one.
existing = ""
if resolved.exists():
try:
existing = resolved.read_text(encoding="utf-8")
except OSError as exc:
logger.warning(
"copilot: cannot read %s: %s", resolved, exc,
)
return False
# Orphaned start marker — refuse to write rather than corrupt.
if (SLM_MARKER_START in existing
and SLM_MARKER_END not in existing):
logger.warning(
"copilot: %s present but %s missing in %s; refusing to write",
SLM_MARKER_START, SLM_MARKER_END, resolved,
)
return False
stripped = _strip_existing_block(existing)
section = _wrap_managed(rendered)
if stripped:
if not stripped.endswith("\n"):
stripped += "\n"
# One blank line between user content and the managed section.
new_content = stripped + "\n" + section
else:
new_content = section
result: WriteResult = atomic_write(
resolved, rendered,
resolved, new_content.encode("utf-8"),
adapter_name=self.name,

@@ -145,7 +205,16 @@ profile_id=self._profile_id,

return
# Marker-bounded strip — never delete the host file (user-owned).
if resolved.exists():
try:
resolved.unlink()
except OSError: # pragma: no cover
pass
existing = resolved.read_text(encoding="utf-8")
except OSError:
existing = ""
stripped = _strip_existing_block(existing)
if stripped != existing:
try:
resolved.write_text(stripped, encoding="utf-8")
except OSError as exc: # pragma: no cover
logger.warning(
"copilot: failed to strip on disable: %s", exc,
)
record_disable(

@@ -152,0 +221,0 @@ resolved,

@@ -39,5 +39,30 @@ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar

_DAEMON_URL = "http://127.0.0.1:8765"
_DEFAULT_DAEMON_PORT = 8765
def _daemon_url() -> str:
"""Resolve the daemon base URL, preferring the per-user port file.
On a shared host each user runs their own daemon bound to a different
port; the active port is written to ``~/.superlocalmemory/daemon.port``
at startup. Reading it here keeps lifecycle hooks pointed at the
caller's own daemon instead of a hard-coded ``8765`` that may belong to
another user's instance. Falls back to the default port when the file is
absent or unreadable. Stdlib only — no SLM imports in the hot path.
"""
port = _DEFAULT_DAEMON_PORT
try:
port_file = os.path.join(
os.path.expanduser("~"), ".superlocalmemory", "daemon.port",
)
with open(port_file) as fh:
port = int(fh.read().strip())
except Exception:
pass
return f"http://127.0.0.1:{port}"
_DAEMON_URL = _daemon_url()
def _daemon_post(path: str, body: dict, timeout: float = 3.0) -> bool:

@@ -44,0 +69,0 @@ """POST to SLM daemon via stdlib urllib. Returns True on success.

@@ -33,3 +33,21 @@ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar

_DEFAULT_DAEMON_PORT = 8765
def _port_file_url() -> str:
"""Loopback daemon URL from the per-user port file (default 8765).
On a shared host each user runs their own daemon on a different port,
written to ``~/.superlocalmemory/daemon.port`` at startup. Falling back
to this instead of a hard-coded ``8765`` keeps the hook pointed at the
caller's own daemon. Stdlib only.
"""
port = _DEFAULT_DAEMON_PORT
try:
port = int((Path.home() / ".superlocalmemory" / "daemon.port").read_text().strip())
except Exception:
pass
return f"http://127.0.0.1:{port}"
def _sanitised_daemon_url() -> str:

@@ -42,7 +60,7 @@ """Return the configured daemon URL only if it's loopback-scoped.

header. We refuse any non-loopback URL and fall back to the local
daemon.
daemon (resolved via the per-user port file, not a hard-coded port).
"""
raw = os.environ.get("SLM_HOOK_DAEMON_URL", "").strip()
if not raw:
return "http://127.0.0.1:8765"
return _port_file_url()
try:

@@ -52,8 +70,8 @@ from urllib.parse import urlparse

except Exception: # pragma: no cover — urllib always importable
return "http://127.0.0.1:8765"
return _port_file_url()
if parsed.scheme not in ("http", "https"):
return "http://127.0.0.1:8765"
return _port_file_url()
host = (parsed.hostname or "").lower()
if host not in _ALLOWED_DAEMON_HOSTS:
return "http://127.0.0.1:8765"
return _port_file_url()
# Preserve the scheme + port (user may bind daemon on a non-default port).

@@ -60,0 +78,0 @@ port = f":{parsed.port}" if parsed.port else ""

@@ -35,2 +35,6 @@ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar

"memory.recalled", # Memory retrieved by an agent
"memory.observed", # /observe accepted content into the debounce buffer
"memory.captured", # AutoCapture matched a buffered observation
"memory.dropped", # AutoCapture rejected a buffered observation
"memory.queued", # /remember accepted content into pending.db (async)
"graph.updated", # Knowledge graph rebuilt

@@ -37,0 +41,0 @@ "pattern.learned", # New pattern detected

@@ -44,2 +44,14 @@ #!/usr/bin/env python3

# Dashboard UI vocabulary -> (signal_type, signal_value). The dashboard speaks
# thumbs_up/thumbs_down/pin (explicit) and dwell_positive/dwell_negative
# (derived from modal dwell time). Unknown types fall back to a neutral
# user_correction signal rather than being dropped.
_DASHBOARD_SIGNAL_MAP: Dict[str, tuple[str, float]] = {
"thumbs_up": ("user_positive", 1.0),
"thumbs_down": ("user_negative", 0.0),
"pin": ("user_pin", 1.0),
"dwell_positive": ("dwell_positive", 0.6),
"dwell_negative": ("dwell_negative", 0.2),
}
_CREATE_TABLE = """

@@ -224,2 +236,49 @@ CREATE TABLE IF NOT EXISTS learning_feedback (

# ------------------------------------------------------------------
# Public API: record dashboard feedback
# ------------------------------------------------------------------
def record_dashboard_feedback(
self,
memory_id: str,
query: str = "",
feedback_type: str = "",
profile_id: str = "default",
) -> Optional[int]:
"""Record an explicit feedback signal raised from the dashboard UI.
Maps the dashboard's vocabulary (``thumbs_up``/``thumbs_down``/``pin``
and the dwell-derived ``dwell_positive``/``dwell_negative``) onto a
stored ``(signal_type, signal_value)`` pair. ``memory_id`` is the fact
id; the raw ``query`` is hashed and never stored. Returns the inserted
row id, or ``None`` on missing ``memory_id``.
This method restores the dashboard feedback path: the HTTP routes in
``server/routes/learning.py`` called it before it existed, so every
thumbs/pin/dwell write raised ``AttributeError`` (issues #53/#59).
"""
if not memory_id:
return None
signal_type, value = _DASHBOARD_SIGNAL_MAP.get(
feedback_type, ("user_correction", 0.5),
)
qhash = _hash_query(query) if query else None
now = _utcnow_iso()
with self._lock:
conn = self._connect()
try:
cursor = conn.execute(
"INSERT INTO learning_feedback "
"(profile_id, fact_id, signal_type, signal_value, "
"query_hash, created_at, metadata) "
"VALUES (?, ?, ?, ?, ?, ?, ?)",
(profile_id or "default", str(memory_id), signal_type,
value, qhash, now, None),
)
conn.commit()
return cursor.lastrowid
finally:
conn.close()
# ------------------------------------------------------------------
# Public API: read feedback

@@ -226,0 +285,0 @@ # ------------------------------------------------------------------

@@ -210,7 +210,15 @@ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar

next_reap = _time.monotonic() + _REAP_INTERVAL_S
while not _stop_event.wait(interval_s):
# Adaptive idle back-off: poll at interval_s under load, but relax the wait
# (doubling, capped) when the queue drains empty so an idle daemon stops
# contending on the shared SQLite file every 0.25s (issue #53). Snaps back
# to interval_s the instant there is work again.
_idle_cap = max(interval_s, 2.0)
cur_wait = interval_s
while not _stop_event.wait(cur_wait):
try:
_drain_once(memory_db_path)
drained = _drain_once(memory_db_path)
except Exception as exc: # pragma: no cover — defensive
logger.warning("outcome_queue drain crashed: %s", exc)
drained = 0
cur_wait = interval_s if drained else min(cur_wait * 2.0, _idle_cap)
# Periodic reaper for CLI/dashboard outcomes that no Stop hook

@@ -217,0 +225,0 @@ # will ever finalize. Runs OFF the drain path so a busy queue

@@ -298,3 +298,10 @@ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar

payload["system"] = system
return _ANTHROPIC_URL, headers, payload
# Respect custom base_url (e.g. Anthropic-compatible proxy).
# Append /v1/messages to the root URL, mirroring how _build_openai
# handles api_base. Falls back to the official Anthropic endpoint.
url = (
self._base_url.rstrip("/") + "/v1/messages"
if self._base_url else _ANTHROPIC_URL
)
return url, headers, payload

@@ -301,0 +308,0 @@ def _build_azure(

@@ -200,2 +200,7 @@ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar

"ORT_DISABLE_COREML": "1",
# Restore parallel OpenMP. The package caps OMP_NUM_THREADS
# globally to avoid a torch+lightgbm libomp SIGSEGV in the
# main process. This worker loads torch but never lightgbm,
# so there is no collision risk and full parallelism is safe.
"OMP_NUM_THREADS": str(os.cpu_count() or 4),
}

@@ -202,0 +207,0 @@ from superlocalmemory.core.platform_utils import popen_platform_kwargs

@@ -328,2 +328,3 @@ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar

memory_id=str(memory_id), query=query, feedback_type=feedback_type,
profile_id=get_active_profile() or "default",
)

@@ -373,2 +374,3 @@

memory_id=str(memory_id), query=query, feedback_type=feedback_type,
profile_id=get_active_profile() or "default",
)

@@ -375,0 +377,0 @@

@@ -15,3 +15,3 @@ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar

import json, logging, sqlite3, threading, time
import json, logging, os, sqlite3, threading, time
from contextlib import contextmanager

@@ -31,6 +31,12 @@ from pathlib import Path

def _jl(raw: Any, default: Any = None) -> Any:
"""JSON-load a value, returning *default* on None/empty."""
_MISSING = object()
def _jl(raw: Any, default: Any = _MISSING) -> Any:
"""JSON-load a value, returning *default* on None/empty.
_jl(raw) -> [] when raw is None/empty (list fields)
_jl(raw, None) -> None when raw is None/empty (optional fields)
"""
if raw is None or raw == "":
return default if default is not None else []
return [] if default is _MISSING else default
return json.loads(raw)

@@ -43,7 +49,28 @@

_BUSY_TIMEOUT_MS = 10_000 # 10 seconds — wait for other writers
_MAX_RETRIES = 5 # retry on transient SQLITE_BUSY
_RETRY_BASE_DELAY = 0.1 # seconds — exponential backoff base
def _env_int(name: str, default: int) -> int:
"""Read a positive int from the environment, falling back on bad/absent."""
try:
val = int(os.environ.get(name, "").strip())
return val if val > 0 else default
except (ValueError, AttributeError):
return default
def _env_float(name: str, default: float) -> float:
"""Read a positive float from the environment, falling back on bad/absent."""
try:
val = float(os.environ.get(name, "").strip())
return val if val > 0 else default
except (ValueError, AttributeError):
return default
# SQLite endurance tuning. Defaults preserve prior hard-coded behaviour exactly;
# operators on slow/contended I/O can raise them via env (issue #53) without a
# code change. Unset env => byte-identical to the previous constants.
_BUSY_TIMEOUT_MS = _env_int("SLM_DB_BUSY_TIMEOUT_MS", 10_000) # wait for writers
_MAX_RETRIES = _env_int("SLM_DB_MAX_RETRIES", 5) # retry on SQLITE_BUSY
_RETRY_BASE_DELAY = _env_float("SLM_DB_RETRY_BASE_DELAY", 0.1) # backoff base (s)
def _scope_where(

@@ -50,0 +77,0 @@ profile_id: str,

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display