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.
Files changed (53) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +21 -6
  3. package/package.json +1 -1
  4. package/plugin/.claude-plugin/plugin.json +1 -1
  5. package/plugin/CLAUDE.md +3 -3
  6. package/plugin/agents/slm-memory-advisor.md +1 -1
  7. package/plugin/agents/slm-optimize-advisor.md +1 -1
  8. package/plugin/requirements.txt +1 -1
  9. package/plugin/skills/slm-cache/SKILL.md +1 -1
  10. package/plugin/skills/slm-compress/SKILL.md +1 -1
  11. package/plugin/skills/slm-graph/SKILL.md +1 -1
  12. package/plugin/skills/slm-recall/SKILL.md +1 -1
  13. package/plugin/skills/slm-remember/SKILL.md +1 -1
  14. package/plugin/skills/slm-session/SKILL.md +1 -1
  15. package/plugin/skills/slm-status/SKILL.md +1 -1
  16. package/plugin-src/agents/slm-memory-advisor.md +1 -1
  17. package/plugin-src/agents/slm-optimize-advisor.md +1 -1
  18. package/plugin-src/commands/slm-optimize.md +1 -1
  19. package/plugin-src/commands/slm-recall.md +1 -1
  20. package/plugin-src/commands/slm-remember.md +1 -1
  21. package/plugin-src/commands/slm-status.md +1 -1
  22. package/plugin-src/manifest.json +1 -1
  23. package/plugin-src/requirements.txt +1 -1
  24. package/plugin-src/rules/AGENTS.md +1 -1
  25. package/plugin-src/rules/CLAUDE.md.fragment +3 -3
  26. package/plugin-src/skills/slm-cache/SKILL.md +1 -1
  27. package/plugin-src/skills/slm-compress/SKILL.md +1 -1
  28. package/plugin-src/skills/slm-graph/SKILL.md +1 -1
  29. package/plugin-src/skills/slm-recall/SKILL.md +1 -1
  30. package/plugin-src/skills/slm-remember/SKILL.md +1 -1
  31. package/plugin-src/skills/slm-session/SKILL.md +1 -1
  32. package/plugin-src/skills/slm-status/SKILL.md +1 -1
  33. package/pyproject.toml +1 -1
  34. package/scripts/build-plugin.js +1 -1
  35. package/src/superlocalmemory/__init__.py +1 -1
  36. package/src/superlocalmemory/core/embeddings.py +5 -0
  37. package/src/superlocalmemory/dynamics/fisher_langevin_coupling.py +4 -0
  38. package/src/superlocalmemory/hooks/adapter_base.py +10 -3
  39. package/src/superlocalmemory/hooks/copilot_adapter.py +78 -9
  40. package/src/superlocalmemory/hooks/hook_handlers.py +26 -1
  41. package/src/superlocalmemory/hooks/memory_protocol.py +102 -0
  42. package/src/superlocalmemory/hooks/post_tool_async_hook.py +23 -5
  43. package/src/superlocalmemory/infra/event_bus.py +4 -0
  44. package/src/superlocalmemory/learning/feedback.py +59 -0
  45. package/src/superlocalmemory/learning/outcome_queue.py +10 -2
  46. package/src/superlocalmemory/llm/backbone.py +8 -1
  47. package/src/superlocalmemory/retrieval/reranker.py +5 -0
  48. package/src/superlocalmemory/server/routes/learning.py +2 -0
  49. package/src/superlocalmemory/server/routes/v3_api.py +11 -0
  50. package/src/superlocalmemory/server/unified_daemon.py +78 -0
  51. package/src/superlocalmemory/storage/database.py +34 -7
  52. package/src/superlocalmemory.egg-info/PKG-INFO +22 -7
  53. 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.15</h1>
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.15</code> — <strong>Plugin-native. Profile-aware. Distributed-ready.</strong><br/>
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> &nbsp;·&nbsp; MCP: add <code>slm_compress</code> to your config &nbsp;·&nbsp; 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 without a system-level npm/pip install:
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
- See [docs/getting-started.md](docs/getting-started.md) for full plugin walkthrough.
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.15",
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",
@@ -16,5 +16,5 @@
16
16
  "mcpServers": "./.mcp.json",
17
17
  "name": "superlocalmemory",
18
18
  "repository": "https://github.com/qualixar/superlocalmemory",
19
- "version": "3.6.15"
19
+ "version": "3.6.17"
20
20
  }
package/plugin/CLAUDE.md CHANGED
@@ -1,4 +1,4 @@
1
- <!-- BEGIN SuperLocalMemory v3.6.15 -->
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.15 -->
42
+ <!-- END SuperLocalMemory v3.6.17 -->
43
43
 
