superlocalmemory 3.6.15 → 3.6.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -0
- package/README.md +21 -6
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/CLAUDE.md +3 -3
- package/plugin/agents/slm-memory-advisor.md +1 -1
- package/plugin/agents/slm-optimize-advisor.md +1 -1
- package/plugin/requirements.txt +1 -1
- package/plugin/skills/slm-cache/SKILL.md +1 -1
- package/plugin/skills/slm-compress/SKILL.md +1 -1
- package/plugin/skills/slm-graph/SKILL.md +1 -1
- package/plugin/skills/slm-recall/SKILL.md +1 -1
- package/plugin/skills/slm-remember/SKILL.md +1 -1
- package/plugin/skills/slm-session/SKILL.md +1 -1
- package/plugin/skills/slm-status/SKILL.md +1 -1
- package/plugin-src/agents/slm-memory-advisor.md +1 -1
- package/plugin-src/agents/slm-optimize-advisor.md +1 -1
- package/plugin-src/commands/slm-optimize.md +1 -1
- package/plugin-src/commands/slm-recall.md +1 -1
- package/plugin-src/commands/slm-remember.md +1 -1
- package/plugin-src/commands/slm-status.md +1 -1
- package/plugin-src/manifest.json +1 -1
- package/plugin-src/requirements.txt +1 -1
- package/plugin-src/rules/AGENTS.md +1 -1
- package/plugin-src/rules/CLAUDE.md.fragment +3 -3
- package/plugin-src/skills/slm-cache/SKILL.md +1 -1
- package/plugin-src/skills/slm-compress/SKILL.md +1 -1
- package/plugin-src/skills/slm-graph/SKILL.md +1 -1
- package/plugin-src/skills/slm-recall/SKILL.md +1 -1
- package/plugin-src/skills/slm-remember/SKILL.md +1 -1
- package/plugin-src/skills/slm-session/SKILL.md +1 -1
- package/plugin-src/skills/slm-status/SKILL.md +1 -1
- package/pyproject.toml +1 -1
- package/scripts/build-plugin.js +1 -1
- package/src/superlocalmemory/__init__.py +1 -1
- package/src/superlocalmemory/core/embeddings.py +5 -0
- package/src/superlocalmemory/dynamics/fisher_langevin_coupling.py +4 -0
- package/src/superlocalmemory/hooks/adapter_base.py +10 -3
- package/src/superlocalmemory/hooks/copilot_adapter.py +78 -9
- package/src/superlocalmemory/hooks/hook_handlers.py +26 -1
- package/src/superlocalmemory/hooks/memory_protocol.py +102 -0
- package/src/superlocalmemory/hooks/post_tool_async_hook.py +23 -5
- package/src/superlocalmemory/infra/event_bus.py +4 -0
- package/src/superlocalmemory/learning/feedback.py +59 -0
- package/src/superlocalmemory/learning/outcome_queue.py +10 -2
- package/src/superlocalmemory/llm/backbone.py +8 -1
- package/src/superlocalmemory/retrieval/reranker.py +5 -0
- package/src/superlocalmemory/server/routes/learning.py +2 -0
- package/src/superlocalmemory/server/routes/v3_api.py +11 -0
- package/src/superlocalmemory/server/unified_daemon.py +78 -0
- package/src/superlocalmemory/storage/database.py +34 -7
- package/src/superlocalmemory.egg-info/PKG-INFO +22 -7
- package/src/superlocalmemory.egg-info/SOURCES.txt +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,31 @@ All notable changes to SuperLocalMemory V3 will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [3.6.17] - 2026-06-21 — Community PR round + dashboard-feedback fix + SQLite tuning
|
|
9
|
+
|
|
10
|
+
Eight community pull requests merged after line-by-line review, plus fixes for the open issues. Every change was validated against the full test suite under the real 3.12 runtime; default single-machine behavior is unchanged.
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **HTTP write-path observability** (PR #52, @barrygfox). The HTTP fast paths (`/observe`, `/remember`, the AutoCapture pipeline, the materializer) now emit `EventBus` events tagged `source_protocol="http"`, so the dashboard event stream is no longer structurally empty. New event types: `memory.observed`, `memory.captured`, `memory.dropped`, `memory.queued`. Emission is best-effort and never affects the caller's response.
|
|
15
|
+
- **Marker-bounded adapter writes** (PR #54, @barrygfox). `CopilotAdapter` now wraps its content in `<!-- SLM-START -->` / `<!-- SLM-END -->` markers and merges into `.github/copilot-instructions.md` instead of overwriting it, preserving user- and agent-curated content. `disable()` strips the SLM block instead of deleting the file. New `hooks/memory_protocol.py` is the shared single source of truth for the marker contract.
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- **Dashboard feedback was completely broken** (issues #53/#59). The dashboard thumbs-up/down/pin and dwell handlers called `FeedbackCollector.record_dashboard_feedback()` — a method that did not exist, so every write raised `AttributeError` (caught by the route, so no lock leak, but the feature was dead). Implemented the method, mapping the dashboard vocabulary onto stored `(signal_type, value)` pairs; the raw query is hashed, never stored.
|
|
20
|
+
- **NULL columns reloaded as `[]` instead of `None`** (PR #50, @barrygfox). `_jl()` collapsed "no default" and explicit `default=None`, defeating downstream `is None` guards and causing `Mean of empty slice` warnings in the Fisher–Langevin coupling. Fixed with a `_MISSING` sentinel + an empty-array guard.
|
|
21
|
+
- **Lifecycle hooks hard-coded daemon port `:8765`** (PR #51, @barrygfox). Hooks now resolve the port from the per-user `~/.superlocalmemory/daemon.port` file; the non-loopback SSRF guard on `SLM_HOOK_DAEMON_URL` is preserved.
|
|
22
|
+
- **`atomic_write` honored a stale sync-log skip** (PR #55, @barrygfox). The on-disk file is re-hashed before a durable skip, so an out-of-band edit (`git restore`, manual edit) is no longer silently ignored.
|
|
23
|
+
- **Embedding/reranker workers ran single-threaded** (PR #56, @barrygfox). `OMP_NUM_THREADS` is restored in those subprocess workers (which load torch but never lightgbm, so the libomp SIGSEGV cannot occur).
|
|
24
|
+
- **Anthropic provider ignored `api_base`** (PR #57, @barrygfox). The Anthropic backbone now honors a configured base URL (Anthropic-compatible proxy), mirroring the OpenAI provider.
|
|
25
|
+
- **Dashboard screenshot committed as a raw binary** (PR #58, @MelleKoning). Converted to a Git LFS pointer per the existing `.gitattributes` rule.
|
|
26
|
+
- **"Test Connection" blocked for remote dashboards** (issue #40 residue). In `SLM_REMOTE` mode, an allowlisted LAN client may probe its own LAN LLM endpoint, exactly like the loopback dashboard. The SSRF guard is not relaxed for any non-allowlisted caller.
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
|
|
30
|
+
- **SQLite endurance knobs are env-tunable** (issue #53). `SLM_DB_BUSY_TIMEOUT_MS`, `SLM_DB_MAX_RETRIES`, and `SLM_DB_RETRY_BASE_DELAY` override the defaults for operators on slow/contended I/O. Unset env is byte-identical to the prior hard-coded constants.
|
|
31
|
+
- **`outcome_queue` polling backs off when idle** (issue #53). The drain worker now relaxes its 0.25s poll (doubling, capped at 2s) when the queue drains empty and snaps back to 0.25s the instant there is work, reducing idle contention on the shared SQLite file.
|
|
32
|
+
|
|
8
33
|
## [3.6.14] - 2026-06-18 — Audit-hardened: memory bounds, cross-tenant cache isolation, atomic credentials
|
|
9
34
|
|
|
10
35
|
Shipped through two adversarial audit passes (Qualixar Iron Pattern Stages 8–9), validated against a green 5933-test suite under the real 3.12 runtime. Default single-machine behavior is unchanged.
|
package/README.md
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
<img src="https://superlocalmemory.com/assets/logo-mark.png" alt="SuperLocalMemory" width="200"/>
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
|
-
<h1 align="center">SuperLocalMemory V3.6.
|
|
5
|
+
<h1 align="center">SuperLocalMemory V3.6.17</h1>
|
|
6
6
|
<p align="center"><strong>Cache. Compress. Remember. Three surfaces — proxy, MCP tools, or skill. Every setup covered.</strong><br/>
|
|
7
7
|
<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>
|
|
8
|
-
<p align="center"><code>v3.6.
|
|
8
|
+
<p align="center"><code>v3.6.17</code> — <strong>Plugin-native. Profile-aware. Distributed-ready.</strong><br/>
|
|
9
9
|
Proxy: <code>slm wrap claude</code> · MCP: add <code>slm_compress</code> to your config · Skill: zero-config</p>
|
|
10
10
|
<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>
|
|
11
11
|
|
|
@@ -162,7 +162,7 @@ Full docs: [docs/multi-machine.md](docs/multi-machine.md) · [docs/distributed-d
|
|
|
162
162
|
|:-----|:--------|:-----|
|
|
163
163
|
| **npm** (recommended) | `npm install -g superlocalmemory` | Node 14+, installs Python deps automatically |
|
|
164
164
|
| **pip** | `pip install superlocalmemory` | Python 3.11+, direct install |
|
|
165
|
-
| **Claude Code Plugin** (WP-06) | `/plugin install superlocalmemory@qualixar` | Self-bootstraps venv, isolated SLM_DATA_DIR, additive — 14-tool core |
|
|
165
|
+
| **Claude Code Plugin** (WP-06) | `/plugin marketplace add qualixar/superlocalmemory` then `/plugin install superlocalmemory@qualixar` | Self-bootstraps venv, isolated SLM_DATA_DIR, additive — 14-tool core. Ships the skills/agents/hooks/commands |
|
|
166
166
|
| **Portable / IDE connect** (WP-08) | `slm connect <ide> [--here]` | Wire any IDE without reinstalling; `slm connect claude-code` → plugin pointer |
|
|
167
167
|
|
|
168
168
|
After any install path: `slm setup` → `slm doctor` → `slm warmup` (optional, pre-downloads ~500MB embedding model).
|
|
@@ -218,18 +218,31 @@ Per-IDE configs available for Claude Code, Cursor, Windsurf, VS Code Copilot, Co
|
|
|
218
218
|
|
|
219
219
|
## Claude Code Plugin
|
|
220
220
|
|
|
221
|
-
Install directly in Claude Code
|
|
221
|
+
Install directly in Claude Code — no system-level npm/pip needed. This is how you
|
|
222
|
+
get the **skills, agents, hooks, commands, and rules** (the MCP server is
|
|
223
|
+
bootstrapped automatically). It is a two-step flow — add the marketplace once,
|
|
224
|
+
then install:
|
|
222
225
|
|
|
223
226
|
```bash
|
|
227
|
+
# 1. Add the Qualixar marketplace (one-time — the repo IS the marketplace)
|
|
228
|
+
/plugin marketplace add qualixar/superlocalmemory
|
|
229
|
+
|
|
230
|
+
# 2. Install the plugin
|
|
224
231
|
/plugin install superlocalmemory@qualixar
|
|
225
232
|
```
|
|
226
233
|
|
|
227
234
|
- Self-bootstraps a Python venv, installs all deps in an isolated `SLM_DATA_DIR`
|
|
228
|
-
- Registers 14-tool core MCP surface (`core14` profile by default)
|
|
235
|
+
- Registers the 14-tool core MCP surface (`core14` profile by default)
|
|
236
|
+
- Ships the SLM skills / agents / hooks / commands / rules
|
|
229
237
|
- Additive — does not replace an existing SLM install
|
|
230
238
|
- `slm connect claude-code` detects an existing plugin install and links them
|
|
231
239
|
|
|
232
|
-
|
|
240
|
+
> **Plugin vs `pip`/`npm`:** `pip install superlocalmemory` / `npm i -g superlocalmemory`
|
|
241
|
+
> give you the `slm` CLI + the MCP server (the *tools*). The **skills/agents/hooks/
|
|
242
|
+
> commands** come only through the plugin above. Use the plugin for Claude Code; use
|
|
243
|
+
> pip/npm for the CLI or other IDEs.
|
|
244
|
+
|
|
245
|
+
To update later: `/plugin marketplace update qualixar` then `/plugin install superlocalmemory@qualixar`.
|
|
233
246
|
|
|
234
247
|
---
|
|
235
248
|
|
|
@@ -294,6 +307,8 @@ slm dashboard # Opens at http://localhost:8765
|
|
|
294
307
|
|
|
295
308
|
| Version | Codename | Key Features |
|
|
296
309
|
|---|---|---|
|
|
310
|
+
| **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) |
|
|
311
|
+
| **v3.6.16** | Docs | Corrected Claude Code plugin install — adds the required `/plugin marketplace add` step; clarifies plugin vs pip/npm delivery |
|
|
297
312
|
| **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 |
|
|
298
313
|
| **v3.6.14** | Plugin-native | Claude Code Plugin (WP-06), MCP profiles (WP-01), IDE connect (WP-08), asset consolidation, UI polish (WP-12) |
|
|
299
314
|
| **v3.6.x** | Optimize Everywhere / Distributed-ready | Three surfaces (proxy/MCP/skill), `SLM_REMOTE=1` LAN mode, remote dashboard, custom LLM endpoints |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "superlocalmemory",
|
|
3
|
-
"version": "3.6.
|
|
3
|
+
"version": "3.6.17",
|
|
4
4
|
"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
5
|
"keywords": [
|
|
6
6
|
"ai-memory",
|
package/plugin/CLAUDE.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!-- BEGIN SuperLocalMemory v3.6.
|
|
1
|
+
<!-- BEGIN SuperLocalMemory v3.6.17 -->
|
|
2
2
|
|
|
3
3
|
## SuperLocalMemory (SLM) — Agent Rules
|
|
4
4
|
|
|
@@ -39,6 +39,6 @@ slm-recall · slm-remember · slm-session · slm-status · slm-cache · slm-comp
|
|
|
39
39
|
### Subagents
|
|
40
40
|
slm-memory-advisor (memory decisions, session hygiene) · slm-optimize-advisor (context compression + KV cache)
|
|
41
41
|
|
|
42
|
-
<!-- END SuperLocalMemory v3.6.
|
|
42
|
+
<!-- END SuperLocalMemory v3.6.17 -->
|
|
43
43
|
|
|
44
|
-
SuperLocalMemory v3.6.
|
|
44
|
+
SuperLocalMemory v3.6.17 · Qualixar · AGPL-3.0-or-later
|
|
@@ -41,4 +41,4 @@ recall→`slm recall "<q>" --limit N` (add `--include-global`/`--include-shared`
|
|
|
41
41
|
# What NOT to do
|
|
42
42
|
Never session_init twice; never forget dry_run=False without reporting preview; never dump a whole file into remember; never invent a memory; never claim "saved" without success:true / clean CLI exit.
|
|
43
43
|
|
|
44
|
-
SuperLocalMemory v3.6.
|
|
44
|
+
SuperLocalMemory v3.6.17 · Qualixar · AGPL-3.0-or-later
|
|
@@ -35,4 +35,4 @@ stats→`slm optimize status`/`savings` · compress→`slm compress` · cache→
|
|
|
35
35
|
# What NOT to do
|
|
36
36
|
Never compress code-for-edit/JSON-to-parse/<500 chars; never store secrets/ccr_ids; never let optimize failure block/alter the task; never claim a specific savings %.
|
|
37
37
|
|
|
38
|
-
SuperLocalMemory v3.6.
|
|
38
|
+
SuperLocalMemory v3.6.17 · Qualixar · AGPL-3.0-or-later
|
package/plugin/requirements.txt
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
superlocalmemory==3.6.
|
|
1
|
+
superlocalmemory==3.6.17
|
|
@@ -41,4 +41,4 @@ recall→`slm recall "<q>" --limit N` (add `--include-global`/`--include-shared`
|
|
|
41
41
|
# What NOT to do
|
|
42
42
|
Never session_init twice; never forget dry_run=False without reporting preview; never dump a whole file into remember; never invent a memory; never claim "saved" without success:true / clean CLI exit.
|
|
43
43
|
|
|
44
|
-
SuperLocalMemory v3.6.
|
|
44
|
+
SuperLocalMemory v3.6.17 · Qualixar · AGPL-3.0-or-later
|
|
@@ -35,4 +35,4 @@ stats→`slm optimize status`/`savings` · compress→`slm compress` · cache→
|
|
|
35
35
|
# What NOT to do
|
|
36
36
|
Never compress code-for-edit/JSON-to-parse/<500 chars; never store secrets/ccr_ids; never let optimize failure block/alter the task; never claim a specific savings %.
|
|
37
37
|
|
|
38
|
-
SuperLocalMemory v3.6.
|
|
38
|
+
SuperLocalMemory v3.6.17 · Qualixar · AGPL-3.0-or-later
|
|
@@ -13,4 +13,4 @@ Save to SuperLocalMemory: $ARGUMENTS
|
|
|
13
13
|
5. Confirm only on success:true. If success is not true, report the error — never claim "saved."
|
|
14
14
|
6. MCP unavailable → CLI fallback: `slm remember "$ARGUMENTS" --tags <tags>` (note: `--importance` is MCP-only, not a CLI flag).
|
|
15
15
|
|
|
16
|
-
SuperLocalMemory v3.6.
|
|
16
|
+
SuperLocalMemory v3.6.17 · Qualixar · AGPL-3.0-or-later
|
|
@@ -12,4 +12,4 @@ Show SuperLocalMemory status and optimization counters.
|
|
|
12
12
|
|
|
13
13
|
Note: MCP get_status is intentionally NOT used here — it is outside the core profile and would error. Use `slm status` (CLI) + `slm_optimize_stats` (MCP) only.
|
|
14
14
|
|
|
15
|
-
SuperLocalMemory v3.6.
|
|
15
|
+
SuperLocalMemory v3.6.17 · Qualixar · AGPL-3.0-or-later
|
package/plugin-src/manifest.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
superlocalmemory==3.6.
|
|
1
|
+
superlocalmemory==3.6.17
|
|
@@ -88,4 +88,4 @@ When the SLM MCP server is unavailable, use these CLI equivalents:
|
|
|
88
88
|
| `slm_cache_get` | `key` | KV cache get; returns hit, value |
|
|
89
89
|
| `slm_optimize_stats` | `()` | Returns compress_runs, tokens_saved_compress, cache_kv_hits |
|
|
90
90
|
|
|
91
|
-
SuperLocalMemory v3.6.
|
|
91
|
+
SuperLocalMemory v3.6.17 · Qualixar · AGPL-3.0-or-later
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!-- BEGIN SuperLocalMemory v3.6.
|
|
1
|
+
<!-- BEGIN SuperLocalMemory v3.6.17 -->
|
|
2
2
|
|
|
3
3
|
## SuperLocalMemory (SLM) — Agent Rules
|
|
4
4
|
|
|
@@ -39,6 +39,6 @@ slm-recall · slm-remember · slm-session · slm-status · slm-cache · slm-comp
|
|
|
39
39
|
### Subagents
|
|
40
40
|
slm-memory-advisor (memory decisions, session hygiene) · slm-optimize-advisor (context compression + KV cache)
|
|
41
41
|
|
|
42
|
-
<!-- END SuperLocalMemory v3.6.
|
|
42
|
+
<!-- END SuperLocalMemory v3.6.17 -->
|
|
43
43
|
|
|
44
|
-
SuperLocalMemory v3.6.
|
|
44
|
+
SuperLocalMemory v3.6.17 · Qualixar · AGPL-3.0-or-later
|
package/pyproject.toml
CHANGED
package/scripts/build-plugin.js
CHANGED
|
@@ -30,7 +30,7 @@ import process from 'node:process';
|
|
|
30
30
|
// ---------------------------------------------------------------------------
|
|
31
31
|
// Constants
|
|
32
32
|
// ---------------------------------------------------------------------------
|
|
33
|
-
const VERSION = '3.6.
|
|
33
|
+
const VERSION = '3.6.17';
|
|
34
34
|
const MANIFEST_REL = 'plugin-src/manifest.json';
|
|
35
35
|
const GENERATED_BANNER = `# _GENERATED — DO NOT HAND-EDIT
|
|
36
36
|
|
|
@@ -32,7 +32,7 @@ if "OMP_NUM_THREADS" not in os.environ:
|
|
|
32
32
|
os.environ["OMP_NUM_THREADS"] = "2"
|
|
33
33
|
# ---------------------------------------------------------------------------
|
|
34
34
|
|
|
35
|
-
__version__ = "3.6.
|
|
35
|
+
__version__ = "3.6.17"
|
|
36
36
|
|
|
37
37
|
_REQUIRED_VERSIONS = {
|
|
38
38
|
"sentence_transformers": "5.3.0",
|
|
@@ -473,6 +473,11 @@ class EmbeddingService:
|
|
|
473
473
|
"TOKENIZERS_PARALLELISM": "false",
|
|
474
474
|
"TORCH_DEVICE": "cpu",
|
|
475
475
|
"ORT_DISABLE_COREML": "1",
|
|
476
|
+
# Restore parallel OpenMP. The package caps OMP_NUM_THREADS
|
|
477
|
+
# globally to avoid a torch+lightgbm libomp SIGSEGV in the
|
|
478
|
+
# main process. This worker loads torch but never lightgbm,
|
|
479
|
+
# so there is no collision risk and full parallelism is safe.
|
|
480
|
+
"OMP_NUM_THREADS": str(os.cpu_count() or 4),
|
|
476
481
|
}
|
|
477
482
|
from superlocalmemory.core.platform_utils import popen_platform_kwargs
|
|
478
483
|
self._worker_proc = subprocess.Popen(
|
|
@@ -111,6 +111,8 @@ class FisherLangevinCoupling:
|
|
|
111
111
|
CouplingState with derived temperature, direction, and weight.
|
|
112
112
|
"""
|
|
113
113
|
var_arr = np.asarray(fisher_variance, dtype=np.float64)
|
|
114
|
+
if var_arr.size == 0:
|
|
115
|
+
return CouplingState()
|
|
114
116
|
|
|
115
117
|
# Step 1: Fisher confidence from variance
|
|
116
118
|
# Low variance = high confidence (memory is well-characterized)
|
|
@@ -203,6 +205,8 @@ class FisherLangevinCoupling:
|
|
|
203
205
|
return self._base_temp
|
|
204
206
|
|
|
205
207
|
var_arr = np.asarray(fisher_variance, dtype=np.float64)
|
|
208
|
+
if var_arr.size == 0:
|
|
209
|
+
return self._base_temp
|
|
206
210
|
avg_var = float(np.mean(np.clip(var_arr, 1e-8, None)))
|
|
207
211
|
fisher_conf = min(1.0, 1.0 / (1.0 + avg_var) + min(access_count * 0.02, 0.2))
|
|
208
212
|
return self._base_temp / (fisher_conf + self._epsilon)
|
|
@@ -217,9 +217,16 @@ def atomic_write(
|
|
|
217
217
|
prev = sync_log_last_content_sha256(sync_log_db, adapter_name, target_sha)
|
|
218
218
|
|
|
219
219
|
if prev == new_hash and resolved_path.exists():
|
|
220
|
-
# Durable skip
|
|
221
|
-
#
|
|
222
|
-
|
|
220
|
+
# Durable skip only if the on-disk content also matches the new hash.
|
|
221
|
+
# The sync-log row alone is not authoritative: the file may have been
|
|
222
|
+
# mutated out-of-band (e.g. ``git restore``, manual edit) since the
|
|
223
|
+
# last sync. Re-hash the file and re-write if it diverges.
|
|
224
|
+
try:
|
|
225
|
+
disk_hash = hashlib.sha256(resolved_path.read_bytes()).hexdigest()
|
|
226
|
+
except OSError:
|
|
227
|
+
disk_hash = None
|
|
228
|
+
if disk_hash == new_hash:
|
|
229
|
+
return WriteResult(wrote=False, bytes_written=0, content_sha256=new_hash)
|
|
223
230
|
|
|
224
231
|
resolved_path.parent.mkdir(parents=True, exist_ok=True)
|
|
225
232
|
tmp = resolved_path.with_suffix(resolved_path.suffix + ".slm-tmp")
|
|
@@ -8,9 +8,15 @@ LLD-05 §6. Verified (verification-2026-04-17.md claim 5): plain markdown,
|
|
|
8
8
|
no frontmatter, soft 2 KB / hard 4 KB cap. Adapter is INACTIVE when the
|
|
9
9
|
project has no ``.github/`` directory — we do not create it ourselves.
|
|
10
10
|
|
|
11
|
+
v3.4.23 fix: the SLM-managed content is wrapped in
|
|
12
|
+
``<!-- SLM-START -->`` / ``<!-- SLM-END -->`` markers and merged into the
|
|
13
|
+
host file rather than overwriting it. ``.github/copilot-instructions.md``
|
|
14
|
+
is typically a curated, project-specific document; destructive rewrites
|
|
15
|
+
deleted the user's prose.
|
|
16
|
+
|
|
11
17
|
Hard rules covered here:
|
|
12
18
|
- A1 / A2 / A3 / A7: via ``adapter_base.atomic_write``.
|
|
13
|
-
- A4: soft 2 KB + hard 4 KB cap enforcement.
|
|
19
|
+
- A4: soft 2 KB + hard 4 KB cap enforcement on the SLM section.
|
|
14
20
|
"""
|
|
15
21
|
|
|
16
22
|
from __future__ import annotations
|
|
@@ -39,6 +45,12 @@ from superlocalmemory.hooks.context_payload import (
|
|
|
39
45
|
format_topics,
|
|
40
46
|
truncate_payload_for_cap,
|
|
41
47
|
)
|
|
48
|
+
from superlocalmemory.hooks.memory_protocol import (
|
|
49
|
+
SLM_MARKER_END,
|
|
50
|
+
SLM_MARKER_START,
|
|
51
|
+
memory_protocol_markdown,
|
|
52
|
+
strip_slm_block as _strip_existing_block,
|
|
53
|
+
)
|
|
42
54
|
|
|
43
55
|
logger = logging.getLogger(__name__)
|
|
44
56
|
|
|
@@ -52,20 +64,37 @@ _BODY_TEMPLATE = (
|
|
|
52
64
|
"## Entities\n{entities}\n\n"
|
|
53
65
|
"## Never do\n"
|
|
54
66
|
"- Do not modify files under `.slm/`\n"
|
|
55
|
-
"- Do not commit `*.slm-cache.db`\n"
|
|
67
|
+
"- Do not commit `*.slm-cache.db`\n\n"
|
|
56
68
|
)
|
|
57
69
|
|
|
58
70
|
|
|
59
71
|
def render_copilot(payload: ContextPayload) -> bytes:
|
|
60
|
-
|
|
72
|
+
# Two-stage assembly: format the dynamic header (which contains {}
|
|
73
|
+
# placeholders), then concatenate the static memory-protocol block
|
|
74
|
+
# verbatim. The memory-protocol block legitimately contains literal
|
|
75
|
+
# braces (JSON-shaped argument examples for the agent) which must not
|
|
76
|
+
# be interpreted as format fields.
|
|
77
|
+
header = _BODY_TEMPLATE.format(
|
|
61
78
|
version=payload.version,
|
|
62
79
|
topics=format_topics(payload),
|
|
63
80
|
entities=format_entities(payload),
|
|
64
|
-
)
|
|
81
|
+
)
|
|
82
|
+
return (header + memory_protocol_markdown()).encode("utf-8")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _wrap_managed(rendered: bytes) -> str:
|
|
86
|
+
"""Wrap rendered SLM content in ``<!-- SLM-START -->`` markers."""
|
|
87
|
+
return (
|
|
88
|
+
f"{SLM_MARKER_START}\n"
|
|
89
|
+
"<!-- Managed by SuperLocalMemory. Edits between SLM-START and "
|
|
90
|
+
"SLM-END will be overwritten. -->\n\n"
|
|
91
|
+
f"{rendered.decode('utf-8')}\n"
|
|
92
|
+
f"{SLM_MARKER_END}\n"
|
|
93
|
+
)
|
|
65
94
|
|
|
66
95
|
|
|
67
96
|
class CopilotAdapter:
|
|
68
|
-
"""Project-scope Copilot adapter."""
|
|
97
|
+
"""Project-scope Copilot adapter (marker-bounded merge)."""
|
|
69
98
|
|
|
70
99
|
def __init__(
|
|
71
100
|
self,
|
|
@@ -124,8 +153,39 @@ class CopilotAdapter:
|
|
|
124
153
|
)
|
|
125
154
|
rendered = truncate_to_cap(rendered, cap=self._hard_cap)
|
|
126
155
|
|
|
156
|
+
# Marker-bounded merge — preserve any user-curated content in the
|
|
157
|
+
# host file. Strip any prior SLM block(s) and re-append a fresh one.
|
|
158
|
+
existing = ""
|
|
159
|
+
if resolved.exists():
|
|
160
|
+
try:
|
|
161
|
+
existing = resolved.read_text(encoding="utf-8")
|
|
162
|
+
except OSError as exc:
|
|
163
|
+
logger.warning(
|
|
164
|
+
"copilot: cannot read %s: %s", resolved, exc,
|
|
165
|
+
)
|
|
166
|
+
return False
|
|
167
|
+
|
|
168
|
+
# Orphaned start marker — refuse to write rather than corrupt.
|
|
169
|
+
if (SLM_MARKER_START in existing
|
|
170
|
+
and SLM_MARKER_END not in existing):
|
|
171
|
+
logger.warning(
|
|
172
|
+
"copilot: %s present but %s missing in %s; refusing to write",
|
|
173
|
+
SLM_MARKER_START, SLM_MARKER_END, resolved,
|
|
174
|
+
)
|
|
175
|
+
return False
|
|
176
|
+
|
|
177
|
+
stripped = _strip_existing_block(existing)
|
|
178
|
+
section = _wrap_managed(rendered)
|
|
179
|
+
if stripped:
|
|
180
|
+
if not stripped.endswith("\n"):
|
|
181
|
+
stripped += "\n"
|
|
182
|
+
# One blank line between user content and the managed section.
|
|
183
|
+
new_content = stripped + "\n" + section
|
|
184
|
+
else:
|
|
185
|
+
new_content = section
|
|
186
|
+
|
|
127
187
|
result: WriteResult = atomic_write(
|
|
128
|
-
resolved,
|
|
188
|
+
resolved, new_content.encode("utf-8"),
|
|
129
189
|
adapter_name=self.name,
|
|
130
190
|
profile_id=self._profile_id,
|
|
131
191
|
sync_log_db=self._sync_log_db,
|
|
@@ -137,11 +197,20 @@ class CopilotAdapter:
|
|
|
137
197
|
resolved = self.target_path
|
|
138
198
|
except PathTraversalError:
|
|
139
199
|
return
|
|
200
|
+
# Marker-bounded strip — never delete the host file (user-owned).
|
|
140
201
|
if resolved.exists():
|
|
141
202
|
try:
|
|
142
|
-
resolved.
|
|
143
|
-
except OSError:
|
|
144
|
-
|
|
203
|
+
existing = resolved.read_text(encoding="utf-8")
|
|
204
|
+
except OSError:
|
|
205
|
+
existing = ""
|
|
206
|
+
stripped = _strip_existing_block(existing)
|
|
207
|
+
if stripped != existing:
|
|
208
|
+
try:
|
|
209
|
+
resolved.write_text(stripped, encoding="utf-8")
|
|
210
|
+
except OSError as exc: # pragma: no cover
|
|
211
|
+
logger.warning(
|
|
212
|
+
"copilot: failed to strip on disable: %s", exc,
|
|
213
|
+
)
|
|
145
214
|
record_disable(
|
|
146
215
|
resolved,
|
|
147
216
|
adapter_name=self.name,
|
|
@@ -36,7 +36,32 @@ _LAST_CONSOLIDATION = os.path.join(
|
|
|
36
36
|
)
|
|
37
37
|
|
|
38
38
|
|
|
39
|
-
|
|
39
|
+
_DEFAULT_DAEMON_PORT = 8765
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _daemon_url() -> str:
|
|
43
|
+
"""Resolve the daemon base URL, preferring the per-user port file.
|
|
44
|
+
|
|
45
|
+
On a shared host each user runs their own daemon bound to a different
|
|
46
|
+
port; the active port is written to ``~/.superlocalmemory/daemon.port``
|
|
47
|
+
at startup. Reading it here keeps lifecycle hooks pointed at the
|
|
48
|
+
caller's own daemon instead of a hard-coded ``8765`` that may belong to
|
|
49
|
+
another user's instance. Falls back to the default port when the file is
|
|
50
|
+
absent or unreadable. Stdlib only — no SLM imports in the hot path.
|
|
51
|
+
"""
|
|
52
|
+
port = _DEFAULT_DAEMON_PORT
|
|
53
|
+
try:
|
|
54
|
+
port_file = os.path.join(
|
|
55
|
+
os.path.expanduser("~"), ".superlocalmemory", "daemon.port",
|
|
56
|
+
)
|
|
57
|
+
with open(port_file) as fh:
|
|
58
|
+
port = int(fh.read().strip())
|
|
59
|
+
except Exception:
|
|
60
|
+
pass
|
|
61
|
+
return f"http://127.0.0.1:{port}"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
_DAEMON_URL = _daemon_url()
|
|
40
65
|
|
|
41
66
|
|
|
42
67
|
def _daemon_post(path: str, body: dict, timeout: float = 3.0) -> bool:
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
|
|
2
|
+
# Licensed under AGPL-3.0-or-later - see LICENSE file
|
|
3
|
+
# Part of SuperLocalMemory V3 | https://qualixar.com | https://varunpratap.com
|
|
4
|
+
|
|
5
|
+
"""Shared utilities for marker-bounded writes into agent instruction files.
|
|
6
|
+
|
|
7
|
+
Adapters that inject SLM content into IDE/agent instruction files (e.g.
|
|
8
|
+
``.github/copilot-instructions.md``) use the constants and helpers here to
|
|
9
|
+
demarcate the SLM-managed section so user-curated content outside the
|
|
10
|
+
markers is preserved on every sync.
|
|
11
|
+
|
|
12
|
+
Marker contract
|
|
13
|
+
---------------
|
|
14
|
+
SLM wraps its content in a pair of HTML comments::
|
|
15
|
+
|
|
16
|
+
<!-- SLM-START -->
|
|
17
|
+
... managed content ...
|
|
18
|
+
<!-- SLM-END -->
|
|
19
|
+
|
|
20
|
+
``strip_slm_block`` removes all such pairs idempotently; adapters call it
|
|
21
|
+
before re-writing so a fresh block replaces the old one in place.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import logging
|
|
27
|
+
|
|
28
|
+
logger = logging.getLogger(__name__)
|
|
29
|
+
|
|
30
|
+
#: Opening marker for the SLM-managed section.
|
|
31
|
+
SLM_MARKER_START = "<!-- SLM-START -->"
|
|
32
|
+
#: Closing marker for the SLM-managed section.
|
|
33
|
+
SLM_MARKER_END = "<!-- SLM-END -->"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def strip_slm_block(text: str) -> str:
|
|
37
|
+
"""Remove all SLM-managed sections from *text*.
|
|
38
|
+
|
|
39
|
+
Idempotent — returns *text* unchanged when no markers are present.
|
|
40
|
+
Strips every ``SLM-START``/``SLM-END`` pair to handle files that
|
|
41
|
+
accumulated duplicates from a previous bug or a competing writer.
|
|
42
|
+
|
|
43
|
+
If a ``SLM-START`` marker has no matching ``SLM-END``, the file is
|
|
44
|
+
returned unchanged to avoid eating user content; the caller should
|
|
45
|
+
treat this as an orphaned-marker error and skip the write.
|
|
46
|
+
"""
|
|
47
|
+
out = text
|
|
48
|
+
while True:
|
|
49
|
+
start_idx = out.find(SLM_MARKER_START)
|
|
50
|
+
if start_idx == -1:
|
|
51
|
+
return out
|
|
52
|
+
end_idx = out.find(SLM_MARKER_END, start_idx)
|
|
53
|
+
if end_idx == -1:
|
|
54
|
+
logger.warning(
|
|
55
|
+
"memory_protocol: %s found but %s missing; leaving file unchanged",
|
|
56
|
+
SLM_MARKER_START,
|
|
57
|
+
SLM_MARKER_END,
|
|
58
|
+
)
|
|
59
|
+
return text
|
|
60
|
+
cut_end = end_idx + len(SLM_MARKER_END)
|
|
61
|
+
if cut_end < len(out) and out[cut_end] == "\n":
|
|
62
|
+
cut_end += 1
|
|
63
|
+
# Pull back up to two leading newlines added as a boundary separator.
|
|
64
|
+
cut_start = start_idx
|
|
65
|
+
while cut_start > 0 and out[cut_start - 1] == "\n":
|
|
66
|
+
cut_start -= 1
|
|
67
|
+
if start_idx - cut_start >= 2:
|
|
68
|
+
break
|
|
69
|
+
out = out[:cut_start] + out[cut_end:]
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def memory_protocol_markdown() -> str:
|
|
73
|
+
"""Return the agent-facing Markdown memory protocol block.
|
|
74
|
+
|
|
75
|
+
Embedded verbatim into Markdown instruction files such as
|
|
76
|
+
``.github/copilot-instructions.md``. Trailing newline included so
|
|
77
|
+
callers can concatenate without worrying about boundary whitespace.
|
|
78
|
+
"""
|
|
79
|
+
return (
|
|
80
|
+
"## Memory protocol\n"
|
|
81
|
+
"SLM tools are available via the `slm-hub` MCP gateway. Use them to "
|
|
82
|
+
"make this brain context grow across sessions.\n\n"
|
|
83
|
+
"- **At the start of work on an unfamiliar area**, call "
|
|
84
|
+
"`hub__call_tool` with `tool=\"slm__recall\"` and "
|
|
85
|
+
"`arguments={\"query\": \"<topic>\"}` to surface prior decisions "
|
|
86
|
+
"and patterns.\n"
|
|
87
|
+
"- **At the end of a substantial task** (a fix, a decision, a "
|
|
88
|
+
"non-trivial change, a session conclusion), call `hub__call_tool` "
|
|
89
|
+
"with `tool=\"slm__remember\"` and `arguments={\"content\": "
|
|
90
|
+
"\"<one-paragraph summary of what was decided / changed / "
|
|
91
|
+
"learned>\", \"tags\": \"<comma-separated kebab-case keywords>\"}`.\n"
|
|
92
|
+
"- A \"substantial task\" is anything you would write a commit "
|
|
93
|
+
"message or handoff note about — not every tool call.\n"
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
__all__ = (
|
|
98
|
+
"SLM_MARKER_START",
|
|
99
|
+
"SLM_MARKER_END",
|
|
100
|
+
"strip_slm_block",
|
|
101
|
+
"memory_protocol_markdown",
|
|
102
|
+
)
|
|
@@ -30,6 +30,24 @@ _ALLOWED_DAEMON_HOSTS: frozenset[str] = frozenset({
|
|
|
30
30
|
"127.0.0.1", "localhost", "::1", "[::1]",
|
|
31
31
|
})
|
|
32
32
|
|
|
33
|
+
_DEFAULT_DAEMON_PORT = 8765
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _port_file_url() -> str:
|
|
37
|
+
"""Loopback daemon URL from the per-user port file (default 8765).
|
|
38
|
+
|
|
39
|
+
On a shared host each user runs their own daemon on a different port,
|
|
40
|
+
written to ``~/.superlocalmemory/daemon.port`` at startup. Falling back
|
|
41
|
+
to this instead of a hard-coded ``8765`` keeps the hook pointed at the
|
|
42
|
+
caller's own daemon. Stdlib only.
|
|
43
|
+
"""
|
|
44
|
+
port = _DEFAULT_DAEMON_PORT
|
|
45
|
+
try:
|
|
46
|
+
port = int((Path.home() / ".superlocalmemory" / "daemon.port").read_text().strip())
|
|
47
|
+
except Exception:
|
|
48
|
+
pass
|
|
49
|
+
return f"http://127.0.0.1:{port}"
|
|
50
|
+
|
|
33
51
|
|
|
34
52
|
def _sanitised_daemon_url() -> str:
|
|
35
53
|
"""Return the configured daemon URL only if it's loopback-scoped.
|
|
@@ -38,21 +56,21 @@ def _sanitised_daemon_url() -> str:
|
|
|
38
56
|
shell profile) could set ``SLM_HOOK_DAEMON_URL`` to a remote host
|
|
39
57
|
and exfiltrate the install token via the ``X-SLM-Hook-Token``
|
|
40
58
|
header. We refuse any non-loopback URL and fall back to the local
|
|
41
|
-
daemon.
|
|
59
|
+
daemon (resolved via the per-user port file, not a hard-coded port).
|
|
42
60
|
"""
|
|
43
61
|
raw = os.environ.get("SLM_HOOK_DAEMON_URL", "").strip()
|
|
44
62
|
if not raw:
|
|
45
|
-
return
|
|
63
|
+
return _port_file_url()
|
|
46
64
|
try:
|
|
47
65
|
from urllib.parse import urlparse
|
|
48
66
|
parsed = urlparse(raw)
|
|
49
67
|
except Exception: # pragma: no cover — urllib always importable
|
|
50
|
-
return
|
|
68
|
+
return _port_file_url()
|
|
51
69
|
if parsed.scheme not in ("http", "https"):
|
|
52
|
-
return
|
|
70
|
+
return _port_file_url()
|
|
53
71
|
host = (parsed.hostname or "").lower()
|
|
54
72
|
if host not in _ALLOWED_DAEMON_HOSTS:
|
|
55
|
-
return
|
|
73
|
+
return _port_file_url()
|
|
56
74
|
# Preserve the scheme + port (user may bind daemon on a non-default port).
|
|
57
75
|
port = f":{parsed.port}" if parsed.port else ""
|
|
58
76
|
return f"{parsed.scheme}://{host}{port}"
|
|
@@ -32,6 +32,10 @@ VALID_EVENT_TYPES = frozenset([
|
|
|
32
32
|
"memory.updated", # Existing memory modified
|
|
33
33
|
"memory.deleted", # Memory removed
|
|
34
34
|
"memory.recalled", # Memory retrieved by an agent
|
|
35
|
+
"memory.observed", # /observe accepted content into the debounce buffer
|
|
36
|
+
"memory.captured", # AutoCapture matched a buffered observation
|
|
37
|
+
"memory.dropped", # AutoCapture rejected a buffered observation
|
|
38
|
+
"memory.queued", # /remember accepted content into pending.db (async)
|
|
35
39
|
"graph.updated", # Knowledge graph rebuilt
|
|
36
40
|
"pattern.learned", # New pattern detected
|
|
37
41
|
"agent.connected", # New agent connects
|
|
@@ -41,6 +41,18 @@ SIGNAL_VALUES: Dict[str, float] = {
|
|
|
41
41
|
"access_pattern": 0.6,
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
# Dashboard UI vocabulary -> (signal_type, signal_value). The dashboard speaks
|
|
45
|
+
# thumbs_up/thumbs_down/pin (explicit) and dwell_positive/dwell_negative
|
|
46
|
+
# (derived from modal dwell time). Unknown types fall back to a neutral
|
|
47
|
+
# user_correction signal rather than being dropped.
|
|
48
|
+
_DASHBOARD_SIGNAL_MAP: Dict[str, tuple[str, float]] = {
|
|
49
|
+
"thumbs_up": ("user_positive", 1.0),
|
|
50
|
+
"thumbs_down": ("user_negative", 0.0),
|
|
51
|
+
"pin": ("user_pin", 1.0),
|
|
52
|
+
"dwell_positive": ("dwell_positive", 0.6),
|
|
53
|
+
"dwell_negative": ("dwell_negative", 0.2),
|
|
54
|
+
}
|
|
55
|
+
|
|
44
56
|
_CREATE_TABLE = """
|
|
45
57
|
CREATE TABLE IF NOT EXISTS learning_feedback (
|
|
46
58
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
@@ -219,6 +231,53 @@ class FeedbackCollector:
|
|
|
219
231
|
finally:
|
|
220
232
|
conn.close()
|
|
221
233
|
|
|
234
|
+
# ------------------------------------------------------------------
|
|
235
|
+
# Public API: record dashboard feedback
|
|
236
|
+
# ------------------------------------------------------------------
|
|
237
|
+
|
|
238
|
+
def record_dashboard_feedback(
|
|
239
|
+
self,
|
|
240
|
+
memory_id: str,
|
|
241
|
+
query: str = "",
|
|
242
|
+
feedback_type: str = "",
|
|
243
|
+
profile_id: str = "default",
|
|
244
|
+
) -> Optional[int]:
|
|
245
|
+
"""Record an explicit feedback signal raised from the dashboard UI.
|
|
246
|
+
|
|
247
|
+
Maps the dashboard's vocabulary (``thumbs_up``/``thumbs_down``/``pin``
|
|
248
|
+
and the dwell-derived ``dwell_positive``/``dwell_negative``) onto a
|
|
249
|
+
stored ``(signal_type, signal_value)`` pair. ``memory_id`` is the fact
|
|
250
|
+
id; the raw ``query`` is hashed and never stored. Returns the inserted
|
|
251
|
+
row id, or ``None`` on missing ``memory_id``.
|
|
252
|
+
|
|
253
|
+
This method restores the dashboard feedback path: the HTTP routes in
|
|
254
|
+
``server/routes/learning.py`` called it before it existed, so every
|
|
255
|
+
thumbs/pin/dwell write raised ``AttributeError`` (issues #53/#59).
|
|
256
|
+
"""
|
|
257
|
+
if not memory_id:
|
|
258
|
+
return None
|
|
259
|
+
signal_type, value = _DASHBOARD_SIGNAL_MAP.get(
|
|
260
|
+
feedback_type, ("user_correction", 0.5),
|
|
261
|
+
)
|
|
262
|
+
qhash = _hash_query(query) if query else None
|
|
263
|
+
now = _utcnow_iso()
|
|
264
|
+
|
|
265
|
+
with self._lock:
|
|
266
|
+
conn = self._connect()
|
|
267
|
+
try:
|
|
268
|
+
cursor = conn.execute(
|
|
269
|
+
"INSERT INTO learning_feedback "
|
|
270
|
+
"(profile_id, fact_id, signal_type, signal_value, "
|
|
271
|
+
"query_hash, created_at, metadata) "
|
|
272
|
+
"VALUES (?, ?, ?, ?, ?, ?, ?)",
|
|
273
|
+
(profile_id or "default", str(memory_id), signal_type,
|
|
274
|
+
value, qhash, now, None),
|
|
275
|
+
)
|
|
276
|
+
conn.commit()
|
|
277
|
+
return cursor.lastrowid
|
|
278
|
+
finally:
|
|
279
|
+
conn.close()
|
|
280
|
+
|
|
222
281
|
# ------------------------------------------------------------------
|
|
223
282
|
# Public API: read feedback
|
|
224
283
|
# ------------------------------------------------------------------
|
|
@@ -207,11 +207,19 @@ def _worker_loop(memory_db_path: Path, interval_s: float) -> None:
|
|
|
207
207
|
)
|
|
208
208
|
import time as _time
|
|
209
209
|
next_reap = _time.monotonic() + _REAP_INTERVAL_S
|
|
210
|
-
|
|
210
|
+
# Adaptive idle back-off: poll at interval_s under load, but relax the wait
|
|
211
|
+
# (doubling, capped) when the queue drains empty so an idle daemon stops
|
|
212
|
+
# contending on the shared SQLite file every 0.25s (issue #53). Snaps back
|
|
213
|
+
# to interval_s the instant there is work again.
|
|
214
|
+
_idle_cap = max(interval_s, 2.0)
|
|
215
|
+
cur_wait = interval_s
|
|
216
|
+
while not _stop_event.wait(cur_wait):
|
|
211
217
|
try:
|
|
212
|
-
_drain_once(memory_db_path)
|
|
218
|
+
drained = _drain_once(memory_db_path)
|
|
213
219
|
except Exception as exc: # pragma: no cover — defensive
|
|
214
220
|
logger.warning("outcome_queue drain crashed: %s", exc)
|
|
221
|
+
drained = 0
|
|
222
|
+
cur_wait = interval_s if drained else min(cur_wait * 2.0, _idle_cap)
|
|
215
223
|
# Periodic reaper for CLI/dashboard outcomes that no Stop hook
|
|
216
224
|
# will ever finalize. Runs OFF the drain path so a busy queue
|
|
217
225
|
# doesn't starve the reaper.
|
|
@@ -295,7 +295,14 @@ class LLMBackbone:
|
|
|
295
295
|
}
|
|
296
296
|
if system:
|
|
297
297
|
payload["system"] = system
|
|
298
|
-
|
|
298
|
+
# Respect custom base_url (e.g. Anthropic-compatible proxy).
|
|
299
|
+
# Append /v1/messages to the root URL, mirroring how _build_openai
|
|
300
|
+
# handles api_base. Falls back to the official Anthropic endpoint.
|
|
301
|
+
url = (
|
|
302
|
+
self._base_url.rstrip("/") + "/v1/messages"
|
|
303
|
+
if self._base_url else _ANTHROPIC_URL
|
|
304
|
+
)
|
|
305
|
+
return url, headers, payload
|
|
299
306
|
|
|
300
307
|
def _build_azure(
|
|
301
308
|
self, prompt: str, system: str, max_tokens: int, temperature: float,
|
|
@@ -197,6 +197,11 @@ class CrossEncoderReranker:
|
|
|
197
197
|
"TOKENIZERS_PARALLELISM": "false",
|
|
198
198
|
"TORCH_DEVICE": "cpu",
|
|
199
199
|
"ORT_DISABLE_COREML": "1",
|
|
200
|
+
# Restore parallel OpenMP. The package caps OMP_NUM_THREADS
|
|
201
|
+
# globally to avoid a torch+lightgbm libomp SIGSEGV in the
|
|
202
|
+
# main process. This worker loads torch but never lightgbm,
|
|
203
|
+
# so there is no collision risk and full parallelism is safe.
|
|
204
|
+
"OMP_NUM_THREADS": str(os.cpu_count() or 4),
|
|
200
205
|
}
|
|
201
206
|
from superlocalmemory.core.platform_utils import popen_platform_kwargs
|
|
202
207
|
self._worker_proc = subprocess.Popen(
|
|
@@ -325,6 +325,7 @@ async def record_feedback(data: dict):
|
|
|
325
325
|
|
|
326
326
|
row_id = feedback.record_dashboard_feedback(
|
|
327
327
|
memory_id=str(memory_id), query=query, feedback_type=feedback_type,
|
|
328
|
+
profile_id=get_active_profile() or "default",
|
|
328
329
|
)
|
|
329
330
|
|
|
330
331
|
return {
|
|
@@ -369,6 +370,7 @@ async def record_dwell(data: dict):
|
|
|
369
370
|
|
|
370
371
|
row_id = feedback.record_dashboard_feedback(
|
|
371
372
|
memory_id=str(memory_id), query=query, feedback_type=feedback_type,
|
|
373
|
+
profile_id=get_active_profile() or "default",
|
|
372
374
|
)
|
|
373
375
|
|
|
374
376
|
return {
|
|
@@ -441,6 +441,17 @@ def _validate_provider_url(url: str, client_host: str) -> str | None:
|
|
|
441
441
|
return "Cloud metadata endpoints are not allowed"
|
|
442
442
|
if client_host in ("127.0.0.1", "::1", "localhost"):
|
|
443
443
|
return None # local dashboard may target its own local/LAN endpoints
|
|
444
|
+
# SLM_REMOTE residue (#40): an allowlisted LAN dashboard is trusted exactly
|
|
445
|
+
# like the loopback one and may probe its own LAN LLM endpoint. This does
|
|
446
|
+
# NOT relax the SSRF guard for arbitrary remote callers —
|
|
447
|
+
# is_lan_client_allowed is False unless remote mode is ON *and* the client
|
|
448
|
+
# IP is in SLM_MCP_ALLOWED_HOSTS.
|
|
449
|
+
try:
|
|
450
|
+
from superlocalmemory.core.remote_mode import is_lan_client_allowed
|
|
451
|
+
if is_lan_client_allowed(client_host):
|
|
452
|
+
return None
|
|
453
|
+
except Exception: # pragma: no cover — defensive, never weaken on import error
|
|
454
|
+
pass
|
|
444
455
|
try:
|
|
445
456
|
ip = ipaddress.ip_address(host)
|
|
446
457
|
except ValueError:
|
|
@@ -171,6 +171,33 @@ from superlocalmemory.core.recall_gate import (
|
|
|
171
171
|
# daemon startup via engine._process_pending_memories().
|
|
172
172
|
_engine = None
|
|
173
173
|
|
|
174
|
+
|
|
175
|
+
def _emit_event(
|
|
176
|
+
event_type: str,
|
|
177
|
+
payload: dict | None = None,
|
|
178
|
+
*,
|
|
179
|
+
source_agent: str = "http_client",
|
|
180
|
+
) -> None:
|
|
181
|
+
"""Emit a best-effort EventBus event from an HTTP write path.
|
|
182
|
+
|
|
183
|
+
Mirrors mcp.shared.emit_event but tags source_protocol="http" so the
|
|
184
|
+
dashboard can distinguish HTTP traffic from MCP tool calls. Never raises
|
|
185
|
+
— a bus failure must not affect the caller's response.
|
|
186
|
+
"""
|
|
187
|
+
try:
|
|
188
|
+
from superlocalmemory.infra.event_bus import EventBus
|
|
189
|
+
from superlocalmemory.server.routes.helpers import DB_PATH
|
|
190
|
+
bus = EventBus.get_instance(DB_PATH)
|
|
191
|
+
bus.emit(
|
|
192
|
+
event_type,
|
|
193
|
+
payload=payload,
|
|
194
|
+
source_agent=source_agent,
|
|
195
|
+
source_protocol="http",
|
|
196
|
+
)
|
|
197
|
+
except Exception as exc:
|
|
198
|
+
logger.debug("EventBus emit failed (%s): %s", event_type, exc)
|
|
199
|
+
|
|
200
|
+
|
|
174
201
|
# v3.4.53: Limit concurrent full (non-fast) recalls. Without this, N parallel
|
|
175
202
|
# /recall calls spawn N × 6-channel threads → Ollama serialises, reranker
|
|
176
203
|
# lock queues, and total wall time is N × single-recall-time. 3 concurrent
|
|
@@ -240,6 +267,14 @@ class ObserveBuffer:
|
|
|
240
267
|
self._timer = threading.Timer(self._debounce_sec, self._flush)
|
|
241
268
|
self._timer.daemon = True
|
|
242
269
|
self._timer.start()
|
|
270
|
+
_emit_event(
|
|
271
|
+
"memory.observed",
|
|
272
|
+
payload={
|
|
273
|
+
"content_hash": content_hash,
|
|
274
|
+
"content_preview": content[:120],
|
|
275
|
+
"buffer_size": buf_size,
|
|
276
|
+
},
|
|
277
|
+
)
|
|
243
278
|
return {"captured": True, "queued": True, "buffer_size": buf_size}
|
|
244
279
|
|
|
245
280
|
def _flush(self) -> None:
|
|
@@ -268,6 +303,22 @@ class ObserveBuffer:
|
|
|
268
303
|
# The prior 'processed N' counted skipped (capture=False)
|
|
269
304
|
# items as successes — a false-positive write count.
|
|
270
305
|
captured_count += 1
|
|
306
|
+
_emit_event(
|
|
307
|
+
"memory.captured",
|
|
308
|
+
payload={
|
|
309
|
+
"category": decision.category,
|
|
310
|
+
"confidence": getattr(decision, "confidence", None),
|
|
311
|
+
"content_preview": content[:120],
|
|
312
|
+
},
|
|
313
|
+
)
|
|
314
|
+
else:
|
|
315
|
+
_emit_event(
|
|
316
|
+
"memory.dropped",
|
|
317
|
+
payload={
|
|
318
|
+
"reason": getattr(decision, "reason", "no patterns matched"),
|
|
319
|
+
"content_preview": content[:120],
|
|
320
|
+
},
|
|
321
|
+
)
|
|
271
322
|
except Exception as exc:
|
|
272
323
|
failed_count += 1
|
|
273
324
|
logger.warning(
|
|
@@ -1784,6 +1835,15 @@ def _register_daemon_routes(application: FastAPI) -> None:
|
|
|
1784
1835
|
req.content, metadata=metadata,
|
|
1785
1836
|
scope=scope, shared_with=shared_with,
|
|
1786
1837
|
)
|
|
1838
|
+
_emit_event(
|
|
1839
|
+
"memory.stored",
|
|
1840
|
+
payload={
|
|
1841
|
+
"fact_ids": list(fact_ids) if fact_ids else [],
|
|
1842
|
+
"count": len(fact_ids) if fact_ids else 0,
|
|
1843
|
+
"path": "remember_sync",
|
|
1844
|
+
"content_preview": req.content[:120],
|
|
1845
|
+
},
|
|
1846
|
+
)
|
|
1787
1847
|
return {"ok": True, "fact_ids": fact_ids, "count": len(fact_ids)}
|
|
1788
1848
|
except Exception as exc:
|
|
1789
1849
|
raise HTTPException(500, detail=str(exc))
|
|
@@ -1823,6 +1883,14 @@ def _register_daemon_routes(application: FastAPI) -> None:
|
|
|
1823
1883
|
pending_id = store_pending(
|
|
1824
1884
|
req.content, tags=req.tags or "", metadata=meta,
|
|
1825
1885
|
)
|
|
1886
|
+
_emit_event(
|
|
1887
|
+
"memory.queued",
|
|
1888
|
+
payload={
|
|
1889
|
+
"pending_id": pending_id,
|
|
1890
|
+
"tags": req.tags or "",
|
|
1891
|
+
"content_preview": req.content[:120],
|
|
1892
|
+
},
|
|
1893
|
+
)
|
|
1826
1894
|
return {
|
|
1827
1895
|
"ok": True,
|
|
1828
1896
|
"fact_ids": fact_ids,
|
|
@@ -2196,6 +2264,16 @@ def _start_pending_materializer() -> None:
|
|
|
2196
2264
|
)
|
|
2197
2265
|
engine.store_fact_direct(fact)
|
|
2198
2266
|
mark_done(item["id"])
|
|
2267
|
+
_emit_event(
|
|
2268
|
+
"memory.stored",
|
|
2269
|
+
payload={
|
|
2270
|
+
"pending_id": item["id"],
|
|
2271
|
+
"memory_id": mem_id,
|
|
2272
|
+
"path": "materializer_drain",
|
|
2273
|
+
"content_preview": content[:120],
|
|
2274
|
+
},
|
|
2275
|
+
source_agent="materializer",
|
|
2276
|
+
)
|
|
2199
2277
|
except Exception as exc:
|
|
2200
2278
|
logger.warning(
|
|
2201
2279
|
"Pending %d failed: %s", item["id"], exc,
|
|
@@ -12,7 +12,7 @@ Part of Qualixar | Author: Varun Pratap Bhardwaj
|
|
|
12
12
|
"""
|
|
13
13
|
from __future__ import annotations
|
|
14
14
|
|
|
15
|
-
import json, logging, sqlite3, threading, time
|
|
15
|
+
import json, logging, os, sqlite3, threading, time
|
|
16
16
|
from contextlib import contextmanager
|
|
17
17
|
from pathlib import Path
|
|
18
18
|
from types import ModuleType
|
|
@@ -27,10 +27,16 @@ from superlocalmemory.storage.models import (
|
|
|
27
27
|
|
|
28
28
|
logger = logging.getLogger(__name__)
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
_MISSING = object()
|
|
31
|
+
|
|
32
|
+
def _jl(raw: Any, default: Any = _MISSING) -> Any:
|
|
33
|
+
"""JSON-load a value, returning *default* on None/empty.
|
|
34
|
+
|
|
35
|
+
_jl(raw) -> [] when raw is None/empty (list fields)
|
|
36
|
+
_jl(raw, None) -> None when raw is None/empty (optional fields)
|
|
37
|
+
"""
|
|
32
38
|
if raw is None or raw == "":
|
|
33
|
-
return
|
|
39
|
+
return [] if default is _MISSING else default
|
|
34
40
|
return json.loads(raw)
|
|
35
41
|
|
|
36
42
|
def _jd(val: Any) -> str | None:
|
|
@@ -38,9 +44,30 @@ def _jd(val: Any) -> str | None:
|
|
|
38
44
|
return json.dumps(val) if val is not None else None
|
|
39
45
|
|
|
40
46
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
47
|
+
def _env_int(name: str, default: int) -> int:
|
|
48
|
+
"""Read a positive int from the environment, falling back on bad/absent."""
|
|
49
|
+
try:
|
|
50
|
+
val = int(os.environ.get(name, "").strip())
|
|
51
|
+
return val if val > 0 else default
|
|
52
|
+
except (ValueError, AttributeError):
|
|
53
|
+
return default
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _env_float(name: str, default: float) -> float:
|
|
57
|
+
"""Read a positive float from the environment, falling back on bad/absent."""
|
|
58
|
+
try:
|
|
59
|
+
val = float(os.environ.get(name, "").strip())
|
|
60
|
+
return val if val > 0 else default
|
|
61
|
+
except (ValueError, AttributeError):
|
|
62
|
+
return default
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
# SQLite endurance tuning. Defaults preserve prior hard-coded behaviour exactly;
|
|
66
|
+
# operators on slow/contended I/O can raise them via env (issue #53) without a
|
|
67
|
+
# code change. Unset env => byte-identical to the previous constants.
|
|
68
|
+
_BUSY_TIMEOUT_MS = _env_int("SLM_DB_BUSY_TIMEOUT_MS", 10_000) # wait for writers
|
|
69
|
+
_MAX_RETRIES = _env_int("SLM_DB_MAX_RETRIES", 5) # retry on SQLITE_BUSY
|
|
70
|
+
_RETRY_BASE_DELAY = _env_float("SLM_DB_RETRY_BASE_DELAY", 0.1) # backoff base (s)
|
|
44
71
|
|
|
45
72
|
|
|
46
73
|
def _scope_where(
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: superlocalmemory
|
|
3
|
-
Version: 3.6.
|
|
3
|
+
Version: 3.6.17
|
|
4
4
|
Summary: Information-geometric agent memory with mathematical guarantees
|
|
5
5
|
Author-email: Varun Pratap Bhardwaj <admin@superlocalmemory.com>
|
|
6
6
|
License: AGPL-3.0-or-later
|
|
@@ -96,10 +96,10 @@ Dynamic: license-file
|
|
|
96
96
|
<img src="https://superlocalmemory.com/assets/logo-mark.png" alt="SuperLocalMemory" width="200"/>
|
|
97
97
|
</p>
|
|
98
98
|
|
|
99
|
-
<h1 align="center">SuperLocalMemory V3.6.
|
|
99
|
+
<h1 align="center">SuperLocalMemory V3.6.17</h1>
|
|
100
100
|
<p align="center"><strong>Cache. Compress. Remember. Three surfaces — proxy, MCP tools, or skill. Every setup covered.</strong><br/>
|
|
101
101
|
<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>
|
|
102
|
-
<p align="center"><code>v3.6.
|
|
102
|
+
<p align="center"><code>v3.6.17</code> — <strong>Plugin-native. Profile-aware. Distributed-ready.</strong><br/>
|
|
103
103
|
Proxy: <code>slm wrap claude</code> · MCP: add <code>slm_compress</code> to your config · Skill: zero-config</p>
|
|
104
104
|
<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>
|
|
105
105
|
|
|
@@ -256,7 +256,7 @@ Full docs: [docs/multi-machine.md](docs/multi-machine.md) · [docs/distributed-d
|
|
|
256
256
|
|:-----|:--------|:-----|
|
|
257
257
|
| **npm** (recommended) | `npm install -g superlocalmemory` | Node 14+, installs Python deps automatically |
|
|
258
258
|
| **pip** | `pip install superlocalmemory` | Python 3.11+, direct install |
|
|
259
|
-
| **Claude Code Plugin** (WP-06) | `/plugin install superlocalmemory@qualixar` | Self-bootstraps venv, isolated SLM_DATA_DIR, additive — 14-tool core |
|
|
259
|
+
| **Claude Code Plugin** (WP-06) | `/plugin marketplace add qualixar/superlocalmemory` then `/plugin install superlocalmemory@qualixar` | Self-bootstraps venv, isolated SLM_DATA_DIR, additive — 14-tool core. Ships the skills/agents/hooks/commands |
|
|
260
260
|
| **Portable / IDE connect** (WP-08) | `slm connect <ide> [--here]` | Wire any IDE without reinstalling; `slm connect claude-code` → plugin pointer |
|
|
261
261
|
|
|
262
262
|
After any install path: `slm setup` → `slm doctor` → `slm warmup` (optional, pre-downloads ~500MB embedding model).
|
|
@@ -312,18 +312,31 @@ Per-IDE configs available for Claude Code, Cursor, Windsurf, VS Code Copilot, Co
|
|
|
312
312
|
|
|
313
313
|
## Claude Code Plugin
|
|
314
314
|
|
|
315
|
-
Install directly in Claude Code
|
|
315
|
+
Install directly in Claude Code — no system-level npm/pip needed. This is how you
|
|
316
|
+
get the **skills, agents, hooks, commands, and rules** (the MCP server is
|
|
317
|
+
bootstrapped automatically). It is a two-step flow — add the marketplace once,
|
|
318
|
+
then install:
|
|
316
319
|
|
|
317
320
|
```bash
|
|
321
|
+
# 1. Add the Qualixar marketplace (one-time — the repo IS the marketplace)
|
|
322
|
+
/plugin marketplace add qualixar/superlocalmemory
|
|
323
|
+
|
|
324
|
+
# 2. Install the plugin
|
|
318
325
|
/plugin install superlocalmemory@qualixar
|
|
319
326
|
```
|
|
320
327
|
|
|
321
328
|
- Self-bootstraps a Python venv, installs all deps in an isolated `SLM_DATA_DIR`
|
|
322
|
-
- Registers 14-tool core MCP surface (`core14` profile by default)
|
|
329
|
+
- Registers the 14-tool core MCP surface (`core14` profile by default)
|
|
330
|
+
- Ships the SLM skills / agents / hooks / commands / rules
|
|
323
331
|
- Additive — does not replace an existing SLM install
|
|
324
332
|
- `slm connect claude-code` detects an existing plugin install and links them
|
|
325
333
|
|
|
326
|
-
|
|
334
|
+
> **Plugin vs `pip`/`npm`:** `pip install superlocalmemory` / `npm i -g superlocalmemory`
|
|
335
|
+
> give you the `slm` CLI + the MCP server (the *tools*). The **skills/agents/hooks/
|
|
336
|
+
> commands** come only through the plugin above. Use the plugin for Claude Code; use
|
|
337
|
+
> pip/npm for the CLI or other IDEs.
|
|
338
|
+
|
|
339
|
+
To update later: `/plugin marketplace update qualixar` then `/plugin install superlocalmemory@qualixar`.
|
|
327
340
|
|
|
328
341
|
---
|
|
329
342
|
|
|
@@ -388,6 +401,8 @@ slm dashboard # Opens at http://localhost:8765
|
|
|
388
401
|
|
|
389
402
|
| Version | Codename | Key Features |
|
|
390
403
|
|---|---|---|
|
|
404
|
+
| **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) |
|
|
405
|
+
| **v3.6.16** | Docs | Corrected Claude Code plugin install — adds the required `/plugin marketplace add` step; clarifies plugin vs pip/npm delivery |
|
|
391
406
|
| **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 |
|
|
392
407
|
| **v3.6.14** | Plugin-native | Claude Code Plugin (WP-06), MCP profiles (WP-01), IDE connect (WP-08), asset consolidation, UI polish (WP-12) |
|
|
393
408
|
| **v3.6.x** | Optimize Everywhere / Distributed-ready | Three surfaces (proxy/MCP/skill), `SLM_REMOTE=1` LAN mode, remote dashboard, custom LLM endpoints |
|
|
@@ -177,6 +177,7 @@ src/superlocalmemory/hooks/cursor_adapter.py
|
|
|
177
177
|
src/superlocalmemory/hooks/hook_daemon.py
|
|
178
178
|
src/superlocalmemory/hooks/hook_handlers.py
|
|
179
179
|
src/superlocalmemory/hooks/ide_connector.py
|
|
180
|
+
src/superlocalmemory/hooks/memory_protocol.py
|
|
180
181
|
src/superlocalmemory/hooks/portable_kit.py
|
|
181
182
|
src/superlocalmemory/hooks/post_tool_async_hook.py
|
|
182
183
|
src/superlocalmemory/hooks/post_tool_outcome_hook.py
|