superlocalmemory 3.8.11 → 3.8.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +85 -0
- package/README.md +7 -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/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-graph/SKILL.md +1 -1
- package/plugin-src/skills/slm-recall/SKILL.md +1 -1
- package/plugin-src/skills/slm-remember/SKILL.md +1 -1
- package/plugin-src/skills/slm-session/SKILL.md +1 -1
- package/plugin-src/skills/slm-status/SKILL.md +1 -1
- package/pyproject.toml +1 -1
- package/src/superlocalmemory/__init__.py +1 -1
- package/src/superlocalmemory/cli/commands.py +62 -3
- package/src/superlocalmemory/cli/daemon.py +219 -10
- package/src/superlocalmemory/cli/setup_wizard.py +45 -1
- package/src/superlocalmemory/core/component_registry.py +25 -0
- package/src/superlocalmemory/core/config.py +35 -1
- package/src/superlocalmemory/core/engine_wiring.py +81 -5
- package/src/superlocalmemory/core/recall_pipeline.py +25 -4
- package/src/superlocalmemory/core/reranker_worker.py +23 -4
- package/src/superlocalmemory/infra/daemon_identity.py +16 -0
- package/src/superlocalmemory/infra/process_identity.py +180 -0
- package/src/superlocalmemory/infra/version_integrity.py +229 -0
- package/src/superlocalmemory/learning/feedback.py +288 -29
- package/src/superlocalmemory/learning/legacy_migration.py +45 -4
- package/src/superlocalmemory/mcp/_daemon_proxy.py +23 -1
- package/src/superlocalmemory/mcp/tools_active.py +109 -58
- package/src/superlocalmemory/mcp/tools_core.py +6 -5
- package/src/superlocalmemory/retrieval/remote_reranker.py +636 -0
- package/src/superlocalmemory/server/unified_daemon.py +27 -0
|
@@ -141,13 +141,21 @@ def _emit_event(event_type: str, payload: dict | None = None,
|
|
|
141
141
|
|
|
142
142
|
|
|
143
143
|
# ---------------------------------------------------------------------------
|
|
144
|
-
# Canonical learning-store feedback (
|
|
144
|
+
# Canonical learning-store feedback (issues #102, #106)
|
|
145
145
|
#
|
|
146
|
-
# learning.db is the single store every learning consumer reads
|
|
147
|
-
#
|
|
148
|
-
#
|
|
149
|
-
#
|
|
150
|
-
#
|
|
146
|
+
# learning.db is the single store every learning consumer reads. Within it the
|
|
147
|
+
# canonical tables are ``learning_signals`` + ``learning_features``: the phase
|
|
148
|
+
# gate (recall_pipeline), the dashboard Living Brain panel, the ranker-phase
|
|
149
|
+
# card, and the retrainer all resolve their phase from ``learning_signals``.
|
|
150
|
+
# ``learning_feedback`` is the pre-v3.4.22 table that legacy_migration copies
|
|
151
|
+
# forward into it.
|
|
152
|
+
#
|
|
153
|
+
# Recall itself is deliberately read-only and must never open a writer, so an
|
|
154
|
+
# explicit feedback command is the only durable writer in the design. These
|
|
155
|
+
# helpers are that writer, and — per issue #106 — they report the SAME number
|
|
156
|
+
# the gate and the dashboard use. There is deliberately no fall back to a
|
|
157
|
+
# different store's count: a cross-store fallback is what let a total write
|
|
158
|
+
# failure still return "success" beside a plausibly incrementing counter.
|
|
151
159
|
# ---------------------------------------------------------------------------
|
|
152
160
|
|
|
153
161
|
_FEEDBACK_SIGNAL_MAP: dict[str, tuple[str, float]] = {
|
|
@@ -162,15 +170,43 @@ def _learning_db_path():
|
|
|
162
170
|
return state_path("learning.db")
|
|
163
171
|
|
|
164
172
|
|
|
173
|
+
def _phase_thresholds() -> tuple[int, int]:
|
|
174
|
+
"""Return the (phase 2, phase 3) signal thresholds.
|
|
175
|
+
|
|
176
|
+
Sourced from ``learning.ranker`` so the MCP surface can never report a
|
|
177
|
+
different phase than the one recall actually applies. Falls back to the
|
|
178
|
+
documented defaults only if the learning package is unavailable.
|
|
179
|
+
"""
|
|
180
|
+
try:
|
|
181
|
+
from superlocalmemory.learning.ranker import (
|
|
182
|
+
PHASE_2_THRESHOLD,
|
|
183
|
+
PHASE_3_THRESHOLD,
|
|
184
|
+
)
|
|
185
|
+
return PHASE_2_THRESHOLD, PHASE_3_THRESHOLD
|
|
186
|
+
except Exception: # pragma: no cover — learning extras absent
|
|
187
|
+
return 50, 200
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
_PHASE_2_THRESHOLD, _PHASE_3_THRESHOLD = _phase_thresholds()
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _phase_for_signal_count(count: int) -> int:
|
|
194
|
+
"""Map a canonical signal count onto the adaptive ranking phase."""
|
|
195
|
+
if count < _PHASE_2_THRESHOLD:
|
|
196
|
+
return 1
|
|
197
|
+
return 2 if count < _PHASE_3_THRESHOLD else 3
|
|
198
|
+
|
|
199
|
+
|
|
165
200
|
def _record_canonical_feedback(
|
|
166
201
|
*, profile_id: str, fact_id: str, feedback: str, query: str = "",
|
|
167
202
|
channel: str = "explicit",
|
|
168
203
|
) -> bool:
|
|
169
204
|
"""Write explicit feedback to learning.db. Returns True on success.
|
|
170
205
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
206
|
+
True means the ``learning_signals`` row that every phase counter reads
|
|
207
|
+
actually landed — not merely that some row was written somewhere. The
|
|
208
|
+
outcome is RETURNED rather than swallowed so the caller can tell the user
|
|
209
|
+
the truth about whether the write was durable.
|
|
174
210
|
"""
|
|
175
211
|
signal_type, value = _FEEDBACK_SIGNAL_MAP.get(
|
|
176
212
|
feedback, ("user_correction", 0.5),
|
|
@@ -179,7 +215,7 @@ def _record_canonical_feedback(
|
|
|
179
215
|
from superlocalmemory.learning.feedback import FeedbackCollector
|
|
180
216
|
|
|
181
217
|
collector = FeedbackCollector(_learning_db_path())
|
|
182
|
-
|
|
218
|
+
write = collector.record_explicit_event(
|
|
183
219
|
profile_id=profile_id,
|
|
184
220
|
fact_id=fact_id,
|
|
185
221
|
signal_type=signal_type,
|
|
@@ -187,7 +223,7 @@ def _record_canonical_feedback(
|
|
|
187
223
|
query=query,
|
|
188
224
|
channel=channel,
|
|
189
225
|
)
|
|
190
|
-
return
|
|
226
|
+
return write.canonical
|
|
191
227
|
except Exception as exc:
|
|
192
228
|
logger.warning(
|
|
193
229
|
"canonical feedback write failed (fact_id=%s): %s", fact_id, exc,
|
|
@@ -196,17 +232,20 @@ def _record_canonical_feedback(
|
|
|
196
232
|
|
|
197
233
|
|
|
198
234
|
def _canonical_feedback_count(profile_id: str) -> int | None:
|
|
199
|
-
"""Count
|
|
235
|
+
"""Count the store that gates the adaptive phases.
|
|
200
236
|
|
|
201
|
-
Returns None when the store cannot be read
|
|
202
|
-
|
|
237
|
+
Returns None when the store cannot be read. The caller must NOT substitute
|
|
238
|
+
a count from a different table: before issue #106 an unreadable learning.db
|
|
239
|
+
silently fell back to ``feedback_records`` in memory.db — a table no
|
|
240
|
+
consumer reads — so the user watched a fabricated counter climb toward a
|
|
241
|
+
threshold that nothing was measuring, while the durable write did nothing.
|
|
203
242
|
"""
|
|
204
243
|
try:
|
|
205
244
|
from superlocalmemory.learning.feedback import FeedbackCollector
|
|
206
245
|
|
|
207
246
|
return FeedbackCollector(
|
|
208
247
|
_learning_db_path(),
|
|
209
|
-
).
|
|
248
|
+
).get_signal_count(profile_id)
|
|
210
249
|
except Exception as exc:
|
|
211
250
|
logger.warning("canonical feedback count failed: %s", exc)
|
|
212
251
|
return None
|
|
@@ -441,16 +480,19 @@ def register_active_tools(server, get_engine: Callable) -> None:
|
|
|
441
480
|
"source_type": m.source_type,
|
|
442
481
|
})
|
|
443
482
|
|
|
444
|
-
#
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
483
|
+
# Learning status — issue #106: read the SAME canonical counter
|
|
484
|
+
# that report_feedback reports, the recall gate applies, and the
|
|
485
|
+
# dashboard displays. This used to read ``feedback_records`` in
|
|
486
|
+
# memory.db, so session_init and report_feedback returned two
|
|
487
|
+
# different "signal" totals for one profile in the same session.
|
|
488
|
+
# A silent zero masks wiring bugs, so a failed read is logged.
|
|
489
|
+
feedback_count = _canonical_feedback_count(pid)
|
|
490
|
+
if feedback_count is None:
|
|
451
491
|
logger.warning(
|
|
452
|
-
"session_init
|
|
492
|
+
"session_init canonical signal count unavailable for "
|
|
493
|
+
"profile %s; reporting 0", pid,
|
|
453
494
|
)
|
|
495
|
+
feedback_count = 0
|
|
454
496
|
|
|
455
497
|
# v3.6.9 (#35): generate a stable session_id so clients can pass it
|
|
456
498
|
# to remember() and close_session() for proper session aggregation.
|
|
@@ -484,11 +526,13 @@ def register_active_tools(server, get_engine: Callable) -> None:
|
|
|
484
526
|
"abstention_reason": getattr(response, "abstention_reason", None),
|
|
485
527
|
"learning": {
|
|
486
528
|
"feedback_signals": feedback_count,
|
|
487
|
-
"phase":
|
|
529
|
+
"phase": _phase_for_signal_count(feedback_count),
|
|
488
530
|
"status": (
|
|
489
531
|
"collecting"
|
|
490
|
-
if feedback_count <
|
|
491
|
-
else "learning"
|
|
532
|
+
if feedback_count < _PHASE_2_THRESHOLD
|
|
533
|
+
else "learning"
|
|
534
|
+
if feedback_count < _PHASE_3_THRESHOLD
|
|
535
|
+
else "trained"
|
|
492
536
|
),
|
|
493
537
|
},
|
|
494
538
|
}
|
|
@@ -623,22 +667,18 @@ def register_active_tools(server, get_engine: Callable) -> None:
|
|
|
623
667
|
profile_id=pid,
|
|
624
668
|
)
|
|
625
669
|
|
|
626
|
-
#
|
|
627
|
-
#
|
|
628
|
-
#
|
|
629
|
-
#
|
|
630
|
-
#
|
|
631
|
-
# consumer saw nothing.
|
|
632
|
-
#
|
|
633
|
-
# The canonical learning store is learning.db. Writing here is
|
|
634
|
-
# what makes feedback do work: the phase gate
|
|
635
|
-
# (_ReadOnlyLearningView.count_feedback) unlocks adaptive ranking
|
|
636
|
-
# at 50 rows, pattern_miner mines channel_performance from it, and
|
|
637
|
-
# the dashboard Living Brain reads it. Recall stays read-only by
|
|
638
|
-
# design, so this explicit path is the ONLY durable writer.
|
|
670
|
+
# The AdaptiveLearner write above lands in ``feedback_records`` in
|
|
671
|
+
# memory.db — a table whose only readers are AdaptiveLearner's own
|
|
672
|
+
# count and its train(), which nothing in the running system
|
|
673
|
+
# calls. It is kept so existing data and GDPR erasure stay intact,
|
|
674
|
+
# but it is NOT the learning write and its count is NOT reported.
|
|
639
675
|
#
|
|
640
|
-
#
|
|
641
|
-
#
|
|
676
|
+
# The canonical store is learning.db's ``learning_signals`` (+ the
|
|
677
|
+
# paired ``learning_features`` row). Writing there is what makes
|
|
678
|
+
# feedback do work: the recall phase gate, the dashboard Living
|
|
679
|
+
# Brain panel, the ranker-phase card, and the retrainer all read
|
|
680
|
+
# it. Recall stays read-only by design, so this explicit path is
|
|
681
|
+
# the only durable writer.
|
|
642
682
|
canonical_recorded = _record_canonical_feedback(
|
|
643
683
|
profile_id=pid,
|
|
644
684
|
fact_id=fact_id,
|
|
@@ -646,16 +686,32 @@ def register_active_tools(server, get_engine: Callable) -> None:
|
|
|
646
686
|
query=query,
|
|
647
687
|
)
|
|
648
688
|
|
|
649
|
-
#
|
|
650
|
-
#
|
|
651
|
-
#
|
|
652
|
-
#
|
|
689
|
+
# issue #106: report the count from the store that ACTUALLY gates
|
|
690
|
+
# the phases, and report NOTHING when it cannot be read. The old
|
|
691
|
+
# fallback to ``feedback_records`` is what made a total write
|
|
692
|
+
# failure indistinguishable from success: the response carried a
|
|
693
|
+
# plausible, incrementing ``total_signals`` sourced from a table
|
|
694
|
+
# nothing consumes, so the caller had no way to notice that
|
|
695
|
+
# learning.db was never touched.
|
|
653
696
|
count = _canonical_feedback_count(pid)
|
|
654
|
-
if count is None:
|
|
655
|
-
count = engine._adaptive_learner.get_feedback_count(pid)
|
|
656
697
|
authorization.complete()
|
|
657
698
|
|
|
658
|
-
|
|
699
|
+
if not canonical_recorded or count is None:
|
|
700
|
+
# Never claim a durable learning write that did not happen.
|
|
701
|
+
return {
|
|
702
|
+
"success": False,
|
|
703
|
+
"durable": False,
|
|
704
|
+
"feedback_id": record.feedback_id,
|
|
705
|
+
"total_signals": count,
|
|
706
|
+
"error": (
|
|
707
|
+
"Feedback was accepted but could not be written to "
|
|
708
|
+
"the canonical learning store (learning.db), so it "
|
|
709
|
+
"will not influence ranking. Run 'slm doctor' to "
|
|
710
|
+
"diagnose learning.db."
|
|
711
|
+
),
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
phase = _phase_for_signal_count(count)
|
|
659
715
|
_emit_event("pattern.learned", {
|
|
660
716
|
"fact_id": fact_id,
|
|
661
717
|
"feedback": feedback,
|
|
@@ -665,21 +721,16 @@ def register_active_tools(server, get_engine: Callable) -> None:
|
|
|
665
721
|
|
|
666
722
|
result = {
|
|
667
723
|
"success": True,
|
|
724
|
+
"durable": True,
|
|
668
725
|
"feedback_id": record.feedback_id,
|
|
669
726
|
"total_signals": count,
|
|
670
727
|
"phase": phase,
|
|
671
728
|
"message": f"Feedback recorded. {count} total signals."
|
|
672
|
-
+ (" Phase 2 unlocked!"
|
|
673
|
-
|
|
729
|
+
+ (" Phase 2 unlocked!"
|
|
730
|
+
if count == _PHASE_2_THRESHOLD else "")
|
|
731
|
+
+ (" Phase 3 (ML) unlocked!"
|
|
732
|
+
if count == _PHASE_3_THRESHOLD else ""),
|
|
674
733
|
}
|
|
675
|
-
if not canonical_recorded:
|
|
676
|
-
# Never claim a durable learning write that did not happen.
|
|
677
|
-
result["durable"] = False
|
|
678
|
-
result["warning"] = (
|
|
679
|
-
"Feedback was accepted but could not be written to the "
|
|
680
|
-
"canonical learning store; it will not influence ranking. "
|
|
681
|
-
"Run 'slm doctor' to diagnose learning.db."
|
|
682
|
-
)
|
|
683
734
|
return result
|
|
684
735
|
except Exception as exc:
|
|
685
736
|
logger.exception("report_feedback failed")
|
|
@@ -21,6 +21,7 @@ from mcp.types import ToolAnnotations
|
|
|
21
21
|
|
|
22
22
|
from superlocalmemory.core.config import CANONICAL_RECALL_LIMIT
|
|
23
23
|
from superlocalmemory.infra.data_root import state_path
|
|
24
|
+
from superlocalmemory.mcp._daemon_proxy import daemon_unavailable_error
|
|
24
25
|
from superlocalmemory.mcp.shared import authorize_mcp_mutation
|
|
25
26
|
|
|
26
27
|
logger = logging.getLogger(__name__)
|
|
@@ -155,7 +156,7 @@ def register_core_tools(server, get_engine: Callable) -> None:
|
|
|
155
156
|
"code": "DAEMON_UNAVAILABLE",
|
|
156
157
|
"retryable": True,
|
|
157
158
|
"error": (
|
|
158
|
-
|
|
159
|
+
daemon_unavailable_error()
|
|
159
160
|
),
|
|
160
161
|
}
|
|
161
162
|
except Exception as dexc:
|
|
@@ -166,7 +167,7 @@ def register_core_tools(server, get_engine: Callable) -> None:
|
|
|
166
167
|
"code": "DAEMON_UNAVAILABLE",
|
|
167
168
|
"retryable": True,
|
|
168
169
|
"error": (
|
|
169
|
-
|
|
170
|
+
daemon_unavailable_error()
|
|
170
171
|
),
|
|
171
172
|
}
|
|
172
173
|
|
|
@@ -198,14 +199,14 @@ def register_core_tools(server, get_engine: Callable) -> None:
|
|
|
198
199
|
"retryable": True,
|
|
199
200
|
"error": stored.get(
|
|
200
201
|
"error",
|
|
201
|
-
|
|
202
|
+
daemon_unavailable_error(),
|
|
202
203
|
),
|
|
203
204
|
}
|
|
204
205
|
return {
|
|
205
206
|
"success": False,
|
|
206
207
|
"code": "DAEMON_UNAVAILABLE",
|
|
207
208
|
"retryable": True,
|
|
208
|
-
"error":
|
|
209
|
+
"error": daemon_unavailable_error(),
|
|
209
210
|
}
|
|
210
211
|
fact_ids = list(stored.get("fact_ids") or [])
|
|
211
212
|
materialization_state = str(
|
|
@@ -242,7 +243,7 @@ def register_core_tools(server, get_engine: Callable) -> None:
|
|
|
242
243
|
"success": False,
|
|
243
244
|
"code": "DAEMON_UNAVAILABLE",
|
|
244
245
|
"retryable": True,
|
|
245
|
-
"error":
|
|
246
|
+
"error": daemon_unavailable_error(),
|
|
246
247
|
}
|
|
247
248
|
|
|
248
249
|
@server.tool(annotations=ToolAnnotations(readOnlyHint=True))
|