superlocalmemory 4.0.9 → 4.1.0
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 +1 -1
- package/CHANGELOG.md +245 -0
- package/README.md +7 -7
- package/package.json +4 -2
- package/plugin/.claude-plugin/plugin.json +2 -2
- package/plugin/CLAUDE.md +3 -3
- package/plugin/agents/slm-governance-advisor.md +1 -1
- package/plugin/agents/slm-loop-runner.md +4 -4
- 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 +2 -2
- package/plugin/skills/slm-mesh/SKILL.md +1 -1
- package/plugin/skills/slm-profile/SKILL.md +5 -5
- package/plugin/skills/slm-recall/SKILL.md +102 -15
- package/plugin/skills/slm-remember/SKILL.md +35 -3
- package/plugin/skills/slm-scope/SKILL.md +1 -1
- package/plugin/skills/slm-session/SKILL.md +29 -3
- package/plugin/skills/slm-status/SKILL.md +1 -1
- package/plugin-src/rules/AGENTS.md +16 -8
- 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 +2 -2
- package/plugin-src/skills/slm-mesh/SKILL.md +1 -1
- package/plugin-src/skills/slm-profile/SKILL.md +5 -5
- package/plugin-src/skills/slm-recall/SKILL.md +102 -15
- package/plugin-src/skills/slm-remember/SKILL.md +35 -3
- package/plugin-src/skills/slm-scope/SKILL.md +1 -1
- package/plugin-src/skills/slm-session/SKILL.md +29 -3
- 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 +308 -20
- package/src/superlocalmemory/cli/daemon.py +30 -0
- package/src/superlocalmemory/cli/db_migrate.py +71 -1
- package/src/superlocalmemory/cli/gdpr_cmd.py +15 -2
- package/src/superlocalmemory/cli/main.py +26 -4
- package/src/superlocalmemory/code_graph/bridge/maintenance.py +8 -0
- package/src/superlocalmemory/code_graph/database.py +44 -0
- package/src/superlocalmemory/compliance/gdpr.py +449 -39
- package/src/superlocalmemory/core/admission.py +231 -11
- package/src/superlocalmemory/core/backend_orchestrator.py +190 -84
- package/src/superlocalmemory/core/config.py +90 -11
- package/src/superlocalmemory/core/consolidation_engine.py +34 -0
- package/src/superlocalmemory/core/engine.py +140 -11
- package/src/superlocalmemory/core/fact_consolidator.py +316 -125
- package/src/superlocalmemory/core/graph_analyzer.py +76 -112
- package/src/superlocalmemory/core/graph_metrics.py +597 -0
- package/src/superlocalmemory/core/graph_pruner.py +121 -0
- package/src/superlocalmemory/core/maintenance.py +44 -6
- package/src/superlocalmemory/core/maintenance_scheduler.py +205 -0
- package/src/superlocalmemory/core/memory_health.py +266 -0
- package/src/superlocalmemory/core/mode_capability.py +111 -0
- package/src/superlocalmemory/core/ollama_validator.py +315 -0
- package/src/superlocalmemory/core/operation_policy_registry.py +1 -1
- package/src/superlocalmemory/core/operation_request.py +1 -1
- package/src/superlocalmemory/core/ops_remediation.py +2 -2
- package/src/superlocalmemory/core/projection_drain.py +380 -0
- package/src/superlocalmemory/core/recall_pipeline.py +390 -3
- package/src/superlocalmemory/core/recall_worker.py +6 -3
- package/src/superlocalmemory/core/scale_autopromote.py +196 -0
- package/src/superlocalmemory/core/scale_engine.py +16 -2
- package/src/superlocalmemory/core/score_contract.py +21 -1
- package/src/superlocalmemory/core/session_identity.py +85 -0
- package/src/superlocalmemory/core/status_contract.py +108 -0
- package/src/superlocalmemory/core/store_pipeline.py +78 -3
- package/src/superlocalmemory/core/worker_pool.py +4 -4
- package/src/superlocalmemory/core/working_memory.py +288 -0
- package/src/superlocalmemory/encoding/cognitive_consolidator.py +51 -7
- package/src/superlocalmemory/encoding/context_generator.py +1 -1
- package/src/superlocalmemory/encoding/entity_resolver.py +38 -0
- package/src/superlocalmemory/encoding/fact_extractor.py +18 -14
- package/src/superlocalmemory/encoding/prospective_markers.py +262 -0
- package/src/superlocalmemory/encoding/type_router.py +12 -12
- package/src/superlocalmemory/evolution/mutation_generator.py +30 -4
- package/src/superlocalmemory/graph/cozo_adjacency.py +122 -0
- package/src/superlocalmemory/graph/cozo_backend.py +103 -138
- package/src/superlocalmemory/hooks/portable_kit.py +10 -2
- package/src/superlocalmemory/learning/bandit.py +43 -0
- package/src/superlocalmemory/learning/consolidation_worker.py +54 -0
- package/src/superlocalmemory/learning/database.py +60 -3
- package/src/superlocalmemory/learning/entity_compiler.py +21 -58
- package/src/superlocalmemory/learning/feedback.py +3 -1
- package/src/superlocalmemory/learning/outcomes.py +47 -16
- package/src/superlocalmemory/learning/pattern_miner.py +28 -3
- package/src/superlocalmemory/learning/pattern_miner_constants.py +43 -0
- package/src/superlocalmemory/learning/pcos.py +291 -0
- package/src/superlocalmemory/learning/reward_from_outcomes.py +365 -0
- package/src/superlocalmemory/learning/reward_proxy.py +100 -10
- package/src/superlocalmemory/learning/signal_kinds.py +79 -0
- package/src/superlocalmemory/mcp/profiles.py +14 -2
- package/src/superlocalmemory/mcp/server.py +1 -1
- package/src/superlocalmemory/mcp/session_binding.py +92 -0
- package/src/superlocalmemory/mcp/tools_active.py +2 -1
- package/src/superlocalmemory/mcp/tools_core.py +71 -42
- package/src/superlocalmemory/mcp/tools_ops.py +2 -2
- package/src/superlocalmemory/mcp/tools_v28.py +20 -1
- package/src/superlocalmemory/parameterization/pattern_extractor.py +14 -1
- package/src/superlocalmemory/parameterization/soft_prompt_generator.py +98 -0
- package/src/superlocalmemory/retrieval/bm25_channel.py +68 -11
- package/src/superlocalmemory/retrieval/channel_status.py +117 -0
- package/src/superlocalmemory/retrieval/engine.py +106 -11
- package/src/superlocalmemory/retrieval/entity_channel.py +217 -257
- package/src/superlocalmemory/retrieval/graph_adjacency.py +219 -0
- package/src/superlocalmemory/retrieval/scope_policy.py +42 -1
- package/src/superlocalmemory/retrieval/semantic_channel.py +47 -5
- package/src/superlocalmemory/retrieval/spreading.py +288 -0
- package/src/superlocalmemory/retrieval/temporal_channel.py +13 -1
- package/src/superlocalmemory/retrieval/vector_store.py +63 -0
- package/src/superlocalmemory/server/api.py +26 -2
- package/src/superlocalmemory/server/asset_versions.py +171 -0
- package/src/superlocalmemory/server/bandit_loops.py +17 -1
- package/src/superlocalmemory/server/rbac_enforce.py +26 -6
- package/src/superlocalmemory/server/recall_serializer.py +9 -0
- package/src/superlocalmemory/server/routes/abstraction.py +201 -0
- package/src/superlocalmemory/server/routes/behavioral.py +75 -10
- package/src/superlocalmemory/server/routes/compliance.py +98 -18
- package/src/superlocalmemory/server/routes/config_api.py +186 -4
- package/src/superlocalmemory/server/routes/data_io.py +29 -1
- package/src/superlocalmemory/server/routes/entity.py +13 -1
- package/src/superlocalmemory/server/routes/evolution.py +178 -0
- package/src/superlocalmemory/server/routes/ingest.py +8 -0
- package/src/superlocalmemory/server/routes/learning_telemetry.py +2 -1
- package/src/superlocalmemory/server/routes/memories.py +49 -7
- package/src/superlocalmemory/server/routes/mesh.py +1 -1
- package/src/superlocalmemory/server/routes/timeline.py +4 -0
- package/src/superlocalmemory/server/routes/v3_api.py +193 -17
- package/src/superlocalmemory/server/ui.py +24 -1
- package/src/superlocalmemory/server/unified_daemon.py +292 -9
- package/src/superlocalmemory/storage/_migration_internals.py +35 -0
- package/src/superlocalmemory/storage/_schema_version.py +24 -3
- package/src/superlocalmemory/storage/database.py +598 -82
- package/src/superlocalmemory/storage/embedding_codec.py +71 -0
- package/src/superlocalmemory/storage/lineage_retention.py +236 -0
- package/src/superlocalmemory/storage/logical_edges.py +43 -2
- package/src/superlocalmemory/storage/migration_runner.py +130 -0
- package/src/superlocalmemory/storage/migrations/M043_quarantine_display_summaries.py +488 -0
- package/src/superlocalmemory/storage/migrations/M044_play_carries_its_own_evidence.py +127 -0
- package/src/superlocalmemory/storage/migrations/M045_fact_outcome_score.py +158 -0
- package/src/superlocalmemory/storage/migrations/M046_prospective_memory_has_its_own_name.py +620 -0
- package/src/superlocalmemory/storage/migrations/M047_fisher_vectors_are_stored_like_every_other_vector.py +306 -0
- package/src/superlocalmemory/storage/migrations/M048_upcoming_holds_only_what_is_upcoming.py +207 -0
- package/src/superlocalmemory/storage/migrations/M049_a_schema_version_marker_is_one_row.py +201 -0
- package/src/superlocalmemory/storage/migrations.py +18 -2
- package/src/superlocalmemory/storage/models.py +40 -1
- package/src/superlocalmemory/storage/projection_outbox.py +346 -0
- package/src/superlocalmemory/storage/retention_policy.py +860 -0
- package/src/superlocalmemory/storage/schema.py +110 -1
- package/src/superlocalmemory/storage/write_coordinator.py +19 -2
- package/src/superlocalmemory/summaries/base.py +1 -1
- package/src/superlocalmemory/summaries/non_answer.py +223 -0
- package/src/superlocalmemory/trust/scorer.py +43 -1
- package/src/superlocalmemory/ui/index.html +10 -19
- package/src/superlocalmemory/ui/js/event-delegation.js +12 -1
- package/src/superlocalmemory/ui/js/od-health.js +28 -6
- package/src/superlocalmemory/ui/js/od-memories.js +209 -1
- package/src/superlocalmemory/ui/js/od-ops-health.js +1 -1
- package/src/superlocalmemory/ui/js/od-settings.js +87 -1
- package/src/superlocalmemory/ui/js/recall-lab.js +78 -3
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
# Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
|
|
2
|
+
# Licensed under AGPL-3.0-or-later - see LICENSE file
|
|
3
|
+
|
|
4
|
+
"""Settle a bandit play from an authenticated outcome.
|
|
5
|
+
|
|
6
|
+
WHY THIS FILE EXISTS
|
|
7
|
+
--------------------
|
|
8
|
+
``reward_proxy.py`` has said since v3.4.22:
|
|
9
|
+
|
|
10
|
+
Replaced in v3.4.22 by ``reward_from_outcomes.py`` — DO NOT extend this
|
|
11
|
+
module beyond the proxy window contract.
|
|
12
|
+
|
|
13
|
+
That file was never written. So the replacement never arrived, and the module
|
|
14
|
+
that tells you not to extend it remained the only settlement path — one that
|
|
15
|
+
cannot work. Measured, not inferred:
|
|
16
|
+
|
|
17
|
+
* ``reward_proxy._tool_event_hit`` selects ``payload_json`` from ``tool_events``
|
|
18
|
+
``WHERE occurred_at BETWEEN ? AND ?``. **Neither column exists** on that
|
|
19
|
+
table, on this install or in any DDL in the codebase. The query raises
|
|
20
|
+
``no such column: payload_json``, a bare ``except sqlite3.Error`` returns
|
|
21
|
+
False, and the "cited" branch of the ladder is therefore unreachable.
|
|
22
|
+
* Even with the names corrected there would be nothing to find: **0 of 2,002**
|
|
23
|
+
``tool_events`` rows on a live store contain a 16-hex token, let alone a
|
|
24
|
+
real ``fact_id``. That table records tool names and summaries, not the
|
|
25
|
+
memories a recall returned.
|
|
26
|
+
|
|
27
|
+
So every settlement fell through to the 120-second default of 0.5, which is
|
|
28
|
+
visible in the arms as an exact tie::
|
|
29
|
+
|
|
30
|
+
SUM(alpha) = SUM(beta) = 867.5 = 165 priors + 1,405 plays x 0.5
|
|
31
|
+
|
|
32
|
+
WHAT THIS USES INSTEAD
|
|
33
|
+
----------------------
|
|
34
|
+
``action_outcomes`` (memory.db, M006) — the table an *explicit* outcome report
|
|
35
|
+
already writes, with a reward the reporter chose:
|
|
36
|
+
``success=1.0 / failure=0.0 / partial=0.5``. That is an authenticated signal
|
|
37
|
+
about whether the memories helped, which is exactly what a play needs and what
|
|
38
|
+
an exposure is not.
|
|
39
|
+
|
|
40
|
+
It carries ``recall_query_id``, a join key straight to ``bandit_plays.query_id``.
|
|
41
|
+
**0 of 162 rows populate it** — the column was added and never written. This
|
|
42
|
+
module reads it when present, and otherwise falls back to overlap between the
|
|
43
|
+
outcome's ``fact_ids_json`` and the ``shown_fact_ids`` the play recorded (M044).
|
|
44
|
+
|
|
45
|
+
The fallback is the load-bearing path, not a nicety: recall does not return its
|
|
46
|
+
``query_id`` to the caller (``server/recall_serializer.py`` has no such field),
|
|
47
|
+
so no existing client *can* supply one. Overlap on the memories actually shown,
|
|
48
|
+
inside a time window, is the strongest link available without changing every
|
|
49
|
+
caller first.
|
|
50
|
+
|
|
51
|
+
THE GRACE WINDOW, AND WHY IT IS NOT 120 SECONDS
|
|
52
|
+
-----------------------------------------------
|
|
53
|
+
``reward_proxy`` defaults a play to 0.5 once it is 120 seconds old. A human or
|
|
54
|
+
an agent deciding whether a memory helped does not do so within two minutes, so
|
|
55
|
+
that deadline would claim every play before any real outcome arrived — the
|
|
56
|
+
default would win a race it should not be in.
|
|
57
|
+
|
|
58
|
+
A play that recorded ``shown_fact_ids`` therefore waits ``_GRACE_SEC`` (default
|
|
59
|
+
900) before it may be defaulted. A play with no recorded facts keeps the old
|
|
60
|
+
120-second behaviour, because nothing can ever settle it from evidence and
|
|
61
|
+
holding it open buys nothing.
|
|
62
|
+
|
|
63
|
+
NOTHING HERE FABRICATES A REWARD. If no outcome is reported, the play settles
|
|
64
|
+
neutral and is *labelled* ``default`` so it stays distinguishable from a
|
|
65
|
+
genuinely neutral outcome. Those two were indistinguishable before, which is why
|
|
66
|
+
this went unnoticed for months.
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
from __future__ import annotations
|
|
70
|
+
|
|
71
|
+
import json
|
|
72
|
+
import logging
|
|
73
|
+
import os
|
|
74
|
+
import sqlite3
|
|
75
|
+
from datetime import datetime, timedelta, timezone
|
|
76
|
+
from pathlib import Path
|
|
77
|
+
|
|
78
|
+
from superlocalmemory.learning.bandit import ContextualBandit
|
|
79
|
+
from superlocalmemory.learning.pcos import update_scores
|
|
80
|
+
|
|
81
|
+
logger = logging.getLogger(__name__)
|
|
82
|
+
|
|
83
|
+
__all__ = ["settle_from_outcomes", "GRACE_SEC", "SETTLEMENT_KIND"]
|
|
84
|
+
|
|
85
|
+
#: How long a play that recorded its shown memories waits for a reported
|
|
86
|
+
#: outcome before it may be defaulted. Deliberately far longer than the proxy's
|
|
87
|
+
#: 120 s: see the module docstring.
|
|
88
|
+
GRACE_SEC: float = float(os.environ.get("SLM_OUTCOME_GRACE_SEC", "900"))
|
|
89
|
+
|
|
90
|
+
#: How far after a play an outcome may arrive and still be attributed to it.
|
|
91
|
+
#: Same order as the grace window — an outcome reported later than this is about
|
|
92
|
+
#: something else.
|
|
93
|
+
_MATCH_WINDOW_SEC: float = GRACE_SEC
|
|
94
|
+
|
|
95
|
+
#: Written to ``bandit_plays.settlement_type``. A settlement that came from a
|
|
96
|
+
#: real report must never be mistaken for the neutral default.
|
|
97
|
+
SETTLEMENT_KIND = "outcome_reported"
|
|
98
|
+
|
|
99
|
+
_MAX_PLAYS_PER_PASS = 500
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _parse_iso(ts: str | None) -> datetime | None:
|
|
103
|
+
if not ts:
|
|
104
|
+
return None
|
|
105
|
+
raw = str(ts).replace("Z", "+00:00")
|
|
106
|
+
try:
|
|
107
|
+
dt = datetime.fromisoformat(raw)
|
|
108
|
+
except ValueError:
|
|
109
|
+
return None
|
|
110
|
+
return dt if dt.tzinfo else dt.replace(tzinfo=timezone.utc)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _open(path: Path) -> sqlite3.Connection | None:
|
|
114
|
+
try:
|
|
115
|
+
conn = sqlite3.connect(str(path), timeout=5.0)
|
|
116
|
+
conn.row_factory = sqlite3.Row
|
|
117
|
+
return conn
|
|
118
|
+
except sqlite3.Error as exc:
|
|
119
|
+
logger.debug("reward_from_outcomes: open %s failed: %s", path, exc)
|
|
120
|
+
return None
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _shown(raw: object) -> set[str]:
|
|
124
|
+
"""``shown_fact_ids`` JSON -> set. Empty on anything unexpected."""
|
|
125
|
+
if not raw:
|
|
126
|
+
return set()
|
|
127
|
+
try:
|
|
128
|
+
parsed = json.loads(str(raw))
|
|
129
|
+
except (TypeError, ValueError):
|
|
130
|
+
return set()
|
|
131
|
+
if not isinstance(parsed, list):
|
|
132
|
+
return set()
|
|
133
|
+
return {str(f) for f in parsed if f}
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _unsettled_plays(
|
|
137
|
+
conn: sqlite3.Connection, profile_id: str,
|
|
138
|
+
) -> list[sqlite3.Row]:
|
|
139
|
+
"""Unsettled plays, oldest first. Degrades if M044 has not run."""
|
|
140
|
+
for columns in (
|
|
141
|
+
"play_id, query_id, played_at, shown_fact_ids",
|
|
142
|
+
"play_id, query_id, played_at, NULL AS shown_fact_ids",
|
|
143
|
+
):
|
|
144
|
+
try:
|
|
145
|
+
return conn.execute(
|
|
146
|
+
f"SELECT {columns} FROM bandit_plays "
|
|
147
|
+
"WHERE profile_id = ? AND settled_at IS NULL "
|
|
148
|
+
"ORDER BY played_at ASC LIMIT ?",
|
|
149
|
+
(str(profile_id), _MAX_PLAYS_PER_PASS),
|
|
150
|
+
).fetchall()
|
|
151
|
+
except sqlite3.Error as exc:
|
|
152
|
+
logger.debug("reward_from_outcomes: play fetch (%s): %s", columns, exc)
|
|
153
|
+
return []
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _candidate_outcomes(
|
|
157
|
+
conn: sqlite3.Connection, profile_id: str, since: datetime,
|
|
158
|
+
) -> list[dict]:
|
|
159
|
+
"""Reported outcomes from ``since`` onward, newest first.
|
|
160
|
+
|
|
161
|
+
Read once per pass rather than once per play: the window holds a handful of
|
|
162
|
+
rows in practice, and a per-play query would scan this table N times on the
|
|
163
|
+
settlement thread.
|
|
164
|
+
"""
|
|
165
|
+
try:
|
|
166
|
+
rows = conn.execute(
|
|
167
|
+
"SELECT outcome_id, fact_ids_json, outcome, reward, timestamp, "
|
|
168
|
+
" recall_query_id "
|
|
169
|
+
"FROM action_outcomes "
|
|
170
|
+
"WHERE profile_id = ? AND timestamp >= ? "
|
|
171
|
+
"ORDER BY timestamp DESC LIMIT 2000",
|
|
172
|
+
(str(profile_id), since.isoformat()),
|
|
173
|
+
).fetchall()
|
|
174
|
+
except sqlite3.Error as exc:
|
|
175
|
+
logger.debug("reward_from_outcomes: outcome fetch: %s", exc)
|
|
176
|
+
return []
|
|
177
|
+
|
|
178
|
+
out: list[dict] = []
|
|
179
|
+
for r in rows:
|
|
180
|
+
at = _parse_iso(r["timestamp"])
|
|
181
|
+
if at is None:
|
|
182
|
+
continue
|
|
183
|
+
try:
|
|
184
|
+
facts = json.loads(r["fact_ids_json"] or "[]")
|
|
185
|
+
except (TypeError, ValueError):
|
|
186
|
+
facts = []
|
|
187
|
+
if not isinstance(facts, list):
|
|
188
|
+
facts = []
|
|
189
|
+
try:
|
|
190
|
+
reward = float(r["reward"])
|
|
191
|
+
except (TypeError, ValueError):
|
|
192
|
+
continue
|
|
193
|
+
out.append({
|
|
194
|
+
"id": str(r["outcome_id"]),
|
|
195
|
+
"at": at,
|
|
196
|
+
"facts": {str(f) for f in facts if f},
|
|
197
|
+
"reward": max(0.0, min(1.0, reward)),
|
|
198
|
+
"query_id": str(r["recall_query_id"] or ""),
|
|
199
|
+
})
|
|
200
|
+
return out
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _match(
|
|
204
|
+
play_at: datetime,
|
|
205
|
+
query_id: str,
|
|
206
|
+
shown: set[str],
|
|
207
|
+
outcomes: list[dict],
|
|
208
|
+
claimed: set[str],
|
|
209
|
+
) -> dict | None:
|
|
210
|
+
"""The one outcome attributable to this play, or None.
|
|
211
|
+
|
|
212
|
+
Returns the outcome itself rather than its reward, because the caller needs
|
|
213
|
+
to know WHICH memories that outcome named — see ``settle_from_outcomes``.
|
|
214
|
+
|
|
215
|
+
AN OUTCOME IS EVIDENCE ABOUT ONE RECALL, AND IS CONSUMED
|
|
216
|
+
-------------------------------------------------------
|
|
217
|
+
The first version of this matched an outcome against every unsettled play
|
|
218
|
+
whose shown memories intersected it, and never consumed it. Both audits
|
|
219
|
+
reproduced the consequence and it is severe: five recalls that happened to
|
|
220
|
+
display the same memory, plus one reported outcome, settled **five** plays
|
|
221
|
+
and moved five arms; twenty recalls settled twenty. One person saying "that
|
|
222
|
+
helped" became twenty pieces of evidence about twenty different retrieval
|
|
223
|
+
strategies, most of which had nothing to do with it.
|
|
224
|
+
|
|
225
|
+
It also inflated confidence. ``play_count`` is what
|
|
226
|
+
``pcos.confidence_weight`` reads, so the same fan-out could push a memory to
|
|
227
|
+
"proven" — full bonus — on a single judgement.
|
|
228
|
+
|
|
229
|
+
The effect on the learning claim is the part that matters: Thompson sampling
|
|
230
|
+
would be learning which memories were *nearby*, not which strategy helped.
|
|
231
|
+
That is how arms come off their prior and the ranker is still random.
|
|
232
|
+
|
|
233
|
+
So ``claimed`` carries the outcome ids already spent this pass, and an
|
|
234
|
+
outcome may be spent once. Plays are offered outcomes oldest-first, so the
|
|
235
|
+
recall that ran first gets the credit — the report is far more likely to be
|
|
236
|
+
about the answer the user actually saw.
|
|
237
|
+
|
|
238
|
+
THE EXACT PATH IS ALSO BOUNDED IN TIME. A ``recall_query_id`` is a stronger
|
|
239
|
+
statement of intent than overlap and is tried first, but it used to skip the
|
|
240
|
+
horizon check entirely: an outcome ten days later, naming completely
|
|
241
|
+
different memories, settled a long-closed play at reward 0.0. Reproduced.
|
|
242
|
+
Both paths now require the outcome to fall inside the window.
|
|
243
|
+
"""
|
|
244
|
+
horizon = play_at + timedelta(seconds=_MATCH_WINDOW_SEC)
|
|
245
|
+
|
|
246
|
+
def _in_window(o: dict) -> bool:
|
|
247
|
+
return play_at <= o["at"] <= horizon
|
|
248
|
+
|
|
249
|
+
def _first(candidates: list[dict]) -> dict | None:
|
|
250
|
+
fresh = [o for o in candidates if o["id"] not in claimed]
|
|
251
|
+
return min(fresh, key=lambda o: o["at"]) if fresh else None
|
|
252
|
+
|
|
253
|
+
if query_id:
|
|
254
|
+
exact = _first([
|
|
255
|
+
o for o in outcomes
|
|
256
|
+
if o["query_id"] and o["query_id"] == query_id and _in_window(o)
|
|
257
|
+
])
|
|
258
|
+
if exact is not None:
|
|
259
|
+
return exact
|
|
260
|
+
|
|
261
|
+
if not shown:
|
|
262
|
+
return None
|
|
263
|
+
return _first([
|
|
264
|
+
o for o in outcomes if _in_window(o) and (o["facts"] & shown)
|
|
265
|
+
])
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def settle_from_outcomes(
|
|
269
|
+
profile_id: str,
|
|
270
|
+
learning_db: Path | str,
|
|
271
|
+
memory_db: Path | str,
|
|
272
|
+
*,
|
|
273
|
+
now: datetime | None = None,
|
|
274
|
+
bandit: ContextualBandit | None = None,
|
|
275
|
+
) -> int:
|
|
276
|
+
"""Settle plays from reported outcomes. Returns the count. Never raises.
|
|
277
|
+
|
|
278
|
+
Run this BEFORE ``reward_proxy.settle_stale_plays`` on any pass. The proxy
|
|
279
|
+
defaults a play at 120 seconds; if it goes first it takes every play with
|
|
280
|
+
it and a reported outcome arriving at minute five finds nothing left to
|
|
281
|
+
settle.
|
|
282
|
+
"""
|
|
283
|
+
current = now or datetime.now(timezone.utc)
|
|
284
|
+
learning_conn = _open(Path(learning_db))
|
|
285
|
+
if learning_conn is None:
|
|
286
|
+
return 0
|
|
287
|
+
memory_conn = _open(Path(memory_db))
|
|
288
|
+
if memory_conn is None:
|
|
289
|
+
learning_conn.close()
|
|
290
|
+
return 0
|
|
291
|
+
|
|
292
|
+
owns_bandit = bandit is None
|
|
293
|
+
if bandit is None:
|
|
294
|
+
bandit = ContextualBandit(Path(learning_db), profile_id=str(profile_id))
|
|
295
|
+
|
|
296
|
+
settled = 0
|
|
297
|
+
try:
|
|
298
|
+
plays = _unsettled_plays(learning_conn, profile_id)
|
|
299
|
+
if not plays:
|
|
300
|
+
return 0
|
|
301
|
+
oldest = min(
|
|
302
|
+
(p for p in (_parse_iso(r["played_at"]) for r in plays)
|
|
303
|
+
if p is not None),
|
|
304
|
+
default=current,
|
|
305
|
+
)
|
|
306
|
+
outcomes = _candidate_outcomes(memory_conn, profile_id, oldest)
|
|
307
|
+
if not outcomes:
|
|
308
|
+
return 0
|
|
309
|
+
|
|
310
|
+
# Outcome ids spent this pass. An outcome is evidence about one recall.
|
|
311
|
+
claimed: set[str] = set()
|
|
312
|
+
|
|
313
|
+
for row in plays:
|
|
314
|
+
played = _parse_iso(row["played_at"])
|
|
315
|
+
if played is None:
|
|
316
|
+
continue
|
|
317
|
+
shown = _shown(row["shown_fact_ids"])
|
|
318
|
+
outcome = _match(
|
|
319
|
+
played, str(row["query_id"] or ""), shown, outcomes, claimed,
|
|
320
|
+
)
|
|
321
|
+
if outcome is None:
|
|
322
|
+
continue
|
|
323
|
+
if bandit.update(int(row["play_id"]), outcome["reward"],
|
|
324
|
+
kind=SETTLEMENT_KIND):
|
|
325
|
+
settled += 1
|
|
326
|
+
claimed.add(outcome["id"])
|
|
327
|
+
# Credit ONLY the memories the outcome actually named. Writing
|
|
328
|
+
# it to everything the play displayed spread one judgement
|
|
329
|
+
# across up to five memories: an outcome naming one of them
|
|
330
|
+
# moved all five to 0.55 with play_count 1. Co-displayed
|
|
331
|
+
# memories inherited credit, the bonus then lifted them, and
|
|
332
|
+
# they were shown again — the rich-get-richer path this was
|
|
333
|
+
# built to prevent, one hop removed.
|
|
334
|
+
judged = sorted(outcome["facts"] & shown) if shown else []
|
|
335
|
+
if judged:
|
|
336
|
+
try:
|
|
337
|
+
update_scores(
|
|
338
|
+
memory_conn, profile_id, judged, outcome["reward"],
|
|
339
|
+
)
|
|
340
|
+
memory_conn.commit()
|
|
341
|
+
except Exception as exc: # pragma: no cover — defensive
|
|
342
|
+
logger.debug("pcos update skipped: %s", exc)
|
|
343
|
+
except sqlite3.Error as exc: # pragma: no cover — defensive
|
|
344
|
+
logger.warning("reward_from_outcomes: %s", exc)
|
|
345
|
+
finally:
|
|
346
|
+
for conn in (learning_conn, memory_conn):
|
|
347
|
+
try:
|
|
348
|
+
conn.close()
|
|
349
|
+
except sqlite3.Error: # pragma: no cover
|
|
350
|
+
pass
|
|
351
|
+
if owns_bandit:
|
|
352
|
+
try:
|
|
353
|
+
from superlocalmemory.learning.bandit import (
|
|
354
|
+
close_threadlocal_conn,
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
close_threadlocal_conn()
|
|
358
|
+
except Exception: # pragma: no cover — defensive
|
|
359
|
+
pass
|
|
360
|
+
if settled:
|
|
361
|
+
logger.info(
|
|
362
|
+
"reward_from_outcomes: settled %d play(s) from reported outcomes",
|
|
363
|
+
settled,
|
|
364
|
+
)
|
|
365
|
+
return settled
|
|
@@ -27,6 +27,7 @@ Hard rules:
|
|
|
27
27
|
|
|
28
28
|
from __future__ import annotations
|
|
29
29
|
|
|
30
|
+
import json
|
|
30
31
|
import logging
|
|
31
32
|
import sqlite3
|
|
32
33
|
from datetime import datetime, timedelta, timezone
|
|
@@ -76,18 +77,62 @@ def _fetch_unsettled(
|
|
|
76
77
|
now: datetime,
|
|
77
78
|
) -> list[sqlite3.Row]:
|
|
78
79
|
try:
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
"
|
|
83
|
-
"
|
|
84
|
-
|
|
85
|
-
|
|
80
|
+
# shown_fact_ids arrives with M044; the NULL alias keeps this working
|
|
81
|
+
# on a store where it has not run yet.
|
|
82
|
+
for columns in (
|
|
83
|
+
"play_id, query_id, played_at, stratum, shown_fact_ids",
|
|
84
|
+
"play_id, query_id, played_at, stratum, NULL AS shown_fact_ids",
|
|
85
|
+
):
|
|
86
|
+
try:
|
|
87
|
+
return learning_conn.execute(
|
|
88
|
+
f"SELECT {columns} FROM bandit_plays "
|
|
89
|
+
"WHERE profile_id = ? AND settled_at IS NULL "
|
|
90
|
+
"ORDER BY played_at ASC LIMIT 500",
|
|
91
|
+
(profile_id,),
|
|
92
|
+
).fetchall()
|
|
93
|
+
except sqlite3.Error:
|
|
94
|
+
continue
|
|
95
|
+
return []
|
|
86
96
|
except sqlite3.Error as exc:
|
|
87
97
|
logger.debug("reward_proxy: fetch_unsettled: %s", exc)
|
|
88
98
|
return []
|
|
89
99
|
|
|
90
100
|
|
|
101
|
+
def _shown_fact_ids(
|
|
102
|
+
learning_conn: sqlite3.Connection,
|
|
103
|
+
play_id: int,
|
|
104
|
+
) -> list[str]:
|
|
105
|
+
"""The fact_ids this play recorded at recall time (M044), or [].
|
|
106
|
+
|
|
107
|
+
Preferred over the ``learning_signals`` lookup below because it does not
|
|
108
|
+
depend on the exposure enqueue, which is off: twenty rows per query, and
|
|
109
|
+
the source of a 2,675x inflation in the ranking-phase counter. With no
|
|
110
|
+
signals rows there was nothing to look for, so every play settled as the
|
|
111
|
+
120-second default and 165 arms sat at alpha == beta.
|
|
112
|
+
|
|
113
|
+
Returns [] on a store where M044 has not run — "no such column" is an
|
|
114
|
+
``sqlite3.Error`` and is caught, so an unmigrated install falls back to the
|
|
115
|
+
old lookup rather than losing settlement entirely.
|
|
116
|
+
"""
|
|
117
|
+
try:
|
|
118
|
+
row = learning_conn.execute(
|
|
119
|
+
"SELECT shown_fact_ids FROM bandit_plays WHERE play_id = ?",
|
|
120
|
+
(int(play_id),),
|
|
121
|
+
).fetchone()
|
|
122
|
+
except sqlite3.Error:
|
|
123
|
+
return []
|
|
124
|
+
raw = (row[0] if row else None) or ""
|
|
125
|
+
if not raw:
|
|
126
|
+
return []
|
|
127
|
+
try:
|
|
128
|
+
parsed = json.loads(raw)
|
|
129
|
+
except (TypeError, ValueError):
|
|
130
|
+
return []
|
|
131
|
+
if not isinstance(parsed, list):
|
|
132
|
+
return []
|
|
133
|
+
return [str(f) for f in parsed[:3] if f]
|
|
134
|
+
|
|
135
|
+
|
|
91
136
|
def _top3_fact_ids(
|
|
92
137
|
learning_conn: sqlite3.Connection,
|
|
93
138
|
query_id: str,
|
|
@@ -95,6 +140,8 @@ def _top3_fact_ids(
|
|
|
95
140
|
"""Return up to 3 fact_ids for this query_id ordered by position ASC.
|
|
96
141
|
|
|
97
142
|
Returns [] if learning_signals is missing or has no rows for this qid.
|
|
143
|
+
Kept as the fallback for plays written before M044 and for installs that
|
|
144
|
+
do run the exposure enqueue.
|
|
98
145
|
"""
|
|
99
146
|
try:
|
|
100
147
|
rows = learning_conn.execute(
|
|
@@ -281,6 +328,44 @@ def _extract_query(payload_json: str | None) -> str:
|
|
|
281
328
|
return ""
|
|
282
329
|
|
|
283
330
|
|
|
331
|
+
def _default_deadline(
|
|
332
|
+
learning_conn: sqlite3.Connection, play: sqlite3.Row,
|
|
333
|
+
) -> float:
|
|
334
|
+
"""Age in seconds at which this play may be defaulted to 0.5.
|
|
335
|
+
|
|
336
|
+
120 s for a play that recorded nothing — no evidence can ever arrive for
|
|
337
|
+
it, so holding it open buys nothing but an unsettled row.
|
|
338
|
+
|
|
339
|
+
Longer for a play that recorded which memories it showed (M044), because a
|
|
340
|
+
reported outcome CAN settle it and two minutes is not long enough for
|
|
341
|
+
anyone to decide whether an answer helped. Defaulting first would make the
|
|
342
|
+
neutral fallback win a race it should not be in — which is how 1,405
|
|
343
|
+
consecutive plays came to apply exactly 0.5.
|
|
344
|
+
"""
|
|
345
|
+
try:
|
|
346
|
+
from superlocalmemory.learning.reward_from_outcomes import GRACE_SEC
|
|
347
|
+
except Exception: # pragma: no cover — defensive
|
|
348
|
+
return float(_MAX_AGE_SEC)
|
|
349
|
+
try:
|
|
350
|
+
raw = play["shown_fact_ids"]
|
|
351
|
+
except (IndexError, KeyError):
|
|
352
|
+
return float(_MAX_AGE_SEC)
|
|
353
|
+
# PARSE, do not test the raw string. "[]" is truthy, so a play that recorded
|
|
354
|
+
# no memories used to buy the full grace window it could never use: nothing
|
|
355
|
+
# can overlap an empty set, so it stayed unsettled forever, and the
|
|
356
|
+
# retention sweep only removes SETTLED rows. Reproduced: "[]" returned
|
|
357
|
+
# 900 s where it should return 120 s.
|
|
358
|
+
try:
|
|
359
|
+
parsed = json.loads(raw) if raw else []
|
|
360
|
+
except (TypeError, ValueError):
|
|
361
|
+
parsed = []
|
|
362
|
+
has_evidence = bool(parsed) if isinstance(parsed, list) else False
|
|
363
|
+
return (
|
|
364
|
+
max(float(_MAX_AGE_SEC), GRACE_SEC) if has_evidence
|
|
365
|
+
else float(_MAX_AGE_SEC)
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
|
|
284
369
|
def settle_stale_plays(
|
|
285
370
|
profile_id: str,
|
|
286
371
|
db_path: Path | str,
|
|
@@ -315,7 +400,12 @@ def settle_stale_plays(
|
|
|
315
400
|
if age < _MIN_AGE_SEC:
|
|
316
401
|
continue # not yet settleable
|
|
317
402
|
|
|
318
|
-
|
|
403
|
+
# The play's own record first; the signals table only as a
|
|
404
|
+
# fallback for rows written before M044.
|
|
405
|
+
top3 = (
|
|
406
|
+
_shown_fact_ids(learning_conn, row["play_id"])
|
|
407
|
+
or _top3_fact_ids(learning_conn, row["query_id"])
|
|
408
|
+
)
|
|
319
409
|
reward: float | None = None
|
|
320
410
|
kind = "default"
|
|
321
411
|
if memory_conn is not None and _tool_event_hit(
|
|
@@ -328,8 +418,8 @@ def settle_stale_plays(
|
|
|
328
418
|
):
|
|
329
419
|
reward = 0.0
|
|
330
420
|
kind = "proxy_requery"
|
|
331
|
-
elif age >
|
|
332
|
-
# P1: uncertain default
|
|
421
|
+
elif age > _default_deadline(learning_conn, row):
|
|
422
|
+
# P1: uncertain default once the window closes.
|
|
333
423
|
reward = 0.5
|
|
334
424
|
kind = "default"
|
|
335
425
|
else:
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
|
|
2
|
+
# Licensed under AGPL-3.0-or-later - see LICENSE file
|
|
3
|
+
|
|
4
|
+
"""Which ``learning_signals`` rows are feedback, and which are just exposure.
|
|
5
|
+
|
|
6
|
+
THE DISTINCTION, AND WHY IT DECIDES A RANKING PHASE
|
|
7
|
+
---------------------------------------------------
|
|
8
|
+
An **exposure** records that a fact was shown. A **feedback** signal records
|
|
9
|
+
that something happened afterwards. Only the second says anything about whether
|
|
10
|
+
the memory was any good, and the ranker's phase gate is meant to count the
|
|
11
|
+
second.
|
|
12
|
+
|
|
13
|
+
It counted both. Measured on a live ``learning.db``::
|
|
14
|
+
|
|
15
|
+
candidate 5,350 exposure — one row per fact shown at recall
|
|
16
|
+
legacy_feedback 2 the only real feedback in the table
|
|
17
|
+
─────
|
|
18
|
+
5,352 what count_signals() returned
|
|
19
|
+
|
|
20
|
+
An inflation of 2,675x. Every surface that resolves a phase from that number
|
|
21
|
+
believed it had Phase 3 data — LightGBM active — on two feedback events, and
|
|
22
|
+
the model it activated had been trained on 972 rows whose labels were all 0.0.
|
|
23
|
+
So it reordered results at random and outranked the heuristic that would
|
|
24
|
+
otherwise have been used.
|
|
25
|
+
|
|
26
|
+
Correcting the count drops the system to Phase 1 (heuristic). That is not a
|
|
27
|
+
regression; it is the honest state, and the heuristic is the better ranker of
|
|
28
|
+
the two.
|
|
29
|
+
|
|
30
|
+
WHY A SHARED SET RATHER THAN A LITERAL AT EACH CALL SITE
|
|
31
|
+
--------------------------------------------------------
|
|
32
|
+
There are two ``count_signals()`` implementations — ``learning/database.py``
|
|
33
|
+
and the read-only view in ``core/recall_pipeline.py`` — and a phase computed
|
|
34
|
+
from one is compared against a threshold computed from the other. Two literals
|
|
35
|
+
is how they drift.
|
|
36
|
+
|
|
37
|
+
This module deliberately has no imports: ``_ReadOnlyLearningView`` exists to
|
|
38
|
+
read a learning DB *without* being able to initialise or mutate one, so it must
|
|
39
|
+
not pull the writer module in just to learn a set of strings.
|
|
40
|
+
|
|
41
|
+
EXCLUDING EXPOSURES RATHER THAN NAMING FEEDBACK. A new feedback kind (``dwell``,
|
|
42
|
+
``explicit``, ``cited``) must count the day it is introduced, without anyone
|
|
43
|
+
remembering to add it here — so the predicate is a NOT IN over the exposure
|
|
44
|
+
kinds, not an IN over the feedback kinds. The failure modes are not symmetric:
|
|
45
|
+
forgetting to add a feedback kind under-counts and quietly holds the ranker in
|
|
46
|
+
an earlier phase, while forgetting to add an exposure kind re-creates the 2,675x
|
|
47
|
+
inflation above.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
from __future__ import annotations
|
|
51
|
+
|
|
52
|
+
__all__ = ["EXPOSURE_SIGNAL_TYPES", "FEEDBACK_ONLY_SQL", "is_feedback"]
|
|
53
|
+
|
|
54
|
+
#: Signal kinds that record only that a fact was displayed.
|
|
55
|
+
#:
|
|
56
|
+
#: ``candidate`` is written once per fact per recall. ``shown`` is its sibling
|
|
57
|
+
#: in ``_fetch_training_rows``; there are no rows of it on a live store,
|
|
58
|
+
#: and it is listed here because the alternative is a predicate that counts it
|
|
59
|
+
#: as feedback the moment one appears.
|
|
60
|
+
EXPOSURE_SIGNAL_TYPES: frozenset[str] = frozenset({"candidate", "shown"})
|
|
61
|
+
|
|
62
|
+
#: AND-clause selecting feedback rows only. ``signal_type`` is ``NOT NULL`` in
|
|
63
|
+
#: the schema, so no COALESCE is needed — verified against the table definition
|
|
64
|
+
#: rather than assumed, because ``x != 'candidate'`` is NULL (and therefore
|
|
65
|
+
#: false) for a NULL x, which would silently drop rows.
|
|
66
|
+
FEEDBACK_ONLY_SQL: str = " AND signal_type NOT IN ({})".format(
|
|
67
|
+
", ".join(f"'{kind}'" for kind in sorted(EXPOSURE_SIGNAL_TYPES))
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def is_feedback(signal_type: str | None) -> bool:
|
|
72
|
+
"""Whether ``signal_type`` counts toward the ranker's phase gate.
|
|
73
|
+
|
|
74
|
+
The Python twin of :data:`FEEDBACK_ONLY_SQL`, for callers holding a row
|
|
75
|
+
rather than building a query. A missing type counts as feedback: it is not
|
|
76
|
+
one of the two known exposure writers, and under-counting is the failure
|
|
77
|
+
mode that hides a phase transition.
|
|
78
|
+
"""
|
|
79
|
+
return (signal_type or "") not in EXPOSURE_SIGNAL_TYPES
|
|
@@ -17,7 +17,7 @@ from __future__ import annotations
|
|
|
17
17
|
# Named profile definitions (introduced in v3.6.14)
|
|
18
18
|
# ---------------------------------------------------------------------------
|
|
19
19
|
|
|
20
|
-
_PROFILE_CORE: frozenset[str] = frozenset({ #
|
|
20
|
+
_PROFILE_CORE: frozenset[str] = frozenset({ # 18
|
|
21
21
|
"remember", "recall", "search", "fetch", "list_recent", "update_memory", "forget",
|
|
22
22
|
"session_init", "close_session",
|
|
23
23
|
"slm_compress", "slm_retrieve", "slm_cache_set", "slm_cache_get", "slm_optimize_stats",
|
|
@@ -28,6 +28,13 @@ _PROFILE_CORE: frozenset[str] = frozenset({ # 17
|
|
|
28
28
|
# natural caller is the agent holding the conversation — an assistant
|
|
29
29
|
# asked "what did I work on yesterday" should not need a power profile.
|
|
30
30
|
"get_memory_summary",
|
|
31
|
+
# v4.1.0: every memory operation is scoped to a profile, so a surface that
|
|
32
|
+
# can read or write memory must also be able to say which profile it means.
|
|
33
|
+
# Without this, the smallest profile can store and recall but can never
|
|
34
|
+
# leave the profile it happened to start in, and a second workspace is
|
|
35
|
+
# unreachable from the surface most hosts ship. The route stays RBAC
|
|
36
|
+
# member-gated, so company-mode isolation is unaffected.
|
|
37
|
+
"switch_profile",
|
|
31
38
|
})
|
|
32
39
|
|
|
33
40
|
# Portable Brain evidence must reach the coding-host profile shipped by the
|
|
@@ -48,6 +55,11 @@ _PROFILE_CODE: frozenset[str] = _PROFILE_CORE | _PROFILE_BRAIN | frozenset({ #
|
|
|
48
55
|
# v3.8.0: bounded loops on the MCP surface. Coding agents (the /slm-loop
|
|
49
56
|
# command's audience) run gated, bounded loops and inspect the ledger.
|
|
50
57
|
"slm_loop_run", "slm_loop_history", "slm_loop_show",
|
|
58
|
+
# Retrieval ranks a memory partly on whether it has actually helped, and
|
|
59
|
+
# the only evidence of that comes from the assistant that used it. The
|
|
60
|
+
# plugin ships SLM_MCP_PROFILE=code, so without these two the ranker has
|
|
61
|
+
# no input at all for the audience it exists to serve.
|
|
62
|
+
"report_outcome", "report_feedback",
|
|
51
63
|
})
|
|
52
64
|
|
|
53
65
|
_PROFILE_FULL_MESH: frozenset[str] = frozenset({ # 8
|
|
@@ -74,7 +86,7 @@ _PROFILE_FULL: frozenset[str] = frozenset({
|
|
|
74
86
|
# prestage_context remains registered but deliberately raw-server-only.
|
|
75
87
|
}) | _PROFILE_FULL_MESH # 50
|
|
76
88
|
|
|
77
|
-
_PROFILE_POWER: frozenset[str] = _PROFILE_FULL | frozenset({ #
|
|
89
|
+
_PROFILE_POWER: frozenset[str] = _PROFILE_FULL | frozenset({ # 62
|
|
78
90
|
"get_version", "get_mode", "health", "consistency_check", "recall_trace",
|
|
79
91
|
"get_lifecycle_status", "set_retention_policy", "compact_memories",
|
|
80
92
|
"get_behavioral_patterns", "audit_trail", "quantize", "get_retention_stats",
|
|
@@ -282,7 +282,7 @@ register_optimize_tools(_target) # v3.6.11: Surface B Optimize tools (proxy-fre
|
|
|
282
282
|
from superlocalmemory.mcp.tools_loops import register_loop_tools
|
|
283
283
|
register_loop_tools(_target, get_engine) # v3.8.0: bounded-loop tools (CLI+command+MCP)
|
|
284
284
|
from superlocalmemory.mcp.tools_ops import register_ops_tools
|
|
285
|
-
register_ops_tools(_target, get_engine) #
|
|
285
|
+
register_ops_tools(_target, get_engine) # operational recovery & admin remediation
|
|
286
286
|
from superlocalmemory.mcp.tools_brain import register_brain_tools
|
|
287
287
|
register_brain_tools(_target, get_engine) # v4.0.2 portable Brain receipts
|
|
288
288
|
from superlocalmemory.mcp.tools_summaries import register_summary_tools
|