superlocalmemory 4.1.8 → 4.1.11

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 (49) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/CHANGELOG.md +35 -0
  3. package/README.md +3 -3
  4. package/package.json +1 -1
  5. package/plugin/.claude-plugin/plugin.json +1 -1
  6. package/plugin/CLAUDE.md +3 -3
  7. package/plugin/agents/slm-governance-advisor.md +1 -1
  8. package/plugin/agents/slm-loop-runner.md +1 -1
  9. package/plugin/agents/slm-memory-advisor.md +1 -1
  10. package/plugin/agents/slm-optimize-advisor.md +1 -1
  11. package/plugin/requirements.txt +1 -1
  12. package/plugin/skills/slm-cache/SKILL.md +1 -1
  13. package/plugin/skills/slm-compress/SKILL.md +1 -1
  14. package/plugin/skills/slm-governance/SKILL.md +1 -1
  15. package/plugin/skills/slm-graph/SKILL.md +1 -1
  16. package/plugin/skills/slm-loop/SKILL.md +1 -1
  17. package/plugin/skills/slm-mesh/SKILL.md +1 -1
  18. package/plugin/skills/slm-profile/SKILL.md +1 -1
  19. package/plugin/skills/slm-recall/SKILL.md +1 -1
  20. package/plugin/skills/slm-remember/SKILL.md +1 -1
  21. package/plugin/skills/slm-scope/SKILL.md +1 -1
  22. package/plugin/skills/slm-session/SKILL.md +1 -1
  23. package/plugin/skills/slm-status/SKILL.md +1 -1
  24. package/plugin-src/agents/slm-memory-advisor.md +1 -1
  25. package/plugin-src/agents/slm-optimize-advisor.md +1 -1
  26. package/plugin-src/rules/AGENTS.md +1 -1
  27. package/plugin-src/skills/slm-cache/SKILL.md +1 -1
  28. package/plugin-src/skills/slm-compress/SKILL.md +1 -1
  29. package/plugin-src/skills/slm-governance/SKILL.md +1 -1
  30. package/plugin-src/skills/slm-graph/SKILL.md +1 -1
  31. package/plugin-src/skills/slm-loop/SKILL.md +1 -1
  32. package/plugin-src/skills/slm-mesh/SKILL.md +1 -1
  33. package/plugin-src/skills/slm-profile/SKILL.md +1 -1
  34. package/plugin-src/skills/slm-recall/SKILL.md +1 -1
  35. package/plugin-src/skills/slm-remember/SKILL.md +1 -1
  36. package/plugin-src/skills/slm-scope/SKILL.md +1 -1
  37. package/plugin-src/skills/slm-session/SKILL.md +1 -1
  38. package/plugin-src/skills/slm-status/SKILL.md +1 -1
  39. package/pyproject.toml +1 -1
  40. package/src/superlocalmemory/__init__.py +1 -1
  41. package/src/superlocalmemory/cli/daemon.py +18 -0
  42. package/src/superlocalmemory/learning/reward.py +39 -4
  43. package/src/superlocalmemory/loops/__init__.py +2 -0
  44. package/src/superlocalmemory/loops/ledger.py +35 -0
  45. package/src/superlocalmemory/mcp/_daemon_proxy.py +27 -8
  46. package/src/superlocalmemory/mcp/server.py +6 -1
  47. package/src/superlocalmemory/mcp/tools_loops.py +51 -17
  48. package/src/superlocalmemory/server/unified_daemon.py +17 -0
  49. package/src/superlocalmemory/ui/index.html +2 -2
@@ -21,7 +21,7 @@
21
21
  "license": "AGPL-3.0-or-later",
22
22
  "name": "superlocalmemory",
23
23
  "source": "./plugin",
24
- "version": "4.1.8"
24
+ "version": "4.1.11"
25
25
  },
26
26
  {
27
27
  "author": {
@@ -39,7 +39,7 @@
39
39
  "license": "AGPL-3.0-or-later",
40
40
  "name": "superlocalmemory-codex",
41
41
  "source": "./codex-plugin",
42
- "version": "4.1.8"
42
+ "version": "4.1.11"
43
43
  }
44
44
  ]
45
45
  }
package/CHANGELOG.md CHANGED
@@ -5,6 +5,41 @@ All notable changes to SuperLocalMemory 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
+ ## [4.1.11] — MCP loops use the owned runtime
9
+
10
+ ### Fixed
11
+ - MCP bounded loops now route both recall gates and ledger writes through the
12
+ daemon, and normalize ledger tags at the HTTP boundary.
13
+
14
+ ## [4.1.10] — MCP loops use the owned writer
15
+
16
+ ### Fixed
17
+ - MCP bounded loops now persist their ledger through the daemon/worker writer
18
+ instead of calling a write method on the intentionally LIGHT MCP engine, and
19
+ reject stale cross-profile writes rather than misplacing audit records.
20
+ - The benchmark bundle now runs experiments 12 and 13; the emergency-recall
21
+ fixture no longer expires as the calendar advances, and trial totals reflect
22
+ every executed ablation round.
23
+
24
+ ## [4.1.9] — Silence is not a verdict
25
+
26
+ ### Fixed
27
+ - **A recall nobody engaged with was scored as an average one.** How useful a recall
28
+ was is worked out from what happened next — whether a memory was quoted, edited, or
29
+ asked about again. When nothing at all had happened, that calculation still landed
30
+ on a middling score, and it was saved as if it had been measured. That is not a
31
+ blank: downstream it reads as mild approval, nudging results toward whatever was
32
+ shown and making the system steadily more certain it has no preference either way.
33
+ Recalls with nothing observed are now closed without a score, which is what the
34
+ batch pass already did — the two paths simply disagreed. On a store in daily use,
35
+ every one of 492 saved scores turned out to be this value, so nothing recorded there
36
+ reflected real use.
37
+ - **Signals recorded as explicitly absent were counted as present.** A recall whose
38
+ signals were all recorded as "did not happen" was treated as having something to
39
+ say, and scored, because the record existed even though everything in it was empty.
40
+ Both paths now ask whether anything actually happened rather than whether a record
41
+ was written.
42
+
8
43
  ## [4.1.8] — The same question, answered the same way