44
- SuperLocalMemory v3.6.15 · Qualixar · AGPL-3.0-or-later
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.15 · Qualixar · AGPL-3.0-or-later
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.15 · Qualixar · AGPL-3.0-or-later
38
+ SuperLocalMemory v3.6.17 · Qualixar · AGPL-3.0-or-later
@@ -1 +1 @@
1
- superlocalmemory==3.6.15
1
+ superlocalmemory==3.6.17
@@ -137,4 +137,4 @@ These subcommands control daemon-level cache settings. They do not read or write
137
137
 
138
138
  ---
139
139
 
140
- SuperLocalMemory v3.6.15 · Qualixar · AGPL-3.0-or-later
140
+ SuperLocalMemory v3.6.17 · Qualixar · AGPL-3.0-or-later
@@ -140,4 +140,4 @@ Content over 1 MB (1 000 000 bytes UTF-8) is processed but `reversible` is force
140
140
 
141
141
  ---
142
142
 
143
- SuperLocalMemory v3.6.15 · Qualixar · AGPL-3.0-or-later
143
+ SuperLocalMemory v3.6.17 · Qualixar · AGPL-3.0-or-later
@@ -297,4 +297,4 @@ Without this, the six graph tools are not registered and will appear as unknown
297
297
 
298
298
  ---
299
299
 
300
- SuperLocalMemory v3.6.15 · Qualixar · AGPL-3.0-or-later
300
+ SuperLocalMemory v3.6.17 · Qualixar · AGPL-3.0-or-later
@@ -201,4 +201,4 @@ trusts that what you surface came from the store.
201
201
 
202
202
  ---
203
203
 
204
- *SuperLocalMemory v3.6.15 · Qualixar · AGPL-3.0-or-later*
204
+ *SuperLocalMemory v3.6.17 · Qualixar · AGPL-3.0-or-later*
@@ -191,4 +191,4 @@ slm remember "<content>" --scope shared --shared-with alice,bob
191
191
 
192
192
  ---
193
193
 
194
- *SuperLocalMemory v3.6.15 · Qualixar · AGPL-3.0-or-later*
194
+ *SuperLocalMemory v3.6.17 · Qualixar · AGPL-3.0-or-later*
@@ -204,4 +204,4 @@ slm doctor [--json] # preflight check including daemon and embedding worker
204
204
 
205
205
  ---
206
206
 
207
- *SuperLocalMemory v3.6.15 · Qualixar · AGPL-3.0-or-later*
207
+ *SuperLocalMemory v3.6.17 · Qualixar · AGPL-3.0-or-later*
@@ -146,4 +146,4 @@ Note: the `slm optimize` subcommands have known pre-existing parse-test failures
146
146
 
147
147
  ---
148
148
 
149
- SuperLocalMemory v3.6.15 · Qualixar · AGPL-3.0-or-later
149
+ 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.15 · Qualixar · AGPL-3.0-or-later
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.15 · Qualixar · AGPL-3.0-or-later
38
+ SuperLocalMemory v3.6.17 · Qualixar · AGPL-3.0-or-later
@@ -19,4 +19,4 @@ Rules (apply strictly):
19
19
 
20
20
  MCP unavailable → CLI fallback: `slm optimize status`.
21
21
 
22
- SuperLocalMemory v3.6.15 · Qualixar · AGPL-3.0-or-later
22
+ SuperLocalMemory v3.6.17 · Qualixar · AGPL-3.0-or-later
@@ -13,4 +13,4 @@ Recall from SuperLocalMemory using the query: $ARGUMENTS
13
13
  - `slm recall "$ARGUMENTS" --limit 10`
14
14
  - then `slm search "$ARGUMENTS"`
15
15
 
16
- SuperLocalMemory v3.6.15 · Qualixar · AGPL-3.0-or-later
16
+ 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.15 · Qualixar · AGPL-3.0-or-later
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 · Qualixar · AGPL-3.0-or-later
15
+ SuperLocalMemory v3.6.17 · Qualixar · AGPL-3.0-or-later
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "3.6.15",
2
+ "version": "3.6.17",
3
3
  "pluginName": "superlocalmemory",
4
4
  "displayName": "SuperLocalMemory",
5
5
  "repository": "https://github.com/qualixar/superlocalmemory",
@@ -1 +1 @@
1
- superlocalmemory==3.6.15
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.15 · Qualixar · AGPL-3.0-or-later
91
+ SuperLocalMemory v3.6.17 · Qualixar · AGPL-3.0-or-later
@@ -1,4 +1,4 @@
1
- <!-- BEGIN SuperLocalMemory v3.6.15 -->
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.15 -->
42
+ <!-- END SuperLocalMemory v3.6.17 -->
43
43
 
44
- SuperLocalMemory v3.6.15 · Qualixar · AGPL-3.0-or-later
44
+ SuperLocalMemory v3.6.17 · Qualixar · AGPL-3.0-or-later
@@ -137,4 +137,4 @@ These subcommands control daemon-level cache settings. They do not read or write
137
137
 
