superlocalmemory 4.0.7 → 4.0.9
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 +219 -1
- package/README.md +6 -6
- 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/scripts/ensure-venv.sh +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 +3 -3
- package/plugin-src/rules/AGENTS.md +1 -1
- package/plugin-src/skills/slm-status/SKILL.md +2 -2
- package/pyproject.toml +1 -1
- package/scripts/postinstall.js +4 -0
- package/src/superlocalmemory/__init__.py +1 -1
- package/src/superlocalmemory/cli/_lazy_init.py +1 -1
- package/src/superlocalmemory/cli/commands.py +119 -9
- package/src/superlocalmemory/cli/db_migrate.py +0 -2
- package/src/superlocalmemory/cli/gdpr_io.py +1 -1
- package/src/superlocalmemory/cli/main.py +5 -5
- package/src/superlocalmemory/cli/service_installer.py +2 -1
- package/src/superlocalmemory/cli/setup_wizard.py +1 -1
- package/src/superlocalmemory/cli/summary_cmd.py +23 -3
- package/src/superlocalmemory/code_graph/bridge/maintenance.py +7 -1
- package/src/superlocalmemory/core/config.py +41 -7
- package/src/superlocalmemory/core/consolidation_engine.py +14 -15
- package/src/superlocalmemory/core/context_cache.py +0 -2
- package/src/superlocalmemory/core/engine.py +371 -63
- package/src/superlocalmemory/core/evidence_bundle.py +3 -1
- package/src/superlocalmemory/core/install_detector.py +131 -0
- package/src/superlocalmemory/core/progressive_abstraction.py +1 -1
- package/src/superlocalmemory/core/recall_worker.py +4 -0
- package/src/superlocalmemory/core/security_primitives.py +3 -6
- package/src/superlocalmemory/core/store_pipeline.py +94 -26
- package/src/superlocalmemory/core/topic_signature.py +0 -2
- package/src/superlocalmemory/core/transactions/concrete_owners.py +15 -8
- package/src/superlocalmemory/dynamics/eap_scheduler.py +17 -6
- package/src/superlocalmemory/encoding/graph_builder.py +2 -2
- package/src/superlocalmemory/encoding/scene_builder.py +8 -2
- package/src/superlocalmemory/evolution/skill_evolver.py +16 -1
- package/src/superlocalmemory/hooks/adapter_base.py +0 -2
- package/src/superlocalmemory/hooks/context_payload.py +0 -2
- package/src/superlocalmemory/hooks/hook_handlers.py +38 -11
- package/src/superlocalmemory/hooks/portable_kit.py +8 -8
- package/src/superlocalmemory/hooks/post_tool_async_hook.py +0 -2
- package/src/superlocalmemory/hooks/prewarm_auth.py +0 -2
- package/src/superlocalmemory/hooks/user_prompt_hook.py +0 -2
- package/src/superlocalmemory/infra/backup.py +44 -8
- package/src/superlocalmemory/integrations/bounded_loops_mcp.py +24 -7
- package/src/superlocalmemory/learning/arm_catalog.py +0 -2
- package/src/superlocalmemory/learning/bandit.py +0 -2
- package/src/superlocalmemory/learning/bandit_cache.py +0 -2
- package/src/superlocalmemory/learning/dedup_hnsw.py +11 -11
- package/src/superlocalmemory/learning/ensemble.py +0 -2
- package/src/superlocalmemory/learning/labeler.py +0 -2
- package/src/superlocalmemory/learning/legacy_migration.py +0 -2
- package/src/superlocalmemory/learning/model_cache.py +0 -2
- package/src/superlocalmemory/learning/pattern_miner.py +12 -7
- package/src/superlocalmemory/learning/ranker.py +0 -2
- package/src/superlocalmemory/learning/reward_archive.py +6 -1
- package/src/superlocalmemory/learning/reward_proxy.py +0 -2
- package/src/superlocalmemory/learning/signal_worker.py +0 -2
- package/src/superlocalmemory/math/fisher.py +1 -1
- package/src/superlocalmemory/math/hopfield.py +4 -1
- package/src/superlocalmemory/math/langevin.py +1 -1
- package/src/superlocalmemory/math/sheaf.py +7 -3
- package/src/superlocalmemory/mcp/cli_fallback.py +1 -1
- package/src/superlocalmemory/mcp/profiles.py +11 -4
- package/src/superlocalmemory/mcp/server.py +8 -1
- package/src/superlocalmemory/mcp/tools_active.py +56 -0
- package/src/superlocalmemory/mcp/tools_core.py +1 -1
- package/src/superlocalmemory/mcp/tools_summaries.py +147 -0
- package/src/superlocalmemory/optimize/cache/manager.py +2 -2
- package/src/superlocalmemory/optimize/compress/ccr.py +1 -1
- package/src/superlocalmemory/optimize/compress/router.py +1 -1
- package/src/superlocalmemory/optimize/proxy/_helpers.py +2 -2
- package/src/superlocalmemory/optimize/proxy/server.py +1 -1
- package/src/superlocalmemory/optimize/proxy/vertex_surface.py +2 -2
- package/src/superlocalmemory/optimize/storage/db.py +2 -2
- package/src/superlocalmemory/retrieval/agentic.py +1 -1
- package/src/superlocalmemory/retrieval/ann_index.py +9 -2
- package/src/superlocalmemory/retrieval/bm25_channel.py +2 -2
- package/src/superlocalmemory/retrieval/bridge_discovery.py +2 -2
- package/src/superlocalmemory/retrieval/engine.py +272 -43
- package/src/superlocalmemory/retrieval/entity_channel.py +1 -1
- package/src/superlocalmemory/retrieval/hopfield_channel.py +8 -2
- package/src/superlocalmemory/retrieval/profile_channel.py +1 -1
- package/src/superlocalmemory/retrieval/quantization_aware_search.py +1 -1
- package/src/superlocalmemory/retrieval/remote_reranker.py +2 -2
- package/src/superlocalmemory/retrieval/reranker.py +3 -3
- package/src/superlocalmemory/retrieval/semantic_channel.py +3 -3
- package/src/superlocalmemory/retrieval/spreading_activation.py +8 -8
- package/src/superlocalmemory/retrieval/strategy.py +94 -0
- package/src/superlocalmemory/retrieval/temporal_channel.py +167 -10
- package/src/superlocalmemory/retrieval/temporal_validity_filter.py +1 -1
- package/src/superlocalmemory/retrieval/vector_store.py +88 -10
- package/src/superlocalmemory/server/consolidation_runner.py +140 -0
- package/src/superlocalmemory/server/recall_serializer.py +44 -2
- package/src/superlocalmemory/server/routes/agents.py +52 -8
- package/src/superlocalmemory/server/routes/brain.py +110 -2
- package/src/superlocalmemory/server/routes/memories.py +153 -0
- package/src/superlocalmemory/server/routes/prewarm.py +4 -4
- package/src/superlocalmemory/server/routes/v3_api.py +24 -46
- package/src/superlocalmemory/server/unified_daemon.py +566 -7
- package/src/superlocalmemory/storage/_schema_version.py +46 -3
- package/src/superlocalmemory/storage/backup.py +531 -0
- package/src/superlocalmemory/storage/database.py +11 -4
- package/src/superlocalmemory/storage/embedding_codec.py +129 -0
- package/src/superlocalmemory/storage/embedding_migrator.py +5 -3
- package/src/superlocalmemory/storage/migration_runner.py +142 -2
- package/src/superlocalmemory/storage/migrations/__init__.py +1 -1
- package/src/superlocalmemory/storage/migrations.py +15 -1
- package/src/superlocalmemory/storage/models.py +7 -0
- package/src/superlocalmemory/storage/quantized_store.py +4 -2
- package/src/superlocalmemory/summaries/base.py +159 -0
- package/src/superlocalmemory/summaries/daily_reflection.py +55 -8
- package/src/superlocalmemory/summaries/project_work_log.py +23 -7
- package/src/superlocalmemory/summaries/session_summary.py +10 -6
- package/src/superlocalmemory/ui/css/legacy-dashboard.css +1 -1
- package/src/superlocalmemory/ui/css/neural-glass.css +1 -1
- package/src/superlocalmemory/ui/index.html +9 -3
- package/src/superlocalmemory/ui/js/core.js +1 -1
- package/src/superlocalmemory/ui/js/od-boundedloops.js +324 -0
- package/src/superlocalmemory/ui/js/od-brain.js +1 -1
- package/src/superlocalmemory/ui/js/od-memories.js +337 -12
- package/src/superlocalmemory/ui/js/od-mesh.js +97 -5
- package/src/superlocalmemory/ui/js/od-operations.js +1 -150
- package/src/superlocalmemory/ui/js/od-optimize.js +36 -9
- package/src/superlocalmemory/ui/js/od-shell.js +10 -0
|
@@ -96,6 +96,29 @@ async def observe_terminal_runs(
|
|
|
96
96
|
return observed
|
|
97
97
|
|
|
98
98
|
|
|
99
|
+
def _assert_trusted_executable(executable: Path) -> None:
|
|
100
|
+
"""Raise BridgeUnavailable if executable does not pass the bridge trust checks."""
|
|
101
|
+
try:
|
|
102
|
+
st = executable.stat()
|
|
103
|
+
except OSError as exc:
|
|
104
|
+
raise BridgeUnavailable("bounded-loops bridge path is unavailable") from exc
|
|
105
|
+
mode = st.st_mode
|
|
106
|
+
if not stat.S_ISREG(mode) or (
|
|
107
|
+
os.name != "nt" and mode & (stat.S_IWGRP | stat.S_IWOTH)
|
|
108
|
+
):
|
|
109
|
+
raise BridgeUnavailable("bounded-loops executable is not a trusted regular file")
|
|
110
|
+
# Windows trust-check limitation: only S_ISREG applies on Windows.
|
|
111
|
+
# The mode-bit check above (group/other writable) and the uid/owner check
|
|
112
|
+
# below are both skipped because:
|
|
113
|
+
# - Python's os.stat() on Windows returns emulated Unix-style mode bits
|
|
114
|
+
# that do not reflect real ACL permissions; the check would be meaningless.
|
|
115
|
+
# - os.geteuid() does not exist on Windows (AttributeError); st_uid is
|
|
116
|
+
# always 0 there, so the ownership check cannot identify non-root owners.
|
|
117
|
+
# Proper Windows ownership verification requires Win32 ACL APIs (advapi32),
|
|
118
|
+
# which would introduce a heavy optional dependency. Scoped for a future release.
|
|
119
|
+
if os.name != "nt" and st.st_uid not in {0, os.geteuid()}:
|
|
120
|
+
raise BridgeUnavailable("bounded-loops executable owner is not trusted")
|
|
121
|
+
|
|
99
122
|
async def observe_from_stdio(*, command: str, cwd: str, profile_id: str) -> list[dict[str, Any]]:
|
|
100
123
|
"""Run one bounded, explicit MCP 2 observation; never call from recall or remember."""
|
|
101
124
|
executable, workspace = Path(command), Path(cwd)
|
|
@@ -112,15 +135,9 @@ async def observe_from_stdio(*, command: str, cwd: str, profile_id: str) -> list
|
|
|
112
135
|
try:
|
|
113
136
|
executable = executable.resolve(strict=True)
|
|
114
137
|
workspace = workspace.resolve(strict=True)
|
|
115
|
-
mode = executable.stat().st_mode
|
|
116
138
|
except OSError as exc:
|
|
117
139
|
raise BridgeUnavailable("bounded-loops bridge path is unavailable") from exc
|
|
118
|
-
|
|
119
|
-
os.name != "nt" and mode & (stat.S_IWGRP | stat.S_IWOTH)
|
|
120
|
-
):
|
|
121
|
-
raise BridgeUnavailable("bounded-loops executable is not a trusted regular file")
|
|
122
|
-
if executable.stat().st_uid not in {0, os.geteuid()}:
|
|
123
|
-
raise BridgeUnavailable("bounded-loops executable owner is not trusted")
|
|
140
|
+
_assert_trusted_executable(executable)
|
|
124
141
|
|
|
125
142
|
from mcp import ClientSession, StdioServerParameters
|
|
126
143
|
from mcp.client.stdio import stdio_client
|
|
@@ -4,8 +4,6 @@
|
|
|
4
4
|
|
|
5
5
|
"""Static 40-arm catalog for the contextual Thompson bandit.
|
|
6
6
|
|
|
7
|
-
LLD reference: ``.backup/active-brain/lld/LLD-03-contextual-bandit-and-ensemble.md``
|
|
8
|
-
Section 5.1 — arm = (semantic, bm25, entity_graph, temporal,
|
|
9
7
|
cross_encoder_bias) weight bundle drawn from a 7-point canonical grid.
|
|
10
8
|
|
|
11
9
|
Pure-data module — zero imports from the rest of the codebase. Audit-friendly
|
|
@@ -4,8 +4,6 @@
|
|
|
4
4
|
|
|
5
5
|
"""Contextual Thompson-sampling bandit over discrete channel-weight arms.
|
|
6
6
|
|
|
7
|
-
LLD reference: ``.backup/active-brain/lld/LLD-03-contextual-bandit-and-ensemble.md``
|
|
8
|
-
Sections 3 (algorithm), 5.3 (file spec), 8 (hard rules).
|
|
9
7
|
|
|
10
8
|
Schema: ``bandit_arms`` + ``bandit_plays`` live in ``learning.db``, created by
|
|
11
9
|
LLD-07 M005. This module NEVER defines DDL — it only READs / WRITEs.
|
|
@@ -4,8 +4,6 @@
|
|
|
4
4
|
|
|
5
5
|
"""Per-(profile, stratum) posterior LRU cache for the contextual bandit.
|
|
6
6
|
|
|
7
|
-
LLD reference: ``.backup/active-brain/lld/LLD-03-contextual-bandit-and-ensemble.md``
|
|
8
|
-
Section 5.2.
|
|
9
7
|
|
|
10
8
|
Key design:
|
|
11
9
|
- Loader runs OUTSIDE the lock so DB reads never serialise across strata.
|
|
@@ -28,6 +28,7 @@ from pathlib import Path
|
|
|
28
28
|
from typing import Any, Iterable, Sequence
|
|
29
29
|
|
|
30
30
|
from superlocalmemory.core.ram_lock import ram_reservation
|
|
31
|
+
from superlocalmemory.storage.embedding_codec import decode_embedding
|
|
31
32
|
|
|
32
33
|
logger = logging.getLogger(__name__)
|
|
33
34
|
|
|
@@ -71,19 +72,18 @@ __all__ = (
|
|
|
71
72
|
)
|
|
72
73
|
|
|
73
74
|
|
|
74
|
-
def _parse_embedding(raw: str | None) -> list[float] | None:
|
|
75
|
+
def _parse_embedding(raw: bytes | str | None) -> list[float] | None:
|
|
76
|
+
"""Parse an embedding from either TEXT (JSON) or BLOB (binary float32).
|
|
77
|
+
|
|
78
|
+
Returns None only when the embedding is genuinely absent (None or empty).
|
|
79
|
+
Raises ValueError for malformed data so callers can distinguish absence
|
|
80
|
+
from corruption.
|
|
81
|
+
"""
|
|
75
82
|
if not raw:
|
|
76
83
|
return None
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
return None
|
|
81
|
-
if not isinstance(vec, list) or not vec:
|
|
82
|
-
return None
|
|
83
|
-
try:
|
|
84
|
-
return [float(x) for x in vec]
|
|
85
|
-
except (TypeError, ValueError):
|
|
86
|
-
return None
|
|
84
|
+
# decode_embedding raises ValueError for corrupt data; callers that want
|
|
85
|
+
# to skip a bad fact should catch ValueError explicitly, not silently.
|
|
86
|
+
return decode_embedding(raw)
|
|
87
87
|
|
|
88
88
|
|
|
89
89
|
# L-P-01: vectorise ``_cosine`` via NumPy when available. NumPy cold
|
|
@@ -4,8 +4,6 @@
|
|
|
4
4
|
|
|
5
5
|
"""Bandit / LightGBM ensemble blender.
|
|
6
6
|
|
|
7
|
-
LLD reference: ``.backup/active-brain/lld/LLD-03-contextual-bandit-and-ensemble.md``
|
|
8
|
-
Section 5.4.
|
|
9
7
|
|
|
10
8
|
D8 blend policy (``choose_ensemble``):
|
|
11
9
|
- 0..199 signals OR model is None → ``EnsembleWeights(1.0, 0.0)`` (bandit-only).
|
|
@@ -4,8 +4,6 @@
|
|
|
4
4
|
|
|
5
5
|
"""Integer-label mapping for LightGBM ``lambdarank`` training.
|
|
6
6
|
|
|
7
|
-
LLD reference: ``.backup/active-brain/lld/LLD-02-signal-pipeline-and-lightgbm.md``
|
|
8
|
-
Section 4.7 — single source of truth for outcome-reward / position → int label.
|
|
9
7
|
|
|
10
8
|
Rules:
|
|
11
9
|
- Labels are integers in ``[0, 4]`` (5 relevance tiers).
|
|
@@ -4,8 +4,6 @@
|
|
|
4
4
|
|
|
5
5
|
"""Legacy ``learning_feedback`` row migration — data movement only.
|
|
6
6
|
|
|
7
|
-
LLD reference: ``.backup/active-brain/lld/LLD-07-schema-migrations-and-security-primitives.md``
|
|
8
|
-
Section 5 (Legacy learning_feedback Migration).
|
|
9
7
|
|
|
10
8
|
Hard rule H15 (LLD-06 §10): this module MUST NOT contain any schema
|
|
11
9
|
DDL. All schema definitions live in
|
|
@@ -4,8 +4,6 @@
|
|
|
4
4
|
|
|
5
5
|
"""Active-model cache + integrity verification.
|
|
6
6
|
|
|
7
|
-
LLD reference: ``.backup/active-brain/lld/LLD-02-signal-pipeline-and-lightgbm.md``
|
|
8
|
-
Section 4.4 — every model load goes through here.
|
|
9
7
|
|
|
10
8
|
Hard rules enforced:
|
|
11
9
|
M1 — ``pickle.loads`` is FORBIDDEN on ``state_bytes``.
|
|
@@ -367,18 +367,23 @@ def _mine_channel_and_coretrieval(
|
|
|
367
367
|
gen += 1
|
|
368
368
|
|
|
369
369
|
try:
|
|
370
|
+
# Column names must match learning.db: the table is
|
|
371
|
+
# (fact_id_a, fact_id_b, co_count). This query asked for
|
|
372
|
+
# (fact_a, fact_b, co_access_count) and therefore raised
|
|
373
|
+
# sqlite3.OperationalError on every run since it was written —
|
|
374
|
+
# caught below, logged at WARNING, and otherwise invisible. Net
|
|
375
|
+
# effect: 912 co-retrieval edges on a real store never produced a
|
|
376
|
+
# single pattern, and the Brain pane had one fewer signal with no
|
|
377
|
+
# indication anything was missing.
|
|
370
378
|
coret_rows = learn_conn.execute(
|
|
371
|
-
"SELECT
|
|
379
|
+
"SELECT fact_id_a, fact_id_b, co_count "
|
|
372
380
|
"FROM co_retrieval_edges "
|
|
373
|
-
"WHERE profile_id = ? AND
|
|
374
|
-
"ORDER BY
|
|
381
|
+
"WHERE profile_id = ? AND co_count >= 3 "
|
|
382
|
+
"ORDER BY co_count DESC LIMIT 20",
|
|
375
383
|
(profile_id,),
|
|
376
384
|
).fetchall()
|
|
377
385
|
if coret_rows and not dry_run:
|
|
378
|
-
top_pair = (
|
|
379
|
-
dict(coret_rows[0]).get("co_access_count", 0)
|
|
380
|
-
if coret_rows else 0
|
|
381
|
-
)
|
|
386
|
+
top_pair = dict(coret_rows[0]).get("co_count", 0)
|
|
382
387
|
store.record_pattern(
|
|
383
388
|
profile_id=profile_id,
|
|
384
389
|
pattern_type="co_retrieval_clusters",
|
|
@@ -34,6 +34,7 @@ from pathlib import Path
|
|
|
34
34
|
from superlocalmemory.learning.fact_outcome_joins import (
|
|
35
35
|
has_recent_positive_reward,
|
|
36
36
|
)
|
|
37
|
+
from superlocalmemory.storage.embedding_codec import decode_embedding
|
|
37
38
|
from superlocalmemory.storage.write_lock import get_write_lock
|
|
38
39
|
|
|
39
40
|
logger = logging.getLogger(__name__)
|
|
@@ -113,13 +114,17 @@ def run_reward_gated_archive(
|
|
|
113
114
|
window_days=REWARD_WINDOW_DAYS,
|
|
114
115
|
):
|
|
115
116
|
continue
|
|
117
|
+
# Decode the embedding to list[float] so json.dumps(payload)
|
|
118
|
+
# succeeds regardless of whether the row is TEXT or BLOB.
|
|
119
|
+
# ValueError (corrupt buffer) propagates — a silent None here
|
|
120
|
+
# would archive a fact with a missing embedding, which loses data.
|
|
116
121
|
to_archive.append({
|
|
117
122
|
"fid": fid,
|
|
118
123
|
"content": row["content"],
|
|
119
124
|
"canonical_entities_json": row["canonical_entities_json"],
|
|
120
125
|
"importance": row["importance"],
|
|
121
126
|
"confidence": row["confidence"],
|
|
122
|
-
"embedding": row["embedding"],
|
|
127
|
+
"embedding": decode_embedding(row["embedding"], fact_id=fid),
|
|
123
128
|
"created_at": row["created_at"],
|
|
124
129
|
})
|
|
125
130
|
finally:
|
|
@@ -4,8 +4,6 @@
|
|
|
4
4
|
|
|
5
5
|
"""Proxy settlement for bandit plays (v3.4.22 only).
|
|
6
6
|
|
|
7
|
-
LLD reference: ``.backup/active-brain/lld/LLD-03-contextual-bandit-and-ensemble.md``
|
|
8
|
-
Section 3.5 and 5.6.
|
|
9
7
|
|
|
10
8
|
Replaced in v3.4.22 by ``reward_from_outcomes.py`` — DO NOT extend this
|
|
11
9
|
module beyond the proxy window contract.
|
|
@@ -4,8 +4,6 @@
|
|
|
4
4
|
|
|
5
5
|
"""Background signal drain worker.
|
|
6
6
|
|
|
7
|
-
LLD reference: ``.backup/active-brain/lld/LLD-02-signal-pipeline-and-lightgbm.md``
|
|
8
|
-
Section 4.2 — moves signal writes off the recall hot path.
|
|
9
7
|
|
|
10
8
|
Contract (hard rules, enforced by tests):
|
|
11
9
|
SW1 — Hot path never waits for disk.
|
|
@@ -19,7 +19,7 @@ Geodesic distance (Atkinson & Mitchell 1981, Pinele et al. 2020):
|
|
|
19
19
|
Diagonal multivariate (product-manifold decomposition):
|
|
20
20
|
d_FR(p, q) = sqrt( sum_i d_i^2 )
|
|
21
21
|
|
|
22
|
-
Bayesian variance update (
|
|
22
|
+
Bayesian variance update (added in v3.5):
|
|
23
23
|
|
|
24
24
|
V1 bug: query always received UNIFORM variance, so Fisher degenerated
|
|
25
25
|
to a monotonic transform of cosine. FIX: every fact maintains its own
|
|
@@ -56,7 +56,10 @@ class HopfieldConfig:
|
|
|
56
56
|
max_iterations: int = 1
|
|
57
57
|
convergence_epsilon: float = 1e-6
|
|
58
58
|
prefilter_threshold: int = 10_000
|
|
59
|
-
prefilter_candidates
|
|
59
|
+
# MUST match HopfieldConfig.prefilter_candidates in core/config.py, where the
|
|
60
|
+
# reasoning for the value is written. This stage decides final membership, so
|
|
61
|
+
# this number is a hard limit on which memories can be returned at all.
|
|
62
|
+
prefilter_candidates: int = 500
|
|
60
63
|
skip_threshold: int = 100_000
|
|
61
64
|
cache_ttl_seconds: float = 60.0
|
|
62
65
|
|
|
@@ -26,7 +26,7 @@ Potential function:
|
|
|
26
26
|
- Old / unused: gamma term raises potential -> drift to boundary
|
|
27
27
|
- Important memories: delta term lowers potential -> retain near origin
|
|
28
28
|
|
|
29
|
-
V1 bugs fixed in
|
|
29
|
+
V1 bugs fixed in v3.5:
|
|
30
30
|
1. Positions were computed per-recall then DISCARDED. Now ``step()``
|
|
31
31
|
and ``batch_step()`` return new positions for the caller to persist.
|
|
32
32
|
2. Weight range was [0.7, 1.0] --- too narrow to change rankings.
|
|
@@ -24,6 +24,8 @@ from typing import TYPE_CHECKING
|
|
|
24
24
|
|
|
25
25
|
import numpy as np
|
|
26
26
|
|
|
27
|
+
from superlocalmemory.storage.embedding_codec import decode_embedding
|
|
28
|
+
|
|
27
29
|
if TYPE_CHECKING:
|
|
28
30
|
from superlocalmemory.storage.database import DatabaseManager
|
|
29
31
|
from superlocalmemory.storage.models import AtomicFact
|
|
@@ -251,7 +253,9 @@ class SheafConsistencyChecker:
|
|
|
251
253
|
if raw is None or raw == "":
|
|
252
254
|
return None
|
|
253
255
|
try:
|
|
254
|
-
data =
|
|
255
|
-
|
|
256
|
-
|
|
256
|
+
data = decode_embedding(raw, fact_id=str(fact_id))
|
|
257
|
+
except ValueError:
|
|
258
|
+
return None
|
|
259
|
+
if data is None:
|
|
257
260
|
return None
|
|
261
|
+
return np.asarray(data, dtype=np.float64)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
|
|
2
2
|
# Licensed under AGPL-3.0-or-later - see LICENSE file
|
|
3
|
-
#
|
|
3
|
+
# MCP→CLI fallback adapter (ships inert — disabled by default, no active use yet)
|
|
4
4
|
|
|
5
5
|
"""MCP→CLI fallback adapter for SuperLocalMemory.
|
|
6
6
|
|
|
@@ -14,16 +14,20 @@ Do NOT import FastMCP, MemoryEngine, or any heavy dependency here.
|
|
|
14
14
|
from __future__ import annotations
|
|
15
15
|
|
|
16
16
|
# ---------------------------------------------------------------------------
|
|
17
|
-
# v3.6.14
|
|
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({ # 17
|
|
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",
|
|
24
24
|
# A client that can propose a correction must be able to inspect and
|
|
25
25
|
# authenticate its review; otherwise the core lifecycle is incomplete.
|
|
26
26
|
"review_correction", "list_corrections",
|
|
27
|
+
# v4.0.8: the readable summary layer (issue #113). In CORE because the
|
|
28
|
+
# natural caller is the agent holding the conversation — an assistant
|
|
29
|
+
# asked "what did I work on yesterday" should not need a power profile.
|
|
30
|
+
"get_memory_summary",
|
|
27
31
|
})
|
|
28
32
|
|
|
29
33
|
# Portable Brain evidence must reach the coding-host profile shipped by the
|
|
@@ -34,7 +38,7 @@ _PROFILE_BRAIN: frozenset[str] = frozenset({
|
|
|
34
38
|
"observe_bounded_loop_evidence",
|
|
35
39
|
})
|
|
36
40
|
|
|
37
|
-
_PROFILE_CODE: frozenset[str] = _PROFILE_CORE | _PROFILE_BRAIN | frozenset({ #
|
|
41
|
+
_PROFILE_CODE: frozenset[str] = _PROFILE_CORE | _PROFILE_BRAIN | frozenset({ # 32
|
|
38
42
|
"build_code_graph", "get_blast_radius", "query_graph",
|
|
39
43
|
"semantic_search_code", "get_review_context", "detect_changes",
|
|
40
44
|
# switch_profile lets a plugin/IDE session change the active workspace over
|
|
@@ -64,8 +68,11 @@ _PROFILE_FULL: frozenset[str] = frozenset({
|
|
|
64
68
|
"slm_compress", "slm_retrieve", "slm_cache_set", "slm_cache_get", "slm_optimize_stats",
|
|
65
69
|
# v3.8.0: bounded-loop tools (CLI + /slm-loop command + MCP).
|
|
66
70
|
"slm_loop_run", "slm_loop_history", "slm_loop_show",
|
|
71
|
+
# v4.0.8: readable summaries (#113). In core, so it must be in full too —
|
|
72
|
+
# full is asserted to be a superset of core.
|
|
73
|
+
"get_memory_summary",
|
|
67
74
|
# prestage_context remains registered but deliberately raw-server-only.
|
|
68
|
-
}) | _PROFILE_FULL_MESH #
|
|
75
|
+
}) | _PROFILE_FULL_MESH # 50
|
|
69
76
|
|
|
70
77
|
_PROFILE_POWER: frozenset[str] = _PROFILE_FULL | frozenset({ # 61
|
|
71
78
|
"get_version", "get_mode", "health", "consistency_check", "recall_trace",
|
|
@@ -105,6 +105,11 @@ _ESSENTIAL_TOOLS: set[str] = {
|
|
|
105
105
|
"observe_bounded_loop_evidence",
|
|
106
106
|
# Update, review, and list form one core correction lifecycle.
|
|
107
107
|
"review_correction", "list_corrections",
|
|
108
|
+
# v4.0.8 (#113): readable summaries. Present here as well as in the named
|
|
109
|
+
# profiles because this set is the FALLBACK surface — it must mirror
|
|
110
|
+
# ``full``, and a tool that ships in the smallest profile ("core") cannot be
|
|
111
|
+
# missing from the fallback without a client silently losing it.
|
|
112
|
+
"get_memory_summary",
|
|
108
113
|
# Memory management (2)
|
|
109
114
|
"forget", "run_maintenance",
|
|
110
115
|
# NOTE: prestage_context IS registered (see register_prestage_tool below)
|
|
@@ -154,7 +159,7 @@ _all_tools = _os_reg.environ.get("SLM_MCP_ALL_TOOLS") == "1"
|
|
|
154
159
|
_user_allowlist_str = _os_reg.environ.get("SLM_MCP_TOOLS", "").strip()
|
|
155
160
|
|
|
156
161
|
# ---------------------------------------------------------------------------
|
|
157
|
-
# v3.6.14
|
|
162
|
+
# Named profile definitions (introduced in v3.6.14)
|
|
158
163
|
# Extracted to mcp/profiles.py (v3.8.0) — pure data, no side effects.
|
|
159
164
|
# All names re-exported here for backward compatibility with existing tests
|
|
160
165
|
# and any code that imports them from this module.
|
|
@@ -280,6 +285,8 @@ from superlocalmemory.mcp.tools_ops import register_ops_tools
|
|
|
280
285
|
register_ops_tools(_target, get_engine) # Wave-3: operational recovery & admin remediation
|
|
281
286
|
from superlocalmemory.mcp.tools_brain import register_brain_tools
|
|
282
287
|
register_brain_tools(_target, get_engine) # v4.0.2 portable Brain receipts
|
|
288
|
+
from superlocalmemory.mcp.tools_summaries import register_summary_tools
|
|
289
|
+
register_summary_tools(_target, get_engine) # v4.0.8 issue #113 summary reads
|
|
283
290
|
from superlocalmemory.mcp.tools_context import register_prestage_tool
|
|
284
291
|
|
|
285
292
|
|
|
@@ -255,6 +255,56 @@ def _canonical_feedback_count(profile_id: str) -> int | None:
|
|
|
255
255
|
return None
|
|
256
256
|
|
|
257
257
|
|
|
258
|
+
# How far ahead a session looks for scheduled facts, and how many it shows.
|
|
259
|
+
# A session preamble is not a calendar: a long horizon or a large cap turns a
|
|
260
|
+
# useful heads-up into a wall of text nobody reads.
|
|
261
|
+
_SCHEDULED_HORIZON_DAYS = 14
|
|
262
|
+
_SCHEDULED_LIMIT = 5
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def _upcoming_scheduled_facts(engine, now: datetime.datetime) -> list[dict]:
|
|
266
|
+
"""Facts scheduled from today through the horizon, soonest first.
|
|
267
|
+
|
|
268
|
+
Bounded and index-backed, because this runs on every session start. Returns
|
|
269
|
+
an empty list on any failure: a session must still open when this query
|
|
270
|
+
cannot answer, so the caller omits the surface entirely rather than showing
|
|
271
|
+
an empty section that reads like a defect.
|
|
272
|
+
|
|
273
|
+
Selects on the stored type of the fact, which is a classification recorded at
|
|
274
|
+
write time. It is unrelated to the similarly named retrieval channel despite
|
|
275
|
+
sharing the word.
|
|
276
|
+
"""
|
|
277
|
+
try:
|
|
278
|
+
db = getattr(engine, "_db", None) or getattr(engine, "db", None)
|
|
279
|
+
if db is None or not hasattr(db, "execute"):
|
|
280
|
+
return []
|
|
281
|
+
# Both 'YYYY-MM-DD' and full timestamps compare correctly as text,
|
|
282
|
+
# because ISO-8601 orders lexicographically. The upper bound is
|
|
283
|
+
# exclusive, so the horizon day itself is included.
|
|
284
|
+
start = now.date().isoformat()
|
|
285
|
+
end = (now + datetime.timedelta(days=_SCHEDULED_HORIZON_DAYS + 1)).date().isoformat()
|
|
286
|
+
rows = db.execute(
|
|
287
|
+
"SELECT fact_id, content, referenced_date"
|
|
288
|
+
" FROM atomic_facts"
|
|
289
|
+
" WHERE profile_id = ?"
|
|
290
|
+
" AND fact_type = 'temporal'"
|
|
291
|
+
" AND referenced_date IS NOT NULL"
|
|
292
|
+
" AND referenced_date >= ?"
|
|
293
|
+
" AND referenced_date < ?"
|
|
294
|
+
" ORDER BY referenced_date ASC"
|
|
295
|
+
f" LIMIT {_SCHEDULED_LIMIT}",
|
|
296
|
+
(engine.profile_id, start, end),
|
|
297
|
+
)
|
|
298
|
+
return [
|
|
299
|
+
{"fact_id": r["fact_id"], "content": r["content"],
|
|
300
|
+
"scheduled_at": r["referenced_date"]}
|
|
301
|
+
for r in rows
|
|
302
|
+
]
|
|
303
|
+
except Exception as exc:
|
|
304
|
+
logger.warning("scheduled-fact surface failed: %s", exc)
|
|
305
|
+
return []
|
|
306
|
+
|
|
307
|
+
|
|
258
308
|
def register_active_tools(server, get_engine: Callable) -> None:
|
|
259
309
|
"""Register 3 active memory tools on *server*."""
|
|
260
310
|
|
|
@@ -519,6 +569,8 @@ def register_active_tools(server, get_engine: Callable) -> None:
|
|
|
519
569
|
f"-{uuid.uuid4().hex[:8]}"
|
|
520
570
|
)
|
|
521
571
|
|
|
572
|
+
_upcoming_events = _upcoming_scheduled_facts(engine, _now)
|
|
573
|
+
|
|
522
574
|
return {
|
|
523
575
|
"success": True,
|
|
524
576
|
"session_id": session_id,
|
|
@@ -553,6 +605,10 @@ def register_active_tools(server, get_engine: Callable) -> None:
|
|
|
553
605
|
else "trained"
|
|
554
606
|
),
|
|
555
607
|
},
|
|
608
|
+
# Scheduled-event surface: present only when facts exist in the
|
|
609
|
+
# 14-day window. Absent means the window is empty, not an error.
|
|
610
|
+
**( {"upcoming_events": _upcoming_events}
|
|
611
|
+
if _upcoming_events else {} ),
|
|
556
612
|
}
|
|
557
613
|
except Exception as exc:
|
|
558
614
|
logger.exception("session_init failed")
|
|
@@ -560,7 +560,7 @@ def register_core_tools(server, get_engine: Callable) -> None:
|
|
|
560
560
|
if db_path.exists():
|
|
561
561
|
db_size_mb = round(os.path.getsize(db_path) / (1024 * 1024), 2)
|
|
562
562
|
|
|
563
|
-
#
|
|
563
|
+
# additive canonical key set — provider/base_dir/db_path added.
|
|
564
564
|
# All pre-existing keys are preserved (zero removals).
|
|
565
565
|
cfg = engine._config
|
|
566
566
|
return {
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
|
|
2
|
+
# Licensed under AGPL-3.0-or-later - see LICENSE file
|
|
3
|
+
# Part of SuperLocalMemory | https://qualixar.com
|
|
4
|
+
|
|
5
|
+
"""MCP surface for the readable summary layer (issue #113).
|
|
6
|
+
|
|
7
|
+
WHY THIS EXISTS
|
|
8
|
+
---------------
|
|
9
|
+
4.0.6 shipped the summary generators with no caller at all. 4.0.7 added
|
|
10
|
+
``slm summary``, which fixed it for a person at a terminal and left agents with
|
|
11
|
+
nothing — the changelog said the defect was "no command, tool or endpoint" and
|
|
12
|
+
only the command was built. This is the tool half.
|
|
13
|
+
|
|
14
|
+
It matters more than the CLI: the natural consumer of "what did I work on
|
|
15
|
+
yesterday" is the agent holding the conversation, not a human running a command.
|
|
16
|
+
|
|
17
|
+
CONTRACT
|
|
18
|
+
--------
|
|
19
|
+
Read-only, profile-scoped, and honest about coverage. Every response carries
|
|
20
|
+
``coverage`` and ``source_fact_ids``, so a caller can tell a summary of 4% of a
|
|
21
|
+
session from a summary of all of it, and can drill back to the memories it came
|
|
22
|
+
from. Callers must not present a partial summary as complete; the field exists
|
|
23
|
+
precisely so they do not have to guess.
|
|
24
|
+
|
|
25
|
+
NOT ON THE HOT PATH. Summaries read memory.db directly and are invoked on
|
|
26
|
+
demand; nothing here runs during remember or recall.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
import logging
|
|
32
|
+
from datetime import date, timedelta
|
|
33
|
+
from typing import Any, Callable
|
|
34
|
+
|
|
35
|
+
from mcp.types import ToolAnnotations
|
|
36
|
+
|
|
37
|
+
from superlocalmemory.core.admission import admits
|
|
38
|
+
from superlocalmemory.core.operation_request import OperationKind
|
|
39
|
+
from superlocalmemory.infra.data_root import state_path
|
|
40
|
+
|
|
41
|
+
logger = logging.getLogger("superlocalmemory.mcp.summaries")
|
|
42
|
+
|
|
43
|
+
#: Accepted values for the ``kind`` argument.
|
|
44
|
+
_KINDS = ("day", "project", "session")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _result_payload(result: Any) -> dict[str, Any]:
|
|
48
|
+
"""Shape a SummaryResult for the wire.
|
|
49
|
+
|
|
50
|
+
``coverage`` and ``source_fact_ids`` are non-negotiable parts of the
|
|
51
|
+
response: a summary that cannot be traced back, or that hides how much it
|
|
52
|
+
covered, is the opaque generic summary issue #113 asked us not to build.
|
|
53
|
+
"""
|
|
54
|
+
return {
|
|
55
|
+
"success": True,
|
|
56
|
+
"kind": result.kind,
|
|
57
|
+
"profile_id": result.profile_id,
|
|
58
|
+
"summary": result.content,
|
|
59
|
+
"coverage": result.coverage,
|
|
60
|
+
"generated_by": result.generated_by,
|
|
61
|
+
"source_fact_ids": result.source_fact_ids,
|
|
62
|
+
"source_count": len(result.source_fact_ids),
|
|
63
|
+
"metadata": result.metadata,
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _error(message: str, **extra: Any) -> dict[str, Any]:
|
|
68
|
+
out = {"success": False, "error": message}
|
|
69
|
+
out.update(extra)
|
|
70
|
+
return out
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def register_summary_tools(server: Any, get_engine: Callable[[], Any]) -> None:
|
|
74
|
+
"""Register the read-only summary tool."""
|
|
75
|
+
|
|
76
|
+
@server.tool(annotations=ToolAnnotations(readOnlyHint=True))
|
|
77
|
+
@admits(OperationKind.RECALL)
|
|
78
|
+
async def get_memory_summary(
|
|
79
|
+
kind: str = "day",
|
|
80
|
+
target: str = "",
|
|
81
|
+
) -> dict[str, Any]:
|
|
82
|
+
"""Summarise your memories: a day, a project, or one session.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
kind: "day", "project", or "session".
|
|
86
|
+
target: For "day", an ISO date, "today" or "yesterday" (default
|
|
87
|
+
today). For "project", a directory path (default: none — supply
|
|
88
|
+
one). For "session", the session id.
|
|
89
|
+
|
|
90
|
+
Returns a summary plus ``coverage`` and ``source_fact_ids``. Coverage is
|
|
91
|
+
not decoration: session data is sparse — roughly 4% of facts carry a
|
|
92
|
+
session id — so a session summary is usually partial. Do not present a
|
|
93
|
+
partial summary as a complete record of what happened.
|
|
94
|
+
|
|
95
|
+
No language model is required; summaries are extractive unless the
|
|
96
|
+
profile runs a local or cloud model, in which case that writes them.
|
|
97
|
+
"""
|
|
98
|
+
kind = (kind or "day").strip().lower()
|
|
99
|
+
if kind not in _KINDS:
|
|
100
|
+
return _error(
|
|
101
|
+
f"unknown summary kind {kind!r}; expected one of {', '.join(_KINDS)}"
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
engine = get_engine()
|
|
105
|
+
profile_id = getattr(engine, "profile_id", "default")
|
|
106
|
+
db_path = state_path("memory.db")
|
|
107
|
+
if not db_path.exists():
|
|
108
|
+
return _error("no memory database found", db_path=str(db_path))
|
|
109
|
+
|
|
110
|
+
# The engine's config drives Mode B/C enrichment. Passing None would
|
|
111
|
+
# silently force the extractive path for every caller regardless of
|
|
112
|
+
# mode — the exact bug the CLI shipped with in 4.0.7.
|
|
113
|
+
config = getattr(engine, "config", None)
|
|
114
|
+
|
|
115
|
+
try:
|
|
116
|
+
if kind == "day":
|
|
117
|
+
from superlocalmemory.summaries import generate_daily_reflection
|
|
118
|
+
|
|
119
|
+
day = (target or "").strip() or date.today().isoformat()
|
|
120
|
+
if day == "today":
|
|
121
|
+
day = date.today().isoformat()
|
|
122
|
+
elif day == "yesterday":
|
|
123
|
+
day = (date.today() - timedelta(days=1)).isoformat()
|
|
124
|
+
result = generate_daily_reflection(db_path, day, profile_id, config)
|
|
125
|
+
|
|
126
|
+
elif kind == "project":
|
|
127
|
+
from superlocalmemory.summaries import generate_project_work_log
|
|
128
|
+
|
|
129
|
+
if not (target or "").strip():
|
|
130
|
+
return _error("kind='project' requires target=<project path>")
|
|
131
|
+
result = generate_project_work_log(
|
|
132
|
+
db_path, target.strip(), profile_id, config,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
else: # session
|
|
136
|
+
from superlocalmemory.summaries import generate_session_summary
|
|
137
|
+
|
|
138
|
+
if not (target or "").strip():
|
|
139
|
+
return _error("kind='session' requires target=<session id>")
|
|
140
|
+
result = generate_session_summary(
|
|
141
|
+
db_path, target.strip(), profile_id, config,
|
|
142
|
+
)
|
|
143
|
+
except Exception as exc:
|
|
144
|
+
logger.warning("summary generation failed (%s/%s): %s", kind, target, exc)
|
|
145
|
+
return _error(f"summary generation failed: {exc}", kind=kind)
|
|
146
|
+
|
|
147
|
+
return _result_payload(result)
|
|
@@ -223,7 +223,7 @@ class CacheManager:
|
|
|
223
223
|
tenant_id = _hashlib.sha256(tenant_id.encode()).hexdigest()
|
|
224
224
|
|
|
225
225
|
if isinstance(req, ProxyRequest) and req.provider == "vertex":
|
|
226
|
-
#
|
|
226
|
+
# LOCKED: Vertex bodies have NO model/messages/system.
|
|
227
227
|
# Model is in the PATH; prompts are under 'contents'; system under
|
|
228
228
|
# 'systemInstruction'. Without this branch ALL Vertex requests hash to
|
|
229
229
|
# ONE key → first response poisons every subsequent prompt.
|
|
@@ -701,7 +701,7 @@ def json_dumps_bytes(d: dict) -> bytes:
|
|
|
701
701
|
|
|
702
702
|
|
|
703
703
|
# ---------------------------------------------------------------------------
|
|
704
|
-
# Vertex helpers
|
|
704
|
+
# Vertex helpers
|
|
705
705
|
# ---------------------------------------------------------------------------
|
|
706
706
|
|
|
707
707
|
def _vertex_project_location_from_path(path: str) -> tuple[str, str]:
|
|
@@ -99,7 +99,7 @@ class CCRStore:
|
|
|
99
99
|
def delete(self, ccr_id: str, *, tenant_id: str = "default") -> None:
|
|
100
100
|
"""Delete a CCR row by ccr_id scoped to tenant. Idempotent — never raises.
|
|
101
101
|
|
|
102
|
-
|
|
102
|
+
Defensive delete — idempotent: deleting a non-existent ccr_id is a no-op.
|
|
103
103
|
H-02: tenant_id guard prevents cross-tenant deletion.
|
|
104
104
|
"""
|
|
105
105
|
try:
|