9
44
 
10
45
  ### Fixed
package/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
  </picture>
6
6
  </p>
7
7
 
8
- <h1 align="center">SuperLocalMemory V4.1.8</h1>
8
+ <h1 align="center">SuperLocalMemory V4.1.11</h1>
9
9
 
10
10
  <h2 align="center">Rent the LLM. Own the memory.</h2>
11
11
 
@@ -27,12 +27,12 @@ guarantee here is stated as a falsifiable invariant, tested under an adversarial
27
27
  negative control, and shipped with the harness that regenerates the evidence:
28
28
  <code>python benchmark/run_all.py --trials 200 --output-dir results/</code>. What each experiment
29
29
  does <em>not</em> exercise is stated too.</p>
30
- <p align="center"><code>v4.1.8</code> — one control plane: <strong>SLM-Mesh</strong> peer coordination · multi-scope memory (personal / shared / global) · profiles · Cache · Compress · 7-layer retrieval · code graph · Entity Explorer · skill evolution · Modes A/B/C · GDPR retention &amp; audit chain · bounded loops — across CLI, MCP, dashboard, the <strong>Claude plugin</strong>, the <strong>Codex add-on</strong>, and documented IDE integrations.<br/>
30
+ <p align="center"><code>v4.1.11</code> — one control plane: <strong>SLM-Mesh</strong> peer coordination · multi-scope memory (personal / shared / global) · profiles · Cache · Compress · 7-layer retrieval · code graph · Entity Explorer · skill evolution · Modes A/B/C · GDPR retention &amp; audit chain · bounded loops — across CLI, MCP, dashboard, the <strong>Claude plugin</strong>, the <strong>Codex add-on</strong>, and documented IDE integrations.<br/>
31
31
  Proxy: <code>slm wrap claude</code> &nbsp;·&nbsp; MCP: add <code>slm_compress</code> to your config &nbsp;·&nbsp; Skill: zero-config</p>
32
32
  <p align="center"><strong>Four public arXiv preprints</strong> · V4: <a href="https://arxiv.org/abs/2608.08253">arXiv:2608.08253</a> · companion archive: <a href="https://zenodo.org/records/21853302">Zenodo 21853302</a> (<a href="https://doi.org/10.5281/zenodo.21853302">DOI 10.5281/zenodo.21853302</a>) · prior preprints: <a href="https://arxiv.org/abs/2603.02240">2603.02240</a> · <a href="https://arxiv.org/abs/2603.14588">2603.14588</a> · <a href="https://arxiv.org/abs/2604.04514">2604.04514</a>.</p>
33
33
 
34
34
  <p align="center">
35
- <a href="CHANGELOG.md"><img src="https://img.shields.io/badge/v4.1.8-Current_Release-2ea44f?style=for-the-badge&logo=checkmarx&logoColor=white" alt="v4.1.8 — Current Release"/></a>
35
+ <a href="CHANGELOG.md"><img src="https://img.shields.io/badge/v4.1.11-Current_Release-2ea44f?style=for-the-badge&logo=checkmarx&logoColor=white" alt="v4.1.11 — Current Release"/></a>
36
36
  <a href="https://arxiv.org/abs/2608.08253"><img src="https://img.shields.io/badge/arXiv-2608.08253-b31b1b?style=for-the-badge&logo=arxiv&logoColor=white" alt="SuperLocalMemory 4.0 paper on arXiv:2608.08253"/></a>
37
37
  <a href="https://zenodo.org/records/21853302"><img src="https://img.shields.io/badge/Zenodo-10.5281%2Fzenodo.21853302-1682D4?style=for-the-badge&logo=zenodo&logoColor=white" alt="V4 paper on Zenodo: 10.5281/zenodo.21853302"/></a>
38
38
  <a href="https://arxiv.org/abs/2603.14588"><img src="https://img.shields.io/badge/arXiv-2603.14588-b31b1b?style=for-the-badge&logo=arxiv&logoColor=white" alt="arXiv Paper"/></a>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "superlocalmemory",
3
- "version": "4.1.8",
3
+ "version": "4.1.11",
4
4
  "description": "Local-first agent memory with MCP and an agent-native CLI. Documented clients include Claude Code, Cursor, and Windsurf.",
5
5
  "keywords": [
6
6
  "ai-memory",
@@ -15,5 +15,5 @@
15
15
  "mcpServers": "./.mcp.json",
16
16
  "name": "superlocalmemory",
17
17
  "repository": "https://github.com/qualixar/superlocalmemory",
18
- "version": "4.1.8"
18
+ "version": "4.1.11"
19
19
  }
