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.
- package/.claude-plugin/marketplace.json +2 -2
- package/CHANGELOG.md +35 -0
- package/README.md +3 -3
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/CLAUDE.md +3 -3
- package/plugin/agents/slm-governance-advisor.md +1 -1
- package/plugin/agents/slm-loop-runner.md +1 -1
- package/plugin/agents/slm-memory-advisor.md +1 -1
- package/plugin/agents/slm-optimize-advisor.md +1 -1
- package/plugin/requirements.txt +1 -1
- package/plugin/skills/slm-cache/SKILL.md +1 -1
- package/plugin/skills/slm-compress/SKILL.md +1 -1
- package/plugin/skills/slm-governance/SKILL.md +1 -1
- package/plugin/skills/slm-graph/SKILL.md +1 -1
- package/plugin/skills/slm-loop/SKILL.md +1 -1
- package/plugin/skills/slm-mesh/SKILL.md +1 -1
- package/plugin/skills/slm-profile/SKILL.md +1 -1
- package/plugin/skills/slm-recall/SKILL.md +1 -1
- package/plugin/skills/slm-remember/SKILL.md +1 -1
- package/plugin/skills/slm-scope/SKILL.md +1 -1
- package/plugin/skills/slm-session/SKILL.md +1 -1
- package/plugin/skills/slm-status/SKILL.md +1 -1
- package/plugin-src/agents/slm-memory-advisor.md +1 -1
- package/plugin-src/agents/slm-optimize-advisor.md +1 -1
- package/plugin-src/rules/AGENTS.md +1 -1
- package/plugin-src/skills/slm-cache/SKILL.md +1 -1
- package/plugin-src/skills/slm-compress/SKILL.md +1 -1
- package/plugin-src/skills/slm-governance/SKILL.md +1 -1
- package/plugin-src/skills/slm-graph/SKILL.md +1 -1
- package/plugin-src/skills/slm-loop/SKILL.md +1 -1
- package/plugin-src/skills/slm-mesh/SKILL.md +1 -1
- package/plugin-src/skills/slm-profile/SKILL.md +1 -1
- package/plugin-src/skills/slm-recall/SKILL.md +1 -1
- package/plugin-src/skills/slm-remember/SKILL.md +1 -1
- package/plugin-src/skills/slm-scope/SKILL.md +1 -1
- package/plugin-src/skills/slm-session/SKILL.md +1 -1
- package/plugin-src/skills/slm-status/SKILL.md +1 -1
- package/pyproject.toml +1 -1
- package/src/superlocalmemory/__init__.py +1 -1
- package/src/superlocalmemory/cli/daemon.py +18 -0
- package/src/superlocalmemory/learning/reward.py +39 -4
- package/src/superlocalmemory/loops/__init__.py +2 -0
- package/src/superlocalmemory/loops/ledger.py +35 -0
- package/src/superlocalmemory/mcp/_daemon_proxy.py +27 -8
- package/src/superlocalmemory/mcp/server.py +6 -1
- package/src/superlocalmemory/mcp/tools_loops.py +51 -17
- package/src/superlocalmemory/server/unified_daemon.py +17 -0
- 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.
|
|
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.
|
|
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 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.
|
|
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 & 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> · MCP: add <code>slm_compress</code> to your config · 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.
|
|
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
package/plugin/CLAUDE.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!-- BEGIN SuperLocalMemory v4.1.
|
|
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.
|
|
42
|
+
<!-- END SuperLocalMemory v4.1.11 -->
|
|
43
43
|
|
|
44
|
-
SuperLocalMemory v4.1.
|
|
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.
|
|
80
|
+
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.
|
|
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.
|
|
44
|
+
SuperLocalMemory v4.1.11 · Qualixar · AGPL-3.0-or-later
|
package/plugin/requirements.txt
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
superlocalmemory==4.1.
|
|
1
|
+
superlocalmemory==4.1.11
|
|
@@ -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.
|
|
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.
|
|
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.
|
|
140
|
+
SuperLocalMemory v4.1.11 · Qualixar · AGPL-3.0-or-later
|
package/pyproject.toml
CHANGED
|
@@ -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.
|
|
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 >=
|
|
145
|
-
dwell_bonus = min(
|
|
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":
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
150
|
-
|
|
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
|
-
|
|
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 =
|
|
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=
|
|
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=
|
|
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>
|