138
138
  ---
139
139
 
140
- SuperLocalMemory v3.6.15 · Qualixar · AGPL-3.0-or-later
140
+ SuperLocalMemory v3.6.17 · Qualixar · AGPL-3.0-or-later
@@ -140,4 +140,4 @@ Content over 1 MB (1 000 000 bytes UTF-8) is processed but `reversible` is force
140
140
 
141
141
  ---
142
142
 
143
- SuperLocalMemory v3.6.15 · Qualixar · AGPL-3.0-or-later
143
+ SuperLocalMemory v3.6.17 · Qualixar · AGPL-3.0-or-later
@@ -297,4 +297,4 @@ Without this, the six graph tools are not registered and will appear as unknown
297
297
 
298
298
  ---
299
299
 
300
- SuperLocalMemory v3.6.15 · Qualixar · AGPL-3.0-or-later
300
+ SuperLocalMemory v3.6.17 · Qualixar · AGPL-3.0-or-later
@@ -201,4 +201,4 @@ trusts that what you surface came from the store.
201
201
 
202
202
  ---
203
203
 
204
- *SuperLocalMemory v3.6.15 · Qualixar · AGPL-3.0-or-later*
204
+ *SuperLocalMemory v3.6.17 · Qualixar · AGPL-3.0-or-later*
@@ -191,4 +191,4 @@ slm remember "<content>" --scope shared --shared-with alice,bob
191
191
 
192
192
  ---
193
193
 
194
- *SuperLocalMemory v3.6.15 · Qualixar · AGPL-3.0-or-later*
194
+ *SuperLocalMemory v3.6.17 · Qualixar · AGPL-3.0-or-later*
@@ -204,4 +204,4 @@ slm doctor [--json] # preflight check including daemon and embedding worker
204
204
 
205
205
  ---
206
206
 
207
- *SuperLocalMemory v3.6.15 · Qualixar · AGPL-3.0-or-later*
207
+ *SuperLocalMemory v3.6.17 · Qualixar · AGPL-3.0-or-later*
@@ -146,4 +146,4 @@ Note: the `slm optimize` subcommands have known pre-existing parse-test failures
146
146
 
147
147
  ---
148
148
 
149
- SuperLocalMemory v3.6.15 · Qualixar · AGPL-3.0-or-later
149
+ SuperLocalMemory v3.6.17 · Qualixar · AGPL-3.0-or-later
package/pyproject.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "superlocalmemory"
3
- version = "3.6.15"
3
+ version = "3.6.17"
4
4
  description = "Information-geometric agent memory with mathematical guarantees"
5
5
  readme = "README.md"
6
6
  license = {text = "AGPL-3.0-or-later"}
@@ -30,7 +30,7 @@ import process from 'node:process';
30
30
  // ---------------------------------------------------------------------------
31
31
  // Constants
32
32
  // ---------------------------------------------------------------------------
33
- const VERSION = '3.6.15';
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.15"
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 no write, no new sync-log row (the prior row still
221
- # reflects on-disk truth).
222
- return WriteResult(wrote=False, bytes_written=0, content_sha256=new_hash)
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
- return _BODY_TEMPLATE.format(
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
- ).encode("utf-8")
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, rendered,
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.unlink()
143
- except OSError: # pragma: no cover
144
- pass
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
- _DAEMON_URL = "http://127.0.0.1:8765"
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 "http://127.0.0.1:8765"
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 "http://127.0.0.1:8765"
68
+ return _port_file_url()
51
69
  if parsed.scheme not in ("http", "https"):
52
- return "http://127.0.0.1:8765"
70
+ return _port_file_url()
53
71
  host = (parsed.hostname or "").lower()
54
72
  if host not in _ALLOWED_DAEMON_HOSTS:
55
- return "http://127.0.0.1:8765"
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
- while not _stop_event.wait(interval_s):
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
- return _ANTHROPIC_URL, headers, payload
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
- def _jl(raw: Any, default: Any = None) -> Any:
31
- """JSON-load a value, returning *default* on None/empty."""
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 default if default is not None else []
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
- _BUSY_TIMEOUT_MS = 10_000 # 10 seconds wait for other writers
42
- _MAX_RETRIES = 5 # retry on transient SQLITE_BUSY
43
- _RETRY_BASE_DELAY = 0.1 # seconds — exponential backoff base
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.15
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.15</h1>
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.15</code> — <strong>Plugin-native. Profile-aware. Distributed-ready.</strong><br/>
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> &nbsp;·&nbsp; MCP: add <code>slm_compress</code> to your config &nbsp;·&nbsp; 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 without a system-level npm/pip install:
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
- See [docs/getting-started.md](docs/getting-started.md) for full plugin walkthrough.
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