superlocalmemory 4.1.9 → 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 (48) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/CHANGELOG.md +16 -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/loops/__init__.py +2 -0
  43. package/src/superlocalmemory/loops/ledger.py +35 -0
  44. package/src/superlocalmemory/mcp/_daemon_proxy.py +27 -8
  45. package/src/superlocalmemory/mcp/server.py +6 -1
  46. package/src/superlocalmemory/mcp/tools_loops.py +51 -17
  47. package/src/superlocalmemory/server/unified_daemon.py +17 -0
  48. 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.9"
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.9"
42
+ "version": "4.1.11"
43
43
  }
44
44
  ]
45
45
  }
package/CHANGELOG.md CHANGED
@@ -5,6 +5,22 @@ 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
+
8
24
  ## [4.1.9] — Silence is not a verdict
9
25
 
10
26
  ### Fixed
package/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
  </picture>
6
6
  </p>
7
7
 
8
- <h1 align="center">SuperLocalMemory V4.1.9</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.9</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.9-Current_Release-2ea44f?style=for-the-badge&logo=checkmarx&logoColor=white" alt="v4.1.9 — 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.9",
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.9"
18
+ "version": "4.1.11"
19
19
  }
package/plugin/CLAUDE.md CHANGED
@@ -1,4 +1,4 @@
1
- <!-- BEGIN SuperLocalMemory v4.1.9 -->
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.9 -->
42
+ <!-- END SuperLocalMemory v4.1.11 -->
43
43
 
44
- SuperLocalMemory v4.1.9 · 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.9 · 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.9 · 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.9 · 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.9 · Qualixar · AGPL-3.0-or-later
44
+ SuperLocalMemory v4.1.11 · Qualixar · AGPL-3.0-or-later
@@ -1 +1 @@
1
- superlocalmemory==4.1.9
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.9 · 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.9 · 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.9 · 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.9 · 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.9 · 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.9 · 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.9 · 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.9 · 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.9 · 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.9 · 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.9 · 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.9 · 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.9 · 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.9 · 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.9 · 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.9 · 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.9 · 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.9 · 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.9 · 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.9 · 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.9 · 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.9 · 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.9 · 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.9 · 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.9 · 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.9 · 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.9 · 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.9"
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.9"
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
@@ -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>