package/plugin/CLAUDE.md CHANGED
@@ -1,4 +1,4 @@
1
- <!-- BEGIN SuperLocalMemory v4.1.8 -->
1
+ <!-- BEGIN SuperLocalMemory v4.1.11 -->
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, scope/profile guidance) · slm-optimize-advisor (context compression + KV cache) · slm-governance-advisor (scope/roles/compliance/GDPR)
41
41
 
42
- <!-- END SuperLocalMemory v4.1.8 -->
42
+ <!-- END SuperLocalMemory v4.1.11 -->
43
43
 
44
- SuperLocalMemory v4.1.8 · Qualixar · AGPL-3.0-or-later
44
+ SuperLocalMemory v4.1.11 · Qualixar · AGPL-3.0-or-later
@@ -77,4 +77,4 @@ slm-scope · slm-governance · slm-profile · slm-remember · slm-recall
77
77
  # What NOT to do
78
78
  Never session_init twice; never forget without dry-run preview; never store secrets; never bypass role checks; never claim an erasure succeeded without verifying via recall.
79
79
 
80
- SuperLocalMemory v4.1.8 · Qualixar · AGPL-3.0-or-later
80
+ SuperLocalMemory v4.1.11 · Qualixar · AGPL-3.0-or-later
@@ -68,4 +68,4 @@ assessment. The gate is the authority.
68
68
 
69
69
  ---
70
70
 
71
- SuperLocalMemory v4.1.8 · Qualixar · AGPL-3.0-or-later
71
+ SuperLocalMemory v4.1.11 · Qualixar · AGPL-3.0-or-later
@@ -46,4 +46,4 @@ slm-recall · slm-remember · slm-session · slm-scope · slm-profile · slm-gov
46
46
  # What NOT to do
47
47
  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; never bypass scope or governance restrictions.
48
48
 
49
- SuperLocalMemory v4.1.8 · Qualixar · AGPL-3.0-or-later
49
+ SuperLocalMemory v4.1.11 · Qualixar · AGPL-3.0-or-later
@@ -41,4 +41,4 @@ slm-compress · slm-cache · slm-status · slm-profile
41
41
  # What NOT to do
42
42
  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 %; never carry ccr_ids across profile switches.
43
43
 
44
- SuperLocalMemory v4.1.8 · Qualixar · AGPL-3.0-or-later
44
+ SuperLocalMemory v4.1.11 · Qualixar · AGPL-3.0-or-later
@@ -1 +1 @@
1
- superlocalmemory==4.1.8
1
+ superlocalmemory==4.1.11
@@ -145,4 +145,4 @@ These subcommands control daemon-level cache settings. They do not read or write
145
145
 
146
146
  ---
147
147
 
148
- SuperLocalMemory v4.1.8 · Qualixar · AGPL-3.0-or-later
148
+ SuperLocalMemory v4.1.11 · Qualixar · AGPL-3.0-or-later
@@ -147,4 +147,4 @@ Content over 1 MB (1 000 000 bytes UTF-8) is processed but `reversible` is force
147
147
 
148
148
  ---
149
149
 
150
- SuperLocalMemory v4.1.8 · Qualixar · AGPL-3.0-or-later
150
+ SuperLocalMemory v4.1.11 · Qualixar · AGPL-3.0-or-later
@@ -245,4 +245,4 @@ Before running any destructive operation (`forget`, `compact_memories`):
245
245
 
246
246
  ---
247
247
 
248
- *SuperLocalMemory v4.1.8 · Qualixar · AGPL-3.0-or-later*
248
+ *SuperLocalMemory v4.1.11 · Qualixar · AGPL-3.0-or-later*
@@ -312,4 +312,4 @@ profile. See `slm-profile` for the full profile switching workflow.
312
312
 
313
313
  ---
314
314
 
315
- SuperLocalMemory v4.1.8 · Qualixar · AGPL-3.0-or-later
315
+ SuperLocalMemory v4.1.11 · Qualixar · AGPL-3.0-or-later
@@ -96,4 +96,4 @@ paused, name the approval needed; when errored, quote the short detail.
96
96
 
97
97
  ---
98
98
 
99
- SuperLocalMemory v4.1.8 · Qualixar · AGPL-3.0-or-later
99
+ SuperLocalMemory v4.1.11 · Qualixar · AGPL-3.0-or-later
@@ -279,4 +279,4 @@ mesh availability.
279
279
 
280
280
  ---
281
281
 
