superlocalmemory
Advanced tools
| # 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 |
+3
-3
@@ -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 |
+1
-1
| [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" |
+3
-2
@@ -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> · MCP: add <code>slm_compress</code> to your config · 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> · MCP: add <code>slm_compress</code> to your config · 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
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
Found 2 instances
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
7986312
0.28%622
0.16%124937
0.31%418
0.24%46
-2.13%