superlocalmemory 3.8.13 → 4.0.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/ATTRIBUTION.md +4 -4
- package/CHANGELOG.md +113 -121
- package/README.md +65 -63
- package/docs/pi-dev-integration.md +1 -1
- package/package.json +6 -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-governance/SKILL.md +248 -0
- package/plugin-src/skills/slm-graph/SKILL.md +1 -1
- package/plugin-src/skills/slm-loop/SKILL.md +99 -0
- package/plugin-src/skills/slm-mesh/SKILL.md +282 -0
- package/plugin-src/skills/slm-profile/SKILL.md +148 -0
- package/plugin-src/skills/slm-recall/SKILL.md +1 -1
- package/plugin-src/skills/slm-remember/SKILL.md +1 -1
- package/plugin-src/skills/slm-scope/SKILL.md +176 -0
- package/plugin-src/skills/slm-session/SKILL.md +1 -1
- package/plugin-src/skills/slm-status/SKILL.md +1 -1
- package/pyproject.toml +11 -4
- package/src/superlocalmemory/__init__.py +1 -1
- package/src/superlocalmemory/cli/commands.py +125 -11
- package/src/superlocalmemory/cli/daemon.py +5 -1
- package/src/superlocalmemory/cli/main.py +35 -2
- package/src/superlocalmemory/cli/ops_cmd.py +281 -0
- package/src/superlocalmemory/cli/setup_wizard.py +1 -1
- package/src/superlocalmemory/compliance/audit.py +65 -0
- package/src/superlocalmemory/compliance/eu_ai_act.py +27 -57
- package/src/superlocalmemory/compliance/gdpr.py +416 -20
- package/src/superlocalmemory/compliance/retention.py +74 -22
- package/src/superlocalmemory/compliance/scheduler.py +78 -9
- package/src/superlocalmemory/core/actor_context.py +166 -0
- package/src/superlocalmemory/core/admission.py +549 -0
- package/src/superlocalmemory/core/backend_orchestrator.py +23 -10
- package/src/superlocalmemory/core/config.py +202 -24
- package/src/superlocalmemory/core/consolidation_engine.py +13 -13
- package/src/superlocalmemory/core/context_cache.py +28 -0
- package/src/superlocalmemory/core/embeddings.py +64 -2
- package/src/superlocalmemory/core/engine.py +7 -2
- package/src/superlocalmemory/core/engine_ingestion.py +65 -3
- package/src/superlocalmemory/core/engine_wiring.py +36 -9
- package/src/superlocalmemory/core/ingest_policy.py +38 -0
- package/src/superlocalmemory/core/maintenance.py +255 -0
- package/src/superlocalmemory/core/modes.py +40 -13
- package/src/superlocalmemory/core/mutations.py +437 -44
- package/src/superlocalmemory/core/operation_policy.py +92 -0
- package/src/superlocalmemory/core/operation_policy_registry.py +542 -0
- package/src/superlocalmemory/core/operation_request.py +127 -0
- package/src/superlocalmemory/core/ops_remediation.py +542 -0
- package/src/superlocalmemory/core/recall_pipeline.py +7 -0
- package/src/superlocalmemory/core/remember_runtime.py +202 -4
- package/src/superlocalmemory/core/remote_mode.py +20 -5
- package/src/superlocalmemory/core/store_pipeline.py +150 -0
- package/src/superlocalmemory/core/topic_signature.py +19 -4
- package/src/superlocalmemory/core/transactions/__init__.py +78 -0
- package/src/superlocalmemory/core/transactions/concrete_owners.py +597 -0
- package/src/superlocalmemory/core/transactions/erasure.py +825 -0
- package/src/superlocalmemory/core/transactions/manifest.py +255 -0
- package/src/superlocalmemory/core/transactions/manifest_key.py +155 -0
- package/src/superlocalmemory/core/transactions/obligations.py +272 -0
- package/src/superlocalmemory/core/transactions/owners.py +114 -0
- package/src/superlocalmemory/core/transactions/reconciler.py +285 -0
- package/src/superlocalmemory/core/transactions/service.py +330 -0
- package/src/superlocalmemory/core/worker_pool.py +33 -5
- package/src/superlocalmemory/encoding/cognitive_consolidator.py +70 -28
- package/src/superlocalmemory/encoding/emotional.py +75 -14
- package/src/superlocalmemory/encoding/scene_builder.py +115 -13
- package/src/superlocalmemory/encoding/temporal_parser.py +4 -0
- package/src/superlocalmemory/evolution/blind_verifier.py +11 -4
- package/src/superlocalmemory/evolution/evolution_store.py +244 -4
- package/src/superlocalmemory/evolution/llm_dispatch.py +40 -0
- package/src/superlocalmemory/evolution/model_selection.py +18 -3
- package/src/superlocalmemory/evolution/mutation_generator.py +3 -0
- package/src/superlocalmemory/evolution/skill_activator.py +270 -0
- package/src/superlocalmemory/evolution/skill_evolver.py +281 -59
- package/src/superlocalmemory/evolution/types.py +30 -8
- package/src/superlocalmemory/graph/cozo_backend.py +17 -9
- package/src/superlocalmemory/hooks/auto_invoker.py +2 -1
- package/src/superlocalmemory/hooks/auto_recall.py +64 -30
- package/src/superlocalmemory/hooks/codex_assets.py +14 -1
- package/src/superlocalmemory/infra/backup.py +434 -7
- package/src/superlocalmemory/infra/process_reaper.py +18 -0
- package/src/superlocalmemory/infra/self_heal.py +401 -0
- package/src/superlocalmemory/learning/feedback.py +52 -9
- package/src/superlocalmemory/loops/engine.py +10 -0
- package/src/superlocalmemory/mcp/_daemon_proxy.py +3 -0
- package/src/superlocalmemory/mcp/http_transport.py +30 -331
- package/src/superlocalmemory/mcp/profiles.py +5 -0
- package/src/superlocalmemory/mcp/resources.py +8 -0
- package/src/superlocalmemory/mcp/server.py +51 -4
- package/src/superlocalmemory/mcp/shared.py +19 -0
- package/src/superlocalmemory/mcp/tools_active.py +25 -4
- package/src/superlocalmemory/mcp/tools_code_graph.py +26 -18
- package/src/superlocalmemory/mcp/tools_context.py +50 -8
- package/src/superlocalmemory/mcp/tools_core.py +69 -21
- package/src/superlocalmemory/mcp/tools_evolution.py +9 -2
- package/src/superlocalmemory/mcp/tools_learning.py +21 -10
- package/src/superlocalmemory/mcp/tools_loops.py +29 -18
- package/src/superlocalmemory/mcp/tools_mesh.py +8 -0
- package/src/superlocalmemory/mcp/tools_ops.py +115 -0
- package/src/superlocalmemory/mcp/tools_optimize.py +4 -0
- package/src/superlocalmemory/mcp/tools_v28.py +10 -3
- package/src/superlocalmemory/mcp/tools_v3.py +34 -14
- package/src/superlocalmemory/mcp/tools_v33.py +18 -33
- package/src/superlocalmemory/mesh/broker.py +124 -46
- package/src/superlocalmemory/mesh/broker_security.py +470 -0
- package/src/superlocalmemory/mesh/discovery.py +365 -0
- package/src/superlocalmemory/mesh/lock_protocol.py +313 -0
- package/src/superlocalmemory/mesh/node_identity.py +97 -0
- package/src/superlocalmemory/mesh/outbox_remote.py +429 -0
- package/src/superlocalmemory/mesh/remote_sync.py +511 -28
- package/src/superlocalmemory/mesh/state_sync.py +286 -0
- package/src/superlocalmemory/optimize/config/store.py +45 -0
- package/src/superlocalmemory/parameterization/cross_project.py +12 -0
- package/src/superlocalmemory/parameterization/prompt_injector.py +13 -11
- package/src/superlocalmemory/parameterization/prompt_lifecycle.py +8 -2
- package/src/superlocalmemory/parameterization/workflow_miner.py +17 -0
- package/src/superlocalmemory/retrieval/ann_index.py +5 -0
- package/src/superlocalmemory/retrieval/bm25_channel.py +49 -2
- package/src/superlocalmemory/retrieval/engine.py +19 -4
- package/src/superlocalmemory/retrieval/fusion.py +4 -1
- package/src/superlocalmemory/retrieval/hopfield_channel.py +9 -3
- package/src/superlocalmemory/retrieval/remote_reranker.py +47 -22
- package/src/superlocalmemory/retrieval/reranker.py +32 -1
- package/src/superlocalmemory/retrieval/temporal_channel.py +16 -3
- package/src/superlocalmemory/retrieval/temporal_utils.py +107 -0
- package/src/superlocalmemory/retrieval/temporal_validity_filter.py +155 -42
- package/src/superlocalmemory/retrieval/vector_store.py +214 -8
- package/src/superlocalmemory/server/api.py +5 -5
- package/src/superlocalmemory/server/egress_policy.py +258 -0
- package/src/superlocalmemory/server/rbac_enforce.py +32 -0
- package/src/superlocalmemory/server/route_mutations.py +20 -0
- package/src/superlocalmemory/server/routes/compliance.py +153 -7
- package/src/superlocalmemory/server/routes/data_io.py +43 -2
- package/src/superlocalmemory/server/routes/events.py +15 -0
- package/src/superlocalmemory/server/routes/memories.py +56 -3
- package/src/superlocalmemory/server/routes/mesh.py +82 -1
- package/src/superlocalmemory/server/routes/mesh_lock.py +54 -0
- package/src/superlocalmemory/server/routes/mesh_state.py +63 -0
- package/src/superlocalmemory/server/routes/v3_api.py +50 -27
- package/src/superlocalmemory/server/routes/ws.py +86 -0
- package/src/superlocalmemory/server/ui.py +6 -6
- package/src/superlocalmemory/server/unified_daemon.py +942 -119
- package/src/superlocalmemory/storage/_migration_internals.py +568 -0
- package/src/superlocalmemory/storage/_schema_version.py +110 -0
- package/src/superlocalmemory/storage/database.py +329 -24
- package/src/superlocalmemory/storage/embedding_migrator.py +246 -51
- package/src/superlocalmemory/storage/erasure_fence.py +45 -0
- package/src/superlocalmemory/storage/generation_fence.py +63 -0
- package/src/superlocalmemory/storage/migration_runner.py +140 -417
- package/src/superlocalmemory/storage/migrations/M009_model_lineage.py +40 -0
- package/src/superlocalmemory/storage/migrations/M033_projection_transactions.py +148 -0
- package/src/superlocalmemory/storage/migrations/M034_obligation_integrity.py +58 -0
- package/src/superlocalmemory/storage/migrations/M035_erasure_receipts.py +113 -0
- package/src/superlocalmemory/storage/migrations/M036_vector_row_map.py +107 -0
- package/src/superlocalmemory/storage/migrations/M037_manifest_hmac_version.py +162 -0
- package/src/superlocalmemory/storage/migrations/{M033_learning_feedback_channel.py → M038_learning_feedback_channel.py} +3 -3
- package/src/superlocalmemory/storage/migrations/M039_scene_fact_members.py +137 -0
- package/src/superlocalmemory/storage/migrations/__init__.py +4 -2
- package/src/superlocalmemory/storage/schema.py +67 -0
- package/src/superlocalmemory/storage/write_coordinator.py +125 -0
- package/src/superlocalmemory/trust/scorer.py +28 -4
- package/src/superlocalmemory/ui/index.html +14 -3
- package/src/superlocalmemory/ui/js/auto-settings.js +12 -1
- package/src/superlocalmemory/ui/js/brain.js +6 -4
- package/src/superlocalmemory/ui/js/compliance.js +66 -12
- package/src/superlocalmemory/ui/js/dashboard.js +13 -3
- package/src/superlocalmemory/ui/js/feedback.js +8 -2
- package/src/superlocalmemory/ui/js/lifecycle.js +7 -1
- package/src/superlocalmemory/ui/js/modal.js +272 -5
- package/src/superlocalmemory/ui/js/od-backup.js +9 -2
- package/src/superlocalmemory/ui/js/od-compliance-ext.js +301 -0
- package/src/superlocalmemory/ui/js/od-operations.js +154 -23
- package/src/superlocalmemory/ui/js/od-ops-health.js +417 -0
- package/src/superlocalmemory/ui/js/od-optimize.js +35 -21
- package/src/superlocalmemory/ui/js/od-team.js +9 -2
- package/src/superlocalmemory/ui/js/optimize.js +13 -16
- package/src/superlocalmemory/ui/js/profiles.js +7 -3
- package/src/superlocalmemory/ui/js/settings.js +7 -1
- package/src/superlocalmemory/vector/lancedb_backend.py +19 -9
- package/src/superlocalmemory/attribution/mathematical_dna.py +0 -235
- package/src/superlocalmemory/cli/post_install.py +0 -114
- package/src/superlocalmemory/core/clock_monitor.py +0 -45
- package/src/superlocalmemory/core/db_pool.py +0 -80
- package/src/superlocalmemory/core/error_catalog.py +0 -113
- package/src/superlocalmemory/core/loop_watchdog.py +0 -56
- package/src/superlocalmemory/core/priority_queue.py +0 -61
- package/src/superlocalmemory/core/pruning_engine.py +0 -216
- package/src/superlocalmemory/core/queue_dispatcher.py +0 -73
- package/src/superlocalmemory/core/slmignore.py +0 -125
- package/src/superlocalmemory/infra/heartbeat_monitor.py +0 -140
- package/src/superlocalmemory/infra/webhook_dispatcher.py +0 -247
- package/src/superlocalmemory/learning/quantization_scheduler.py +0 -320
- package/src/superlocalmemory/storage/access_control.py +0 -182
|
@@ -1,63 +1,33 @@
|
|
|
1
1
|
# Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
|
|
2
2
|
# Licensed under AGPL-3.0-or-later - see LICENSE file
|
|
3
|
-
# Part of SuperLocalMemory
|
|
3
|
+
# Part of SuperLocalMemory V4 | https://qualixar.com | https://varunpratap.com
|
|
4
4
|
|
|
5
|
-
"""Resource-safe
|
|
5
|
+
"""Resource-safe Streamable-HTTP integration for mcp==2.0.0.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
``
|
|
9
|
-
|
|
10
|
-
endpoint per request for the garbage collector. The response is the owner of
|
|
11
|
-
that per-request iterator, so SLM closes it at the response boundary.
|
|
7
|
+
mcp 2.0.0 deleted ``mcp.server.fastmcp.FastMCP``. Replacement is
|
|
8
|
+
``mcp.server.mcpserver.MCPServer`` with the same ``.tool()`` decorator and
|
|
9
|
+
``run(transport="stdio")``.
|
|
12
10
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
11
|
+
Fully-stateless is the default (see ``remote_mode.mcp_stateless`` and
|
|
12
|
+
``unified_daemon._configure_mcp_transport_settings``). Under
|
|
13
|
+
``stateless_http=True``:
|
|
16
14
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
15
|
+
* ``session_idle_timeout`` is illegal (SDK raises RuntimeError) and unused —
|
|
16
|
+
there are no transport sessions to reap.
|
|
17
|
+
* the EventStore (SSE Last-Event-ID resumability) is not used.
|
|
18
|
+
* therefore the old ``SLMFastMCP.streamable_http_app()`` override that
|
|
19
|
+
pre-created a ``StreamableHTTPSessionManager`` is gone — kwargs go
|
|
20
|
+
straight to ``MCPServer.streamable_http_app(...)``.
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
injected as the session manager's event store. A dropped SSE stream can
|
|
25
|
-
resume via ``Last-Event-ID`` instead of forcing a new ``initialize``
|
|
26
|
-
handshake.
|
|
27
|
-
|
|
28
|
-
KNOWN LIMITATIONS (not fixed here, documented for future work):
|
|
29
|
-
* RC-1 (mcp-remote orphan test-sessions): The mcp-remote v0.1.38 bug
|
|
30
|
-
creates a ``testTransport``/``testClient`` that is never closed. This
|
|
31
|
-
leaks one zombie session per mcp-remote startup. Tracked upstream.
|
|
32
|
-
Session idle-timeout mitigates the accumulation.
|
|
33
|
-
* Client reconnect after daemon restart: ``SLMInMemoryEventStore`` is
|
|
34
|
-
in-memory only and does not survive daemon process restart. After a
|
|
35
|
-
restart, stale ``Last-Event-ID`` values are unknown to the new store;
|
|
36
|
-
``replay_events_after`` returns ``None`` and the client must
|
|
37
|
-
re-initialize. A SQLite-backed EventStore is planned for v3.9.
|
|
38
|
-
* Claude Code client bug Anthropic #48557: Claude Code may send a stale
|
|
39
|
-
MCP session ID after server restart. This is a client-side defect;
|
|
40
|
-
SLM cannot fix it from the server.
|
|
41
|
-
* Event-loop stall during background maintenance (RC-6): The pruner-lock
|
|
42
|
-
stall fix lives in Workstream A+F (already merged into this branch).
|
|
43
|
-
This module depends on that fix; see fix/3.8.4 merge commit.
|
|
22
|
+
Application-level ``session_init`` / ``close_session`` are orthogonal
|
|
23
|
+
(session_id is a str param persisted in the memories table) and unchanged.
|
|
44
24
|
"""
|
|
45
25
|
|
|
46
26
|
from __future__ import annotations
|
|
47
27
|
|
|
48
28
|
import logging
|
|
49
|
-
import os
|
|
50
|
-
from collections import OrderedDict, deque
|
|
51
|
-
from typing import Any
|
|
52
29
|
|
|
53
|
-
from mcp.server.
|
|
54
|
-
from mcp.server.streamable_http import (
|
|
55
|
-
EventCallback,
|
|
56
|
-
EventId,
|
|
57
|
-
EventMessage,
|
|
58
|
-
EventStore,
|
|
59
|
-
StreamId,
|
|
60
|
-
)
|
|
30
|
+
from mcp.server.mcpserver import MCPServer
|
|
61
31
|
from sse_starlette.sse import EventSourceResponse
|
|
62
32
|
from starlette.types import Receive, Scope, Send
|
|
63
33
|
|
|
@@ -65,196 +35,9 @@ from superlocalmemory import __version__
|
|
|
65
35
|
|
|
66
36
|
logger = logging.getLogger(__name__)
|
|
67
37
|
|
|
68
|
-
# ---------------------------------------------------------------------------
|
|
69
|
-
# Session idle timeout — configurable default
|
|
70
|
-
# ---------------------------------------------------------------------------
|
|
71
|
-
|
|
72
|
-
#: Default session idle timeout in seconds.
|
|
73
|
-
#:
|
|
74
|
-
#: Rationale for 600 s (10 minutes), not the SDK's suggested 1800 s:
|
|
75
|
-
#: * At 1800 s, with mcp-remote leaking 3 zombie sessions per conversation
|
|
76
|
-
#: start and a typical conversation cadence of one every 15 min,
|
|
77
|
-
#: up to (1800/15) × 3 = 360 zombie sessions can accumulate in the worst
|
|
78
|
-
#: case before the first one expires. 600 s bounds this to ~6.
|
|
79
|
-
#: * 600 s is well above the 15 s SSE keepalive interval, the typical
|
|
80
|
-
#: in-conversation idle gap (<5 min), and the mcp-remote reconnect window.
|
|
81
|
-
#: * Active sessions push their idle deadline forward on every tool call,
|
|
82
|
-
#: so a user making any request within 10 min never loses their session.
|
|
83
|
-
#: * A session idle for exactly 10 min (user stepped away) is reaped and
|
|
84
|
-
#: recreated transparently on next tool call via mcp-remote reconnect.
|
|
85
|
-
#:
|
|
86
|
-
#: CRIT — anti-patterns to avoid:
|
|
87
|
-
#: Too aggressive (<60 s): frequent idle-deadline checks waste CPU in
|
|
88
|
-
#: AnyIO's CancelScope machinery; legitimate quiet users are interrupted.
|
|
89
|
-
#: Too lenient (>7200 s = 2 h): zombie sessions accumulate at rates that
|
|
90
|
-
#: can reach 100+ entries, degrading AnyIO task-group scheduling.
|
|
91
|
-
_DEFAULT_SESSION_IDLE_TIMEOUT: float = 600.0
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
def _slm_session_idle_timeout() -> float:
|
|
95
|
-
"""Return the configured MCP session idle timeout in seconds.
|
|
96
|
-
|
|
97
|
-
Reads ``SLM_MCP_SESSION_IDLE_TIMEOUT_S`` from the environment.
|
|
98
|
-
Falls back to :data:`_DEFAULT_SESSION_IDLE_TIMEOUT` on missing or
|
|
99
|
-
non-numeric values. A value ≤ 0 is also treated as the default.
|
|
100
|
-
"""
|
|
101
|
-
raw = os.environ.get("SLM_MCP_SESSION_IDLE_TIMEOUT_S", "").strip()
|
|
102
|
-
if raw:
|
|
103
|
-
try:
|
|
104
|
-
val = float(raw)
|
|
105
|
-
if val > 0:
|
|
106
|
-
return val
|
|
107
|
-
except ValueError:
|
|
108
|
-
logger.warning(
|
|
109
|
-
"SLM_MCP_SESSION_IDLE_TIMEOUT_S=%r is not a valid number; "
|
|
110
|
-
"using default %s s",
|
|
111
|
-
raw,
|
|
112
|
-
_DEFAULT_SESSION_IDLE_TIMEOUT,
|
|
113
|
-
)
|
|
114
|
-
return _DEFAULT_SESSION_IDLE_TIMEOUT
|
|
115
|
-
|
|
116
38
|
|
|
117
39
|
# ---------------------------------------------------------------------------
|
|
118
|
-
#
|
|
119
|
-
# ---------------------------------------------------------------------------
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
class SLMInMemoryEventStore(EventStore):
|
|
123
|
-
"""Bounded in-memory event store for MCP SSE resumability.
|
|
124
|
-
|
|
125
|
-
Enables clients to reconnect to the GET /mcp SSE endpoint and replay
|
|
126
|
-
missed events via the ``Last-Event-ID`` header instead of performing a
|
|
127
|
-
full ``initialize`` handshake (which creates a new session and adds to
|
|
128
|
-
the zombie-session count).
|
|
129
|
-
|
|
130
|
-
Design choices:
|
|
131
|
-
---------------
|
|
132
|
-
* **Per-stream bounded deque** — each stream_id maps to a
|
|
133
|
-
``deque(maxlen=max_events_per_stream)``. Oldest events are evicted
|
|
134
|
-
automatically (FIFO) when the cap is hit.
|
|
135
|
-
* **Global stream count cap** — the store tracks at most
|
|
136
|
-
``max_streams`` distinct stream_ids. When exceeded, the least-recently
|
|
137
|
-
used stream is evicted from the dict. This bounds total memory usage.
|
|
138
|
-
* **Async-safe, lock-free** — all access happens on a single asyncio event
|
|
139
|
-
loop; no ``asyncio.Lock`` is needed.
|
|
140
|
-
* **Priming events (message=None) are stored but skipped during replay** —
|
|
141
|
-
the SDK mints a fresh priming event in the replay path (see
|
|
142
|
-
``StreamableHTTPServerTransport._replay_events``).
|
|
143
|
-
|
|
144
|
-
Memory bound:
|
|
145
|
-
-------------
|
|
146
|
-
``max_events_per_stream`` × ~2 KB (avg JSONRPCMessage) × ``max_streams``
|
|
147
|
-
= 200 × 2 KB × 100 = ~40 MB in the absolute worst case.
|
|
148
|
-
At steady state with idle-timeout reaping sessions after 10 min, the
|
|
149
|
-
typical load is 3–6 active streams × 200 events × 2 KB ≈ 1.2–2.4 MB.
|
|
150
|
-
|
|
151
|
-
Known limitation:
|
|
152
|
-
-----------------
|
|
153
|
-
This store is in-memory only. It does not survive a daemon process
|
|
154
|
-
restart. After a restart, any ``Last-Event-ID`` from a previous process
|
|
155
|
-
is unknown; ``replay_events_after`` returns ``None`` and the client must
|
|
156
|
-
re-initialize. A SQLite-backed EventStore is planned for v3.9.
|
|
157
|
-
"""
|
|
158
|
-
|
|
159
|
-
def __init__(
|
|
160
|
-
self,
|
|
161
|
-
max_events_per_stream: int = 200,
|
|
162
|
-
max_streams: int = 100,
|
|
163
|
-
) -> None:
|
|
164
|
-
"""Initialise the bounded event store.
|
|
165
|
-
|
|
166
|
-
Args:
|
|
167
|
-
max_events_per_stream: Maximum number of events stored per
|
|
168
|
-
stream before oldest events are dropped. Default 200
|
|
169
|
-
(~400 KB per stream at 2 KB/event).
|
|
170
|
-
max_streams: Maximum number of distinct stream_ids tracked.
|
|
171
|
-
When exceeded, the least-recently used stream is evicted
|
|
172
|
-
(all its events are lost). Default 100.
|
|
173
|
-
"""
|
|
174
|
-
self._max_events = max_events_per_stream
|
|
175
|
-
self._max_streams = max_streams
|
|
176
|
-
# OrderedDict maintains insertion/access order for LRU eviction.
|
|
177
|
-
# stream_id → deque[(event_id, message)]
|
|
178
|
-
self._store: OrderedDict[str, deque] = OrderedDict()
|
|
179
|
-
# Monotonically incrementing counter; single event loop → no lock needed.
|
|
180
|
-
self._counter: int = 0
|
|
181
|
-
|
|
182
|
-
# ------------------------------------------------------------------
|
|
183
|
-
# EventStore ABC implementation
|
|
184
|
-
# ------------------------------------------------------------------
|
|
185
|
-
|
|
186
|
-
async def store_event(
|
|
187
|
-
self,
|
|
188
|
-
stream_id: StreamId,
|
|
189
|
-
message: Any, # JSONRPCMessage | None
|
|
190
|
-
) -> EventId:
|
|
191
|
-
"""Store an event for the given stream and return its unique event_id.
|
|
192
|
-
|
|
193
|
-
Args:
|
|
194
|
-
stream_id: The stream (GET /mcp SSE connection) this event belongs to.
|
|
195
|
-
message: The JSON-RPC message, or ``None`` for priming events.
|
|
196
|
-
|
|
197
|
-
Returns:
|
|
198
|
-
A monotonically incrementing string event_id.
|
|
199
|
-
"""
|
|
200
|
-
self._counter += 1
|
|
201
|
-
event_id: EventId = str(self._counter)
|
|
202
|
-
|
|
203
|
-
if stream_id not in self._store:
|
|
204
|
-
# Evict oldest stream if at capacity
|
|
205
|
-
if len(self._store) >= self._max_streams:
|
|
206
|
-
oldest_stream, _ = self._store.popitem(last=False)
|
|
207
|
-
logger.debug(
|
|
208
|
-
"SLMInMemoryEventStore: evicted oldest stream %r "
|
|
209
|
-
"(max_streams=%d reached)",
|
|
210
|
-
oldest_stream,
|
|
211
|
-
self._max_streams,
|
|
212
|
-
)
|
|
213
|
-
self._store[stream_id] = deque(maxlen=self._max_events)
|
|
214
|
-
else:
|
|
215
|
-
# Move to "most recently used" end so LRU eviction works correctly
|
|
216
|
-
self._store.move_to_end(stream_id)
|
|
217
|
-
|
|
218
|
-
self._store[stream_id].append((event_id, message))
|
|
219
|
-
return event_id
|
|
220
|
-
|
|
221
|
-
async def replay_events_after(
|
|
222
|
-
self,
|
|
223
|
-
last_event_id: EventId,
|
|
224
|
-
send_callback: EventCallback,
|
|
225
|
-
) -> StreamId | None:
|
|
226
|
-
"""Replay events that occurred after ``last_event_id``.
|
|
227
|
-
|
|
228
|
-
Searches all tracked streams for ``last_event_id`` and forwards every
|
|
229
|
-
subsequent non-None event to ``send_callback``. Priming events
|
|
230
|
-
(``message=None``) are skipped; the SDK mints a fresh priming event
|
|
231
|
-
in the replay path.
|
|
232
|
-
|
|
233
|
-
Args:
|
|
234
|
-
last_event_id: The ID of the last event the client received.
|
|
235
|
-
send_callback: Async callback that receives each missed
|
|
236
|
-
``EventMessage`` in order.
|
|
237
|
-
|
|
238
|
-
Returns:
|
|
239
|
-
The stream_id that contained ``last_event_id``, or ``None`` if
|
|
240
|
-
the event was not found (client must re-initialize).
|
|
241
|
-
"""
|
|
242
|
-
for stream_id, events in self._store.items():
|
|
243
|
-
found = False
|
|
244
|
-
for event_id, message in events:
|
|
245
|
-
if found:
|
|
246
|
-
if message is not None:
|
|
247
|
-
await send_callback(EventMessage(message=message, event_id=event_id))
|
|
248
|
-
elif event_id == last_event_id:
|
|
249
|
-
found = True
|
|
250
|
-
if found:
|
|
251
|
-
return stream_id
|
|
252
|
-
# last_event_id not in any stream (evicted or never seen)
|
|
253
|
-
return None
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
# ---------------------------------------------------------------------------
|
|
257
|
-
# SSE resource guard (unchanged from pre-E)
|
|
40
|
+
# SSE resource guard (still useful if a host opts into non-json SSE responses)
|
|
258
41
|
# ---------------------------------------------------------------------------
|
|
259
42
|
|
|
260
43
|
|
|
@@ -278,109 +61,25 @@ def install_streamable_http_resource_guard() -> None:
|
|
|
278
61
|
|
|
279
62
|
|
|
280
63
|
# ---------------------------------------------------------------------------
|
|
281
|
-
# SLMFastMCP —
|
|
64
|
+
# SLMFastMCP — thin MCPServer wrapper (name kept for import stability)
|
|
282
65
|
# ---------------------------------------------------------------------------
|
|
283
66
|
|
|
284
67
|
|
|
285
|
-
class SLMFastMCP(
|
|
286
|
-
"""
|
|
287
|
-
session lifecycle management (Workstream E).
|
|
288
|
-
|
|
289
|
-
Additions over the base FastMCP:
|
|
68
|
+
class SLMFastMCP(MCPServer):
|
|
69
|
+
"""MCPServer with SLM release identity.
|
|
290
70
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
71
|
+
Named ``SLMFastMCP`` for backward-compatible imports. Behaviour is
|
|
72
|
+
fully-stateless by default at the *call site* via
|
|
73
|
+
``streamable_http_app(stateless_http=True, json_response=True, ...)`` —
|
|
74
|
+
this class does not override that method.
|
|
295
75
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
full ``initialize`` round-trip.
|
|
299
|
-
|
|
300
|
-
3. **Ordering guard**: ``streamable_http_app()`` reads the ``stateless_http``
|
|
301
|
-
flag from ``self.settings`` at call time. If the flag is ``True``
|
|
302
|
-
(set by ``_configure_mcp_transport_settings()`` in unified_daemon.py),
|
|
303
|
-
``session_idle_timeout`` is suppressed — the MCP SDK raises
|
|
304
|
-
``RuntimeError`` if both are set simultaneously. If
|
|
305
|
-
``streamable_http_app()`` is called before settings are configured
|
|
306
|
-
(e.g. in unit tests), the default ``stateless_http=False`` applies
|
|
307
|
-
safely.
|
|
76
|
+
``version`` is passed to ``MCPServer.__init__`` directly (no private
|
|
77
|
+
``_mcp_server.version`` poke — that attribute is gone in mcp 2.0.0).
|
|
308
78
|
"""
|
|
309
79
|
|
|
310
80
|
def __init__(self, *args, product_version: str = __version__, **kwargs) -> None:
|
|
81
|
+
# MCPServer takes version= as a first-class kwarg.
|
|
82
|
+
kwargs.setdefault("version", product_version)
|
|
311
83
|
super().__init__(*args, **kwargs)
|
|
312
|
-
#
|
|
313
|
-
# server. Without an explicit value it reports the installed ``mcp``
|
|
314
|
-
# library version, which makes an SLM 3.7 server identify itself as
|
|
315
|
-
# (for example) 1.27.1 to every IDE client.
|
|
316
|
-
self._mcp_server.version = product_version
|
|
317
|
-
|
|
318
|
-
def streamable_http_app(self): # type: ignore[override]
|
|
319
|
-
"""Return the Streamable-HTTP Starlette app.
|
|
320
|
-
|
|
321
|
-
Pre-creates the ``StreamableHTTPSessionManager`` with SLM-specific
|
|
322
|
-
parameters before delegating to ``super()`` (which skips re-creation
|
|
323
|
-
because the manager is already set).
|
|
324
|
-
|
|
325
|
-
Ordering guarantee
|
|
326
|
-
------------------
|
|
327
|
-
In production ``unified_daemon.py`` always calls
|
|
328
|
-
``_configure_mcp_transport_settings(fastmcp)`` *before* this method,
|
|
329
|
-
so ``self.settings.stateless_http`` reflects the correct runtime value.
|
|
330
|
-
If this method is called first (unit tests, embedded hosts), the
|
|
331
|
-
default ``stateless_http=False`` is used — safe because the stateless
|
|
332
|
-
guard only matters when ``stateless_http=True`` (avoid SDK RuntimeError).
|
|
333
|
-
|
|
334
|
-
Dependency note (A+F)
|
|
335
|
-
---------------------
|
|
336
|
-
This method assumes that ``fix/3.8.4-A+F`` (already merged into this
|
|
337
|
-
branch) has eliminated the pruner-lock stall that caused event-loop
|
|
338
|
-
starvation during background maintenance. Session idle-timeout and
|
|
339
|
-
the event store improve resilience to transient drops, but they cannot
|
|
340
|
-
compensate for a fully stalled event loop.
|
|
341
|
-
"""
|
|
84
|
+
# Optional SSE resource guard for hosts that still request event-stream.
|
|
342
85
|
install_streamable_http_resource_guard()
|
|
343
|
-
|
|
344
|
-
if self._session_manager is None:
|
|
345
|
-
from mcp.server.streamable_http_manager import StreamableHTTPSessionManager
|
|
346
|
-
|
|
347
|
-
# Ordering guard: read stateless_http defensively.
|
|
348
|
-
# ``_configure_mcp_transport_settings()`` may not have been called
|
|
349
|
-
# yet; ``getattr`` with a False default ensures we never pass
|
|
350
|
-
# session_idle_timeout=<value> into a stateless manager (SDK raises
|
|
351
|
-
# RuntimeError if both are set simultaneously).
|
|
352
|
-
is_stateless: bool = getattr(self.settings, "stateless_http", False)
|
|
353
|
-
|
|
354
|
-
# Idle timeout: finite for stateful sessions; None for stateless
|
|
355
|
-
# (stateless sessions have no identity — there is nothing to reap).
|
|
356
|
-
idle_timeout: float | None = (
|
|
357
|
-
None if is_stateless else _slm_session_idle_timeout()
|
|
358
|
-
)
|
|
359
|
-
|
|
360
|
-
# Event store: inject SLMInMemoryEventStore for stateful mode.
|
|
361
|
-
# Stateless mode must not receive an event store (SDK limitation).
|
|
362
|
-
# If the caller already configured an event store (via FastMCP
|
|
363
|
-
# constructor argument), respect it — do not replace with ours.
|
|
364
|
-
if is_stateless:
|
|
365
|
-
event_store: EventStore | None = None
|
|
366
|
-
else:
|
|
367
|
-
event_store = self._event_store or SLMInMemoryEventStore()
|
|
368
|
-
|
|
369
|
-
self._session_manager = StreamableHTTPSessionManager(
|
|
370
|
-
app=self._mcp_server,
|
|
371
|
-
event_store=event_store,
|
|
372
|
-
retry_interval=self._retry_interval,
|
|
373
|
-
json_response=self.settings.json_response,
|
|
374
|
-
stateless=is_stateless,
|
|
375
|
-
security_settings=self.settings.transport_security,
|
|
376
|
-
session_idle_timeout=idle_timeout,
|
|
377
|
-
)
|
|
378
|
-
logger.info(
|
|
379
|
-
"SLM MCP session manager created: stateless=%s, "
|
|
380
|
-
"idle_timeout=%ss, event_store=%s",
|
|
381
|
-
is_stateless,
|
|
382
|
-
idle_timeout,
|
|
383
|
-
type(event_store).__name__ if event_store else "None",
|
|
384
|
-
)
|
|
385
|
-
|
|
386
|
-
return super().streamable_http_app()
|
|
@@ -49,6 +49,11 @@ _PROFILE_FULL: frozenset[str] = frozenset({ # 34 base — EXPLICIT literal, NOT
|
|
|
49
49
|
"slm_compress", "slm_retrieve", "slm_cache_set", "slm_cache_get", "slm_optimize_stats",
|
|
50
50
|
# v3.8.0: bounded-loop tools (CLI + /slm-loop command + MCP).
|
|
51
51
|
"slm_loop_run", "slm_loop_history", "slm_loop_show",
|
|
52
|
+
# v4: prestage_context IS registered (see mcp/server.py) but is deliberately
|
|
53
|
+
# NOT in full/power. The profile names are a user-facing config contract
|
|
54
|
+
# (SLM_MCP_PROFILE=full42) and the published tool-count table depends on
|
|
55
|
+
# them; adding a tool here would make "full42" serve 43 tools. New tools
|
|
56
|
+
# reach users through the `whole` profile until a profile rename is shipped.
|
|
52
57
|
}) | _PROFILE_FULL_MESH # 42
|
|
53
58
|
|
|
54
59
|
_PROFILE_POWER: frozenset[str] = _PROFILE_FULL | frozenset({ # 54
|
|
@@ -36,8 +36,16 @@ def register_resources(server, get_engine: Callable) -> None:
|
|
|
36
36
|
try:
|
|
37
37
|
from superlocalmemory.hooks.auto_recall import AutoRecall
|
|
38
38
|
from superlocalmemory.mcp._pool_adapter import pool_recall
|
|
39
|
+
# Pass engine so the soft-prompt bridge (engine->AutoInvoker) fires on
|
|
40
|
+
# this real MCP session-context surface; recall_fn still drives recall.
|
|
41
|
+
# Fail-soft: if the engine is unavailable, recall via pool_recall alone.
|
|
42
|
+
try:
|
|
43
|
+
_eng = get_engine()
|
|
44
|
+
except Exception:
|
|
45
|
+
_eng = None
|
|
39
46
|
auto = AutoRecall(
|
|
40
47
|
recall_fn=pool_recall,
|
|
48
|
+
engine=_eng,
|
|
41
49
|
config={"enabled": True, "max_memories_injected": 10, "relevance_threshold": 0.3},
|
|
42
50
|
)
|
|
43
51
|
context = auto.get_session_context(query="recent decisions and important context")
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
|
|
2
2
|
# Licensed under AGPL-3.0-or-later - see LICENSE file
|
|
3
|
-
# Part of SuperLocalMemory
|
|
3
|
+
# Part of SuperLocalMemory V4 | https://qualixar.com | https://varunpratap.com
|
|
4
4
|
|
|
5
|
-
"""SuperLocalMemory
|
|
5
|
+
"""SuperLocalMemory V4 — MCP Server.
|
|
6
6
|
|
|
7
7
|
Clean MCP server calling V3 MemoryEngine. Supports all MCP-compatible IDEs.
|
|
8
8
|
|
|
@@ -27,11 +27,13 @@ _os.environ.setdefault('SLM_SKIP_DEP_CHECK', '1')
|
|
|
27
27
|
import logging
|
|
28
28
|
import sys
|
|
29
29
|
|
|
30
|
+
from superlocalmemory import __version__ as _slm_version
|
|
30
31
|
from superlocalmemory.mcp.http_transport import SLMFastMCP
|
|
31
32
|
|
|
32
33
|
logger = logging.getLogger(__name__)
|
|
33
34
|
|
|
34
|
-
|
|
35
|
+
# mcp 2.0.0: MCPServer takes version= directly (no private _mcp_server poke).
|
|
36
|
+
server = SLMFastMCP("SuperLocalMemory V4", version=_slm_version)
|
|
35
37
|
|
|
36
38
|
# Lazy engine singleton -------------------------------------------------------
|
|
37
39
|
|
|
@@ -81,7 +83,7 @@ def reset_engine():
|
|
|
81
83
|
# Antigravity, Windsurf) and a maximal SLM registration crowds out
|
|
82
84
|
# other MCP servers the user may have installed.
|
|
83
85
|
# Admin/diagnostics tools remain available via CLI (`slm <command>`).
|
|
84
|
-
# Set SLM_MCP_ALL_TOOLS=1 to enable all
|
|
86
|
+
# Set SLM_MCP_ALL_TOOLS=1 to enable all 87 tools (power users).
|
|
85
87
|
|
|
86
88
|
import os as _os_reg
|
|
87
89
|
|
|
@@ -97,6 +99,11 @@ _ESSENTIAL_TOOLS: set[str] = {
|
|
|
97
99
|
"report_feedback",
|
|
98
100
|
# Memory management (2)
|
|
99
101
|
"forget", "run_maintenance",
|
|
102
|
+
# NOTE: prestage_context IS registered (see register_prestage_tool below)
|
|
103
|
+
# but is deliberately absent from the default surface. _ESSENTIAL_TOOLS must
|
|
104
|
+
# stay at 42 to match the full42 profile: the profile names are a
|
|
105
|
+
# user-facing config contract and the published tool-count table depends on
|
|
106
|
+
# them. New tools reach users via the `whole` profile until a rename ships.
|
|
100
107
|
# Infinite memory + learning (4)
|
|
101
108
|
"consolidate_cognitive", "get_soft_prompts",
|
|
102
109
|
"set_mode", "report_outcome",
|
|
@@ -263,6 +270,46 @@ from superlocalmemory.mcp.tools_optimize import register_optimize_tools
|
|
|
263
270
|
register_optimize_tools(_target) # v3.6.11: Surface B Optimize tools (proxy-free)
|
|
264
271
|
from superlocalmemory.mcp.tools_loops import register_loop_tools
|
|
265
272
|
register_loop_tools(_target, get_engine) # v3.8.0: bounded-loop tools (CLI+command+MCP)
|
|
273
|
+
from superlocalmemory.mcp.tools_ops import register_ops_tools
|
|
274
|
+
register_ops_tools(_target, get_engine) # Wave-3: operational recovery & admin remediation
|
|
275
|
+
from superlocalmemory.mcp.tools_context import register_prestage_tool
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _prestage_recall(query: str, limit: int, profile_id: str, as_of: str | None = None):
|
|
279
|
+
"""Bridge engine.recall → prestage_context recall_fn shape."""
|
|
280
|
+
engine = get_engine()
|
|
281
|
+
if hasattr(engine, "profile_id") and profile_id:
|
|
282
|
+
try:
|
|
283
|
+
engine.profile_id = profile_id
|
|
284
|
+
except Exception:
|
|
285
|
+
pass
|
|
286
|
+
try:
|
|
287
|
+
if as_of is not None:
|
|
288
|
+
results = engine.recall(query, limit=limit, as_of=as_of)
|
|
289
|
+
else:
|
|
290
|
+
results = engine.recall(query, limit=limit)
|
|
291
|
+
except TypeError:
|
|
292
|
+
results = engine.recall(query, limit=limit)
|
|
293
|
+
out: list[dict] = []
|
|
294
|
+
for r in results or []:
|
|
295
|
+
if isinstance(r, dict):
|
|
296
|
+
out.append({
|
|
297
|
+
"id": str(r.get("fact_id") or r.get("id") or ""),
|
|
298
|
+
"text": str(r.get("content") or r.get("text") or ""),
|
|
299
|
+
"score": float(r.get("score") or r.get("relevance_score") or 0.0),
|
|
300
|
+
"source": str(r.get("source") or "recall"),
|
|
301
|
+
})
|
|
302
|
+
else:
|
|
303
|
+
out.append({
|
|
304
|
+
"id": str(getattr(r, "fact_id", "") or ""),
|
|
305
|
+
"text": str(getattr(r, "content", "") or ""),
|
|
306
|
+
"score": float(getattr(r, "score", 0.0) or 0.0),
|
|
307
|
+
"source": "recall",
|
|
308
|
+
})
|
|
309
|
+
return out
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
register_prestage_tool(_target, _prestage_recall)
|
|
266
313
|
|
|
267
314
|
|
|
268
315
|
# Keep stdio MCP processes thin until a tool truly needs a local LIGHT engine.
|
|
@@ -63,13 +63,32 @@ def authorize_mcp_mutation(
|
|
|
63
63
|
MCP URL/env agent identifiers remain audit attribution only. The trusted
|
|
64
64
|
principal is derived from the private local install capability, matching
|
|
65
65
|
the canonical ingestion and recall-worker mutation boundaries.
|
|
66
|
+
|
|
67
|
+
Phase 1: Routes through the admission registry so enterprise policy applies
|
|
68
|
+
even when the caller has local trust (trust-hook alone cannot bypass registry).
|
|
69
|
+
AdmissionDenied propagates to the caller; personal OWNER is always admitted.
|
|
66
70
|
"""
|
|
67
71
|
if operation not in {"update", "delete"}:
|
|
68
72
|
raise ValueError("MCP mutations must use update or delete policy")
|
|
69
73
|
|
|
74
|
+
from superlocalmemory.core.actor_context import Transport
|
|
75
|
+
from superlocalmemory.core.admission import (
|
|
76
|
+
AdmissionDenied,
|
|
77
|
+
_resolve_deployment,
|
|
78
|
+
admit,
|
|
79
|
+
resolve_actor,
|
|
80
|
+
)
|
|
70
81
|
from superlocalmemory.core.engine_ingestion import local_trusted_actor_id
|
|
82
|
+
from superlocalmemory.core.operation_request import OperationKind
|
|
71
83
|
from superlocalmemory.mcp.agent_context import get_current_agent_id
|
|
72
84
|
|
|
85
|
+
deployment = _resolve_deployment()
|
|
86
|
+
tier = "enterprise" if deployment.is_enterprise else "personal"
|
|
87
|
+
mode = "company" if deployment.is_enterprise else "local"
|
|
88
|
+
actor = resolve_actor(Transport.MCP, tier=tier, mode=mode)
|
|
89
|
+
kind = OperationKind.FORGET if operation == "delete" else OperationKind.CORRECT
|
|
90
|
+
admit(kind, actor, mode=mode) # raises AdmissionDenied on deny
|
|
91
|
+
|
|
73
92
|
source_agent_id = get_current_agent_id(env_fallback=True)
|
|
74
93
|
context = {
|
|
75
94
|
"operation": operation,
|
|
@@ -22,6 +22,10 @@ import logging
|
|
|
22
22
|
import uuid
|
|
23
23
|
from typing import TYPE_CHECKING, Callable
|
|
24
24
|
|
|
25
|
+
from mcp.types import ToolAnnotations
|
|
26
|
+
|
|
27
|
+
from superlocalmemory.core.admission import admits
|
|
28
|
+
from superlocalmemory.core.operation_request import OperationKind
|
|
25
29
|
from superlocalmemory.infra.data_root import state_path
|
|
26
30
|
from superlocalmemory.mcp.shared import authorize_mcp_mutation
|
|
27
31
|
from superlocalmemory.storage.read_connection import ReadConnectionFactory
|
|
@@ -257,7 +261,7 @@ def register_active_tools(server, get_engine: Callable) -> None:
|
|
|
257
261
|
# ------------------------------------------------------------------
|
|
258
262
|
# 1. session_init — Auto-recall project context at session start
|
|
259
263
|
# ------------------------------------------------------------------
|
|
260
|
-
@server.tool()
|
|
264
|
+
@server.tool(annotations=ToolAnnotations(readOnlyHint=True))
|
|
261
265
|
async def session_init(
|
|
262
266
|
project_path: str = "",
|
|
263
267
|
query: str = "",
|
|
@@ -289,7 +293,7 @@ def register_active_tools(server, get_engine: Callable) -> None:
|
|
|
289
293
|
from superlocalmemory.mcp._pool_adapter import pool_recall
|
|
290
294
|
|
|
291
295
|
engine = get_engine()
|
|
292
|
-
rules = RulesEngine()
|
|
296
|
+
rules = RulesEngine(config_path=state_path("config.json"))
|
|
293
297
|
|
|
294
298
|
if not rules.should_recall("session_start"):
|
|
295
299
|
return {
|
|
@@ -436,6 +440,20 @@ def register_active_tools(server, get_engine: Callable) -> None:
|
|
|
436
440
|
# weaker ad-hoc path when the mandatory renderer fails.
|
|
437
441
|
context = ""
|
|
438
442
|
|
|
443
|
+
# Live soft-prompt injection (Phase 5): prepend the profile's behavioral
|
|
444
|
+
# soft prompt so it reaches the session_init context agents actually
|
|
445
|
+
# consume (same engine->AutoInvoker bridge AutoRecall uses). Fail-soft.
|
|
446
|
+
try:
|
|
447
|
+
_sp_getter = getattr(
|
|
448
|
+
getattr(engine, "_auto_invoker", None),
|
|
449
|
+
"_get_soft_prompt_text", None,
|
|
450
|
+
)
|
|
451
|
+
_soft_prompt = _sp_getter() if callable(_sp_getter) else ""
|
|
452
|
+
if _soft_prompt:
|
|
453
|
+
context = f"{_soft_prompt}\n\n{context}" if context else _soft_prompt
|
|
454
|
+
except Exception as exc:
|
|
455
|
+
logger.warning("session_init soft-prompt injection failed: %s", exc)
|
|
456
|
+
|
|
439
457
|
# GAP-FIX (v3.4.65 delivery-lead): the memories[] array is part of
|
|
440
458
|
# the MCP response Claude Code ingests — it MUST be bounded too, not
|
|
441
459
|
# just the rendered `context` string. Previously full unclamped
|
|
@@ -544,6 +562,7 @@ def register_active_tools(server, get_engine: Callable) -> None:
|
|
|
544
562
|
# 2. observe — Auto-capture decisions/bugs/preferences
|
|
545
563
|
# ------------------------------------------------------------------
|
|
546
564
|
@server.tool()
|
|
565
|
+
@admits(OperationKind.REMEMBER)
|
|
547
566
|
async def observe(
|
|
548
567
|
content: str,
|
|
549
568
|
agent_id: str | None = None,
|
|
@@ -568,7 +587,7 @@ def register_active_tools(server, get_engine: Callable) -> None:
|
|
|
568
587
|
from superlocalmemory.hooks.rules_engine import RulesEngine
|
|
569
588
|
from superlocalmemory.mcp._pool_adapter import pool_store
|
|
570
589
|
|
|
571
|
-
rules = RulesEngine()
|
|
590
|
+
rules = RulesEngine(config_path=state_path("config.json"))
|
|
572
591
|
|
|
573
592
|
auto = AutoCapture(
|
|
574
593
|
store_fn=pool_store,
|
|
@@ -626,6 +645,7 @@ def register_active_tools(server, get_engine: Callable) -> None:
|
|
|
626
645
|
# 3. report_feedback — Explicit feedback for learning
|
|
627
646
|
# ------------------------------------------------------------------
|
|
628
647
|
@server.tool()
|
|
648
|
+
@admits(OperationKind.REMEMBER)
|
|
629
649
|
async def report_feedback(
|
|
630
650
|
fact_id: str,
|
|
631
651
|
feedback: str = "relevant",
|
|
@@ -741,6 +761,7 @@ def register_active_tools(server, get_engine: Callable) -> None:
|
|
|
741
761
|
# ------------------------------------------------------------------
|
|
742
762
|
|
|
743
763
|
@server.tool()
|
|
764
|
+
@admits(OperationKind.CONSOLIDATE)
|
|
744
765
|
async def close_session(session_id: str = "") -> dict:
|
|
745
766
|
"""Close the current session and create temporal summary events.
|
|
746
767
|
|
|
@@ -794,10 +815,10 @@ def register_active_tools(server, get_engine: Callable) -> None:
|
|
|
794
815
|
# ------------------------------------------------------------------
|
|
795
816
|
|
|
796
817
|
@server.tool()
|
|
818
|
+
@admits(OperationKind.CORRECT)
|
|
797
819
|
async def core_memory(
|
|
798
820
|
action: str,
|
|
799
821
|
fact_id: str = "",
|
|
800
|
-
profile_id: str = "default",
|
|
801
822
|
) -> dict:
|
|
802
823
|
"""Manage the explicit Core Memory pin set (v3.4.65).
|
|
803
824
|
|