282
- *SuperLocalMemory v4.1.8 · Qualixar · AGPL-3.0-or-later*
282
+ *SuperLocalMemory v4.1.11 · Qualixar · AGPL-3.0-or-later*
@@ -146,4 +146,4 @@ Name them differently in your MCP config (e.g. `superlocalmemory-personal` and
146
146
 
147
147
  ---
148
148
 
149
- *SuperLocalMemory v4.1.8 · Qualixar · AGPL-3.0-or-later*
149
+ *SuperLocalMemory v4.1.11 · Qualixar · AGPL-3.0-or-later*
@@ -323,4 +323,4 @@ before recalling, then switch back. See `slm-profile` for workspace switching.
323
323
 
324
324
  ---
325
325
 
326
- *SuperLocalMemory v4.1.8 · Qualixar · AGPL-3.0-or-later*
326
+ *SuperLocalMemory v4.1.11 · Qualixar · AGPL-3.0-or-later*
@@ -270,4 +270,4 @@ different workspace, use `switch_profile` first. See `slm-profile`.
270
270
 
271
271
  ---
272
272
 
273
- *SuperLocalMemory v4.1.8 · Qualixar · AGPL-3.0-or-later*
273
+ *SuperLocalMemory v4.1.11 · Qualixar · AGPL-3.0-or-later*
@@ -173,4 +173,4 @@ to review the impact. See `slm-remember` for the full deletion discipline.
173
173
 
174
174
  ---
175
175
 
176
- *SuperLocalMemory v4.1.8 · Qualixar · AGPL-3.0-or-later*
176
+ *SuperLocalMemory v4.1.11 · Qualixar · AGPL-3.0-or-later*
@@ -253,4 +253,4 @@ explicitly and call `recall` with `include_global`/`include_shared` after
253
253
 
254
254
  ---
255
255
 
256
- *SuperLocalMemory v4.1.8 · Qualixar · AGPL-3.0-or-later*
256
+ *SuperLocalMemory v4.1.11 · Qualixar · AGPL-3.0-or-later*
@@ -163,4 +163,4 @@ multi-profile setup. To switch the active profile, see `slm-profile`.
163
163
 
164
164
  ---
165
165
 
166
- SuperLocalMemory v4.1.8 · Qualixar · AGPL-3.0-or-later
166
+ SuperLocalMemory v4.1.11 · Qualixar · AGPL-3.0-or-later
@@ -46,4 +46,4 @@ slm-recall · slm-remember · slm-session · slm-scope · slm-profile · slm-gov
46
46
  # What NOT to do
47
47
  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; never bypass scope or governance restrictions.
48
48
 
49
- SuperLocalMemory v4.1.8 · Qualixar · AGPL-3.0-or-later
49
+ SuperLocalMemory v4.1.11 · Qualixar · AGPL-3.0-or-later
@@ -41,4 +41,4 @@ slm-compress · slm-cache · slm-status · slm-profile
41
41
  # What NOT to do
42
42
  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 %; never carry ccr_ids across profile switches.
43
43
 
44
- SuperLocalMemory v4.1.8 · Qualixar · AGPL-3.0-or-later
44
+ SuperLocalMemory v4.1.11 · Qualixar · AGPL-3.0-or-later
@@ -137,4 +137,4 @@ When the SLM MCP server is unavailable, use these CLI equivalents:
137
137
  - **slm-optimize-advisor** — context compression and KV cache
138
138
  - **slm-governance-advisor** — scope/role compliance, retention policies, GDPR
139
139
 
140
- SuperLocalMemory v4.1.8 · Qualixar · AGPL-3.0-or-later
140
+ SuperLocalMemory v4.1.11 · Qualixar · AGPL-3.0-or-later
@@ -145,4 +145,4 @@ These subcommands control daemon-level cache settings. They do not read or write
145
145
 
146
146
  ---
147
147
 
148
- SuperLocalMemory v4.1.8 · Qualixar · AGPL-3.0-or-later
148
+ SuperLocalMemory v4.1.11 · Qualixar · AGPL-3.0-or-later
@@ -147,4 +147,4 @@ Content over 1 MB (1 000 000 bytes UTF-8) is processed but `reversible` is force
147
147
 
148
148
  ---
149
149
 
150
- SuperLocalMemory v4.1.8 · Qualixar · AGPL-3.0-or-later
150
+ SuperLocalMemory v4.1.11 · Qualixar · AGPL-3.0-or-later
@@ -245,4 +245,4 @@ Before running any destructive operation (`forget`, `compact_memories`):
245
245
 
246
246
  ---
247
247
 
248
- *SuperLocalMemory v4.1.8 · Qualixar · AGPL-3.0-or-later*
248
+ *SuperLocalMemory v4.1.11 · Qualixar · AGPL-3.0-or-later*
@@ -312,4 +312,4 @@ profile. See `slm-profile` for the full profile switching workflow.
312
312
 
313
313
  ---
314
314
 
315
- SuperLocalMemory v4.1.8 · Qualixar · AGPL-3.0-or-later
315
+ SuperLocalMemory v4.1.11 · Qualixar · AGPL-3.0-or-later
@@ -96,4 +96,4 @@ paused, name the approval needed; when errored, quote the short detail.
96
96
 
97
97
  ---
98
98
 
99
- SuperLocalMemory v4.1.8 · Qualixar · AGPL-3.0-or-later
99
+ SuperLocalMemory v4.1.11 · Qualixar · AGPL-3.0-or-later
@@ -279,4 +279,4 @@ mesh availability.
279
279
 
280
280
  ---
281
281
 
282
- *SuperLocalMemory v4.1.8 · Qualixar · AGPL-3.0-or-later*
282
+ *SuperLocalMemory v4.1.11 · Qualixar · AGPL-3.0-or-later*
@@ -146,4 +146,4 @@ Name them differently in your MCP config (e.g. `superlocalmemory-personal` and
146
146
 
147
147
  ---
148
148
 
149
- *SuperLocalMemory v4.1.8 · Qualixar · AGPL-3.0-or-later*
149
+ *SuperLocalMemory v4.1.11 · Qualixar · AGPL-3.0-or-later*
@@ -323,4 +323,4 @@ before recalling, then switch back. See `slm-profile` for workspace switching.
323
323
 
324
324
  ---
325
325
 
326
- *SuperLocalMemory v4.1.8 · Qualixar · AGPL-3.0-or-later*
326
+ *SuperLocalMemory v4.1.11 · Qualixar · AGPL-3.0-or-later*
@@ -270,4 +270,4 @@ different workspace, use `switch_profile` first. See `slm-profile`.
270
270
 
271
271
  ---
272
272
 
273
- *SuperLocalMemory v4.1.8 · Qualixar · AGPL-3.0-or-later*
273
+ *SuperLocalMemory v4.1.11 · Qualixar · AGPL-3.0-or-later*
@@ -173,4 +173,4 @@ to review the impact. See `slm-remember` for the full deletion discipline.
173
173
 
174
174
  ---
175
175
 
176
- *SuperLocalMemory v4.1.8 · Qualixar · AGPL-3.0-or-later*
176
+ *SuperLocalMemory v4.1.11 · Qualixar · AGPL-3.0-or-later*
@@ -253,4 +253,4 @@ explicitly and call `recall` with `include_global`/`include_shared` after
253
253
 
254
254
  ---
255
255
 
256
- *SuperLocalMemory v4.1.8 · Qualixar · AGPL-3.0-or-later*
256
+ *SuperLocalMemory v4.1.11 · Qualixar · AGPL-3.0-or-later*
@@ -163,4 +163,4 @@ multi-profile setup. To switch the active profile, see `slm-profile`.
163
163
 
164
164
  ---
165
165
 
166
- SuperLocalMemory v4.1.8 · Qualixar · AGPL-3.0-or-later
166
+ SuperLocalMemory v4.1.11 · Qualixar · AGPL-3.0-or-later
package/pyproject.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "superlocalmemory"
3
- version = "4.1.8"
3
+ version = "4.1.11"
4
4
  description = "Local-first agent memory with auditable hybrid retrieval"
5
5
  readme = "README.md"
6
6
  license = "AGPL-3.0-or-later"
@@ -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__ = "4.1.8"
35
+ __version__ = "4.1.11"
36
36
 
37
37
  _REQUIRED_VERSIONS = {
38
38
  "sentence_transformers": "5.3.0",
@@ -364,6 +364,14 @@ class DaemonRefused(RuntimeError):
364
364
  )
365
365
 
366
366
 
367
+ class DaemonConflict(RuntimeError):
368
+ """A deterministic daemon conflict that the caller must resolve."""
369
+
370
+ def __init__(self, detail: str) -> None:
371
+ self.detail = detail or "daemon request conflicted with current state"
372
+ super().__init__(self.detail)
373
+
374
+
367
375
  def daemon_request(
368
376
  method: str,
369
377
  path: str,
@@ -373,6 +381,7 @@ def daemon_request(
373
381
  expected_descriptor=_EXPECTED_DESCRIPTOR_UNSET,
374
382
  expected_legacy: dict | None = None,
375
383
  verify_health: bool = True,
384
+ preserve_conflict: bool = False,
376
385
  ) -> dict | None:
377
386
  """Send a request only after validating the owned daemon identity.
378
387
 
@@ -463,6 +472,15 @@ def daemon_request(
463
472
  # locally as the machine owner.
464
473
  if exc.code in (401, 403):
465
474
  raise DaemonRefused(exc.code, path) from exc
475
+ if exc.code == 409 and preserve_conflict:
476
+ detail = "daemon request conflicted with current state"
477
+ try:
478
+ payload = json.loads(exc.read().decode())
479
+ if isinstance(payload, dict) and payload.get("detail"):
480
+ detail = str(payload["detail"])
481
+ except Exception:
482
+ pass
483
+ raise DaemonConflict(detail) from exc
466
484
  return None
467
485
  except Exception:
468
486
  return None
@@ -113,6 +113,30 @@ _MAX_CLOCK_SKEW_MS: Final[int] = 60_000
113
113
  # ---------------------------------------------------------------------------
114
114
 
115
115
 
116
+ #: Dwell below this is not engagement; the label formula uses the same bound.
117
+ _DWELL_THRESHOLD_MS: Final[int] = 2000
118
+
119
+
120
+ def _carries_evidence(signals: Mapping[str, object]) -> bool:
121
+ """Did anything actually happen after this recall?
122
+
123
+ ``_compute_label`` is built as ``0.5 + bonuses - penalties``, so a signal
124
+ set with nothing in it evaluates to exactly one half. That number is not an
125
+ absence of judgement, it is a confident one: it becomes a mid-strength
126
+ positive training label and a Beta update that tightens a posterior around
127
+ its prior. An empty dict and a dict of falsy values mean the same thing here
128
+ and must both be refused.
129
+ """
130
+ if not signals:
131
+ return False
132
+ if signals.get("cite") or signals.get("edit") or signals.get("requery"):
133
+ return True
134
+ try:
135
+ return int(signals.get("dwell_ms", 0) or 0) >= _DWELL_THRESHOLD_MS
136
+ except (TypeError, ValueError):
137
+ return False
138
+
139
+
116
140
  def _compute_label(signals: Mapping[str, object]) -> float:
117
141
  """Deterministic label in ``[0.0, 1.0]`` per the manifest A.1 formula.
118
142
 
@@ -141,8 +165,9 @@ def _compute_label(signals: Mapping[str, object]) -> float:
141
165
  dwell_int = 0
142
166
 
143
167
  dwell_bonus = 0.0
144
- if dwell_int >= 2000:
145
- dwell_bonus = min(0.15, 0.05 + (dwell_int - 2000) / 80_000.0)
168
+ if dwell_int >= _DWELL_THRESHOLD_MS:
169
+ dwell_bonus = min(
170
+ 0.15, 0.05 + (dwell_int - _DWELL_THRESHOLD_MS) / 80_000.0)
146
171
 
147
172
  label = (
148
173
  0.5
@@ -585,9 +610,19 @@ class EngagementRewardModel:
585
610
  signals = json.loads(pending["signals_json"] or "{}")
586
611
  except json.JSONDecodeError: # pragma: no cover — defensive
587
612
  signals = {}
588
- reward = _compute_label(signals)
589
613
  now_ms = self._clock_ms()
590
614
  timestamp_iso = _iso_from_ms(now_ms)
615
+ if not _carries_evidence(signals):
616
+ # Finalize so it stops being rescanned, but record no
617
+ # outcome: nothing was observed, and a score here would
618
+ # be invented rather than measured. Mirrors reap_stale.
619
+ conn.execute(
620
+ "UPDATE pending_outcomes "
621
+ "SET status = 'settled' WHERE outcome_id = ?",
622
+ (outcome_id,),
623
+ )
624
+ return _FALLBACK_REWARD
625
+ reward = _compute_label(signals)
591
626
 
592
627
  # NOTE: Split INSERT across lines so the Stage-5b CI
593
628
  # gate's single-line regex (LLD-00 §13) does not fire.
@@ -746,7 +781,7 @@ class EngagementRewardModel:
746
781
  #
747
782
  # The row is still finalized, so it stops being rescanned; it is
748
783
  # simply not turned into evidence it never was.
749
- if not signals:
784
+ if not _carries_evidence(signals):
750
785
  settle_ids.append(row["outcome_id"])
751
786
  continue
752
787
  reward = _compute_label(signals)
@@ -21,6 +21,7 @@ from superlocalmemory.loops.ledger import (
21
21
  SLMMemoryLedger,
22
22
  engine_backed_ledger,
23
23
  open_engine_store,
24
+ pool_backed_ledger,
24
25
  )
25
26
  from superlocalmemory.loops.models import (
26
27
  Bounds,
@@ -49,6 +50,7 @@ __all__ = [
49
50
  "InMemoryLedger",
50
51
  "SLMMemoryLedger",
51
52
  "engine_backed_ledger",
53
+ "pool_backed_ledger",
52
54
  "open_engine_store",
53
55
  "no_progress",
54
56
  "rung_requires_approval",
@@ -223,6 +223,36 @@ class _EngineLedgerStore:
223
223
  self._engine.close()
224
224
 
225
225
 
226
+ class _PoolLedgerStore(_EngineLedgerStore):
227
+ """Write through the owned daemon/worker; read through the LIGHT engine.
228
+
229
+ MCP intentionally keeps a LIGHT engine in-process. That engine owns a
230
+ read-only database view but refuses ``store``/``store_fast``. The pool is
231
+ the admitted writer and shares the same canonical store, so loop laps stay
232
+ immediately queryable without turning every MCP process into a heavy
233
+ writer.
234
+ """
235
+
236
+ def __init__(self, pool: Any, reader_engine: Any) -> None:
237
+ super().__init__(reader_engine, owns_engine=False)
238
+ self._pool = pool
239
+
240
+ def add(self, content: str, *, session_id: str, metadata: dict) -> None:
241
+ result = self._pool.store(
242
+ content,
243
+ metadata={
244
+ **metadata,
245
+ "session_id": session_id,
246
+ "profile_id": self._engine.profile_id,
247
+ },
248
+ )
249
+ accepted = bool(result.get("ok") or result.get("success"))
250
+ if not accepted:
251
+ raise RuntimeError(
252
+ result.get("error", "owned SLM writer rejected loop ledger entry")
253
+ )
254
+
255
+
226
256
  def engine_backed_ledger(engine: Any) -> SLMMemoryLedger:
227
257
  """Build an SLM-backed ledger over an ALREADY-OPEN engine.
228
258
 
@@ -236,6 +266,11 @@ def engine_backed_ledger(engine: Any) -> SLMMemoryLedger:
236
266
  return SLMMemoryLedger(_EngineLedgerStore(engine, owns_engine=False))
237
267
 
238
268
 
269
+ def pool_backed_ledger(pool: Any, reader_engine: Any) -> SLMMemoryLedger:
270
+ """Build an MCP-safe ledger: admitted pool writes, LIGHT engine reads."""
271
+ return SLMMemoryLedger(_PoolLedgerStore(pool, reader_engine))
272
+
273
+
239
274
  def open_engine_store(db_path: str | Path) -> _EngineLedgerStore:
240
275
  """Build an engine-backed ledger store rooted at ``db_path``.
241
276
 
@@ -139,21 +139,40 @@ class DaemonPoolProxy:
139
139
  ) -> dict[str, Any]:
140
140
  if self._unavailable:
141
141
  return self._unavailable_response()
142
+ tags = (metadata or {}).get("tags", "")
143
+ if isinstance(tags, (list, tuple, set)):
144
+ tags = ",".join(str(tag) for tag in tags)
142
145
  body = {
143
146
  "content": content,
144
- "tags": (metadata or {}).get("tags", ""),
147
+ "tags": tags,
145
148
  "metadata": metadata or {},
146
149
  "session_id": (metadata or {}).get("session_id", ""),
147
150
  "idempotency_key": (metadata or {}).get("idempotency_key") or None,
151
+ "profile_id": (metadata or {}).get("profile_id", ""),
148
152
  }
153
+ # One identity-aware daemon client owns descriptor validation,
154
+ # capability delivery, and exact-instance targeting. A raw urllib POST
155
+ # here previously became unauthenticated when /remember was hardened
156
+ # and could also attach to a stale/foreign port.
149
157
  try:
150
- # One identity-aware daemon client owns descriptor validation,
151
- # capability delivery, and exact-instance targeting. A raw urllib
152
- # POST here previously became unauthenticated when /remember was
153
- # hardened and could also attach to a stale/foreign port.
154
- from superlocalmemory.cli.daemon import daemon_request
155
-
156
- data = daemon_request("POST", "/remember", body)
158
+ from superlocalmemory.cli.daemon import DaemonConflict, daemon_request
159
+ except Exception as exc:
160
+ logger.warning("daemon client import failed: %s", exc)
161
+ return self._unavailable_response()
162
+ try:
163
+ data = daemon_request(
164
+ "POST",
165
+ "/remember",
166
+ body,
167
+ preserve_conflict=True,
168
+ )
169
+ except DaemonConflict as exc:
170
+ return {
171
+ "ok": False,
172
+ "code": "PROFILE_MISMATCH",
173
+ "retryable": False,
174
+ "error": str(exc),
175
+ }
157
176
  except Exception as exc:
158
177
  logger.warning("daemon /remember failed: %s", exc)
159
178
  return self._unavailable_response()
@@ -280,7 +280,12 @@ register_evolution_tools(_target, get_engine) # v3.4.11: Skill evolution tools
280
280
  from superlocalmemory.mcp.tools_optimize import register_optimize_tools
281
281
  register_optimize_tools(_target) # v3.6.11: Surface B Optimize tools (proxy-free)
282
282
  from superlocalmemory.mcp.tools_loops import register_loop_tools
283
- register_loop_tools(_target, get_engine) # v3.8.0: bounded-loop tools (CLI+command+MCP)
283
+ from superlocalmemory.mcp._daemon_proxy import choose_pool as _choose_loop_pool
284
+ register_loop_tools(
285
+ _target,
286
+ get_engine,
287
+ _choose_loop_pool,
288
+ ) # v3.8.0: bounded-loop tools (CLI+command+MCP)
284
289
  from superlocalmemory.mcp.tools_ops import register_ops_tools
285
290
  register_ops_tools(_target, get_engine) # operational recovery & admin remediation
286
291
  from superlocalmemory.mcp.tools_brain import register_brain_tools
@@ -47,6 +47,7 @@ from superlocalmemory.loops import (
47
47
  LedgerEntry,
48
48
  Verdict,
49
49
  engine_backed_ledger,
50
+ pool_backed_ledger,
50
51
  run_bounded_loop,
51
52
  )
52
53
 
@@ -69,7 +70,11 @@ _MAX_NAME_CHARS = 128
69
70
  _MAX_QUERY_CHARS = 2000
70
71
 
71
72
 
72
- def register_loop_tools(server, get_engine: Callable) -> None:
73
+ def register_loop_tools(
74
+ server,
75
+ get_engine: Callable,
76
+ get_pool: Callable | None = None,
77
+ ) -> None:
73
78
  """Register the 3 bounded-loop tools on *server*.
74
79
 
75
80
  *server* is duck-typed: must support the ``@server.tool()`` decorator.
@@ -144,10 +149,46 @@ def register_loop_tools(server, get_engine: Callable) -> None:
144
149
  min_score = float(gate_min_score)
145
150
 
146
151
  engine = get_engine()
152
+ pool = get_pool() if get_pool is not None else None
147
153
 
148
154
  def gate(lap: int) -> Verdict:
149
- resp = engine.recall(gate_query, limit=3, fast=True)
150
- all_results = getattr(resp, "results", None) or []
155
+ if pool is not None:
156
+ resp = pool.recall(gate_query, limit=3, fast=True)
157
+ if not isinstance(resp, dict) or resp.get("ok") is False:
158
+ raise RuntimeError(
159
+ (resp or {}).get("error", "owned SLM reader rejected recall")
160
+ if isinstance(resp, dict)
161
+ else "owned SLM reader returned an invalid response"
162
+ )
163
+ all_results = resp.get("results", []) or []
164
+ floored = bool(resp.get("no_confident_match", False))
165
+ else:
166
+ resp = engine.recall(gate_query, limit=3, fast=True)
167
+ all_results = getattr(resp, "results", None) or []
168
+ floored = bool(getattr(resp, "no_confident_match", False))
169
+
170
+ def _content(result: Any) -> str:
171
+ if isinstance(result, dict):
172
+ fact = result.get("fact")
173
+ if isinstance(fact, dict):
174
+ return str(fact.get("content", "") or "")
175
+ return str(result.get("content", "") or "")
176
+ return str(
177
+ getattr(getattr(result, "fact", None), "content", "") or ""
178
+ )
179
+
180
+ def _score(result: Any) -> float:
181
+ if isinstance(result, dict):
182
+ value = result.get("score", result.get("relevance_score", 0.0))
183
+ else:
184
+ value = getattr(result, "score", None)
185
+ if value is None:
186
+ value = getattr(result, "relevance_score", 0.0) or 0.0
187
+ try:
188
+ return float(value)
189
+ except (TypeError, ValueError):
190
+ return 0.0
191
+
151
192
  # Exclude the loop's own audit records from the gate evaluation.
152
193
  # Each lap writes a LedgerEntry (JSON with "run_id" + "lap") into
153
194
  # SLM via store_fast; the FTS5 trigger indexes every such insert.
@@ -157,20 +198,9 @@ def register_loop_tools(server, get_engine: Callable) -> None:
157
198
  # can satisfy an independent gate.
158
199
  results = [
159
200
  r for r in all_results
160
- if LedgerEntry.from_json(
161
- getattr(getattr(r, "fact", None), "content", "") or ""
162
- ) is None
201
+ if LedgerEntry.from_json(_content(r)) is None
163
202
  ]
164
- floored = bool(getattr(resp, "no_confident_match", False))
165
- best: float = 0.0
166
- for r in results:
167
- s = getattr(r, "score", None)
168
- if s is None:
169
- s = getattr(r, "relevance_score", 0.0) or 0.0
170
- try:
171
- best = max(best, float(s))
172
- except (TypeError, ValueError):
173
- pass
203
+ best = max((_score(result) for result in results), default=0.0)
174
204
  passed = bool(results) and not floored and best >= min_score
175
205
  return Verdict(
176
206
  passed,
@@ -187,7 +217,11 @@ def register_loop_tools(server, get_engine: Callable) -> None:
187
217
  time.sleep(poll)
188
218
  return LapResult(changed=False, tokens=0)
189
219
 
190
- ledger = engine_backed_ledger(engine)
220
+ ledger = (
221
+ pool_backed_ledger(pool, engine)
222
+ if pool is not None
223
+ else engine_backed_ledger(engine)
224
+ )
191
225
 
192
226
  # The loop blocks (sleeps between laps); run it off the event loop.
193
227
  # The engine's per-call WAL connection model makes this thread-safe.
@@ -630,6 +630,10 @@ class RememberRequest(BaseModel):
630
630
  metadata: dict | None = None # v3.4.26: pass-through from MCP pool_store
631
631
  idempotency_key: str | None = None
632
632
  session_id: str = ""
633
+ # Optional compare-and-write guard for profile-sensitive clients. The
634
+ # daemon remains single-profile, so a stale MCP client must fail instead
635
+ # of silently writing into whichever profile another client selected.
636
+ profile_id: str = ""
633
637
  #: WHEN this memory is about, as distinct from when it was written.
634
638
  #:
635
639
  #: The internal admission record has carried this field all along and this
@@ -693,6 +697,18 @@ class RememberRequest(BaseModel):
693
697
  shared_with: list[str] | None = None
694
698
 
695
699
 
700
+ def _require_remember_profile(requested: str, active: str) -> None:
701
+ """Reject a stale profile-bound write before any durable mutation."""
702
+ if requested and requested != active:
703
+ raise HTTPException(
704
+ status_code=409,
705
+ detail=(
706
+ f"profile mismatch: request is bound to '{requested}' but "
707
+ f"the daemon currently serves '{active}'"
708
+ ),
709
+ )
710
+
711
+
696
712
  class SessionOpenRequest(BaseModel):
697
713
  # #49: local session-open warm (no model roundtrip needed)
698
714
  project_path: str = ""
@@ -4671,6 +4687,7 @@ def _register_daemon_routes(application: FastAPI) -> None:
4671
4687
  trusted_actor_id = _require_write_actor(request)
4672
4688
  _update_activity()
4673
4689
  engine = _get_engine_or_503()
4690
+ _require_remember_profile(req.profile_id, engine._profile_id)
4674
4691
 
4675
4692
  # v3.6.15 multi-scope: resolve the write scope. ``None`` (not specified
4676
4693
  # by the caller) → the configured default_scope (personal). Shared
@@ -1587,7 +1587,7 @@
1587
1587
  <script src="static/js/event-delegation.js?v=379"></script>
1588
1588
  <!-- OD screen modules (v3.7.9): approved-design panes wired to live data.
1589
1589
  Load LAST so their window.load<Screen> overrides win over the legacy loaders. -->
1590
- <script src="static/js/od-brain.js?v=4f233c96"></script>
1590
+ <script src="static/js/od-brain.js?v=77369272"></script>
1591
1591
  <script src="static/js/od-components.js?v=382"></script>
1592
1592
  <script src="static/js/od-health.js?v=382"></script>
1593
1593
  <script src="static/js/od-operations.js?v=380"></script>
@@ -1595,7 +1595,7 @@
1595
1595
  <script src="static/js/od-ops-health.js?v=400"></script>
1596
1596
  <script src="static/js/od-team.js?v=379"></script>
1597
1597
  <script src="static/js/od-graph.js?v=6812bf6c"></script>
1598
- <script src="static/js/od-memories.js?v=997f1674"></script>
1598
+ <script src="static/js/od-memories.js?v=30ac32ed"></script>
1599
1599
  <script src="static/js/od-entities.js?v=379"></script>
1600
1600
  <!-- Multi-Agent Memory pane (v3.8.0): visualises memory written by multiple agents -->
1601
1601
  <script src="static/js/od-agents.js?v=c75ff3c2"></script>