superlocalmemory 4.1.21 → 4.1.23
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 +3 -3
- package/CHANGELOG.md +210 -0
- package/README.md +101 -33
- package/antigravity-plugin/_GENERATED.md +33 -0
- package/antigravity-plugin/agents/slm-governance-advisor.md +78 -0
- package/antigravity-plugin/agents/slm-loop-runner.md +82 -0
- package/antigravity-plugin/agents/slm-memory-advisor.md +50 -0
- package/antigravity-plugin/agents/slm-optimize-advisor.md +44 -0
- package/antigravity-plugin/commands/slm-loop.md +33 -0
- package/antigravity-plugin/hooks/hooks.json +59 -0
- package/antigravity-plugin/hooks.json +42 -0
- package/antigravity-plugin/mcp_config.json +14 -0
- package/antigravity-plugin/plugin.json +25 -0
- package/antigravity-plugin/scripts/antigravity_hook_adapter.py +85 -0
- package/antigravity-plugin/skills/slm-bot-memory/SKILL.md +146 -0
- package/antigravity-plugin/skills/slm-cache/SKILL.md +147 -0
- package/antigravity-plugin/skills/slm-compress/SKILL.md +150 -0
- package/antigravity-plugin/skills/slm-getting-started-bot/SKILL.md +146 -0
- package/antigravity-plugin/skills/slm-governance/SKILL.md +221 -0
- package/antigravity-plugin/skills/slm-graph/SKILL.md +327 -0
- package/antigravity-plugin/skills/slm-loop/SKILL.md +153 -0
- package/antigravity-plugin/skills/slm-mesh/SKILL.md +287 -0
- package/antigravity-plugin/skills/slm-profile/SKILL.md +159 -0
- package/antigravity-plugin/skills/slm-recall/SKILL.md +430 -0
- package/antigravity-plugin/skills/slm-remember/SKILL.md +370 -0
- package/antigravity-plugin/skills/slm-scope/SKILL.md +183 -0
- package/antigravity-plugin/skills/slm-session/SKILL.md +291 -0
- package/antigravity-plugin/skills/slm-status/SKILL.md +185 -0
- package/antigravity-plugin/skills/slm-web-access/SKILL.md +91 -0
- package/codex-plugin/.claude-plugin/plugin.json +19 -0
- package/codex-plugin/.codex/config.toml +40 -0
- package/codex-plugin/.codex-plugin/plugin.json +43 -0
- package/codex-plugin/.mcp.json +16 -0
- package/codex-plugin/AGENTS.md +161 -0
- package/codex-plugin/README.md +205 -0
- package/codex-plugin/_GENERATED.md +52 -0
- package/codex-plugin/agents/slm-governance-advisor.md +78 -0
- package/codex-plugin/agents/slm-loop-runner.md +82 -0
- package/codex-plugin/agents/slm-memory-advisor.md +50 -0
- package/codex-plugin/agents/slm-optimize-advisor.md +44 -0
- package/codex-plugin/commands/slm-loop.md +33 -0
- package/codex-plugin/hooks/hooks.json +59 -0
- package/codex-plugin/scripts/ensure-venv.bat +108 -0
- package/codex-plugin/scripts/ensure-venv.sh +100 -0
- package/codex-plugin/scripts/slm-launch +55 -0
- package/codex-plugin/scripts/slm-launch.bat +30 -0
- package/codex-plugin/skills/slm-bot-memory/SKILL.md +146 -0
- package/codex-plugin/skills/slm-cache/SKILL.md +147 -0
- package/codex-plugin/skills/slm-compress/SKILL.md +150 -0
- package/codex-plugin/skills/slm-getting-started-bot/SKILL.md +146 -0
- package/codex-plugin/skills/slm-governance/SKILL.md +221 -0
- package/codex-plugin/skills/slm-graph/SKILL.md +327 -0
- package/codex-plugin/skills/slm-loop/SKILL.md +153 -0
- package/codex-plugin/skills/slm-mesh/SKILL.md +287 -0
- package/codex-plugin/skills/slm-profile/SKILL.md +159 -0
- package/codex-plugin/skills/slm-recall/SKILL.md +430 -0
- package/codex-plugin/skills/slm-remember/SKILL.md +370 -0
- package/codex-plugin/skills/slm-scope/SKILL.md +183 -0
- package/codex-plugin/skills/slm-session/SKILL.md +291 -0
- package/codex-plugin/skills/slm-status/SKILL.md +185 -0
- package/codex-plugin/skills/slm-web-access/SKILL.md +91 -0
- package/copilot-plugin/.github/agents/slm-governance-advisor.agent.md +80 -0
- package/copilot-plugin/.github/agents/slm-loop-runner.agent.md +84 -0
- package/copilot-plugin/.github/agents/slm-memory-advisor.agent.md +52 -0
- package/copilot-plugin/.github/agents/slm-optimize-advisor.agent.md +46 -0
- package/copilot-plugin/.github/copilot-instructions.md +52 -0
- package/copilot-plugin/.github/hooks/slm-hooks.json +44 -0
- package/copilot-plugin/.github/prompts/slm-bot-memory.prompt.md +147 -0
- package/copilot-plugin/.github/prompts/slm-cache.prompt.md +151 -0
- package/copilot-plugin/.github/prompts/slm-compress.prompt.md +154 -0
- package/copilot-plugin/.github/prompts/slm-getting-started-bot.prompt.md +147 -0
- package/copilot-plugin/.github/prompts/slm-governance.prompt.md +224 -0
- package/copilot-plugin/.github/prompts/slm-graph.prompt.md +322 -0
- package/copilot-plugin/.github/prompts/slm-loop.prompt.md +159 -0
- package/copilot-plugin/.github/prompts/slm-mesh.prompt.md +290 -0
- package/copilot-plugin/.github/prompts/slm-profile.prompt.md +155 -0
- package/copilot-plugin/.github/prompts/slm-recall.prompt.md +434 -0
- package/copilot-plugin/.github/prompts/slm-remember.prompt.md +375 -0
- package/copilot-plugin/.github/prompts/slm-scope.prompt.md +183 -0
- package/copilot-plugin/.github/prompts/slm-session.prompt.md +291 -0
- package/copilot-plugin/.github/prompts/slm-status.prompt.md +190 -0
- package/copilot-plugin/.github/prompts/slm-web-access.prompt.md +95 -0
- package/copilot-plugin/.vscode/mcp.json +11 -0
- package/copilot-plugin/README.md +26 -0
- package/copilot-plugin/_GENERATED.md +49 -0
- package/copilot-plugin/scripts/ensure-venv.bat +122 -0
- package/copilot-plugin/scripts/ensure-venv.sh +169 -0
- package/copilot-plugin/scripts/slm-launch +207 -0
- package/copilot-plugin/scripts/slm-launch.bat +32 -0
- package/copilot-plugin/scripts/slm-resolve.sh +128 -0
- package/copilot-plugin/scripts/slm-run +76 -0
- package/copilot-plugin/scripts/torch-cpu-resolve.sh +86 -0
- package/docs/pi-dev-integration.md +0 -2
- package/hermes-plugin/README.md +30 -0
- package/hermes-plugin/_GENERATED.md +3 -0
- package/hermes-plugin/__init__.py +417 -0
- package/hermes-plugin/agents/slm-governance-advisor.md +78 -0
- package/hermes-plugin/agents/slm-loop-runner.md +82 -0
- package/hermes-plugin/agents/slm-memory-advisor.md +50 -0
- package/hermes-plugin/agents/slm-optimize-advisor.md +44 -0
- package/hermes-plugin/command-inventory.json +7 -0
- package/hermes-plugin/plugin.yaml +24 -0
- package/hermes-plugin/remote.py +390 -0
- package/hermes-plugin/skills/slm-bot-memory/SKILL.md +146 -0
- package/hermes-plugin/skills/slm-cache/SKILL.md +147 -0
- package/hermes-plugin/skills/slm-compress/SKILL.md +150 -0
- package/hermes-plugin/skills/slm-getting-started-bot/SKILL.md +146 -0
- package/hermes-plugin/skills/slm-governance/SKILL.md +221 -0
- package/hermes-plugin/skills/slm-graph/SKILL.md +327 -0
- package/hermes-plugin/skills/slm-loop/SKILL.md +153 -0
- package/hermes-plugin/skills/slm-mesh/SKILL.md +287 -0
- package/hermes-plugin/skills/slm-profile/SKILL.md +159 -0
- package/hermes-plugin/skills/slm-recall/SKILL.md +430 -0
- package/hermes-plugin/skills/slm-remember/SKILL.md +370 -0
- package/hermes-plugin/skills/slm-scope/SKILL.md +183 -0
- package/hermes-plugin/skills/slm-session/SKILL.md +291 -0
- package/hermes-plugin/skills/slm-status/SKILL.md +185 -0
- package/hermes-plugin/skills/slm-web-access/SKILL.md +91 -0
- package/package.json +9 -16
- package/plugin/.claude-plugin/plugin.json +2 -2
- package/plugin/.cursor-plugin/plugin.json +24 -0
- package/plugin/CLAUDE.md +14 -12
- package/plugin/agents/slm-governance-advisor.md +14 -16
- package/plugin/agents/slm-loop-runner.md +22 -11
- package/plugin/agents/slm-memory-advisor.md +14 -14
- package/plugin/agents/slm-optimize-advisor.md +5 -5
- package/plugin/assets/logo.svg +36 -0
- package/plugin/commands/slm-loop.md +5 -3
- package/plugin/cursor-skills/slm-bot-memory/SKILL.md +146 -0
- package/plugin/cursor-skills/slm-getting-started-bot/SKILL.md +146 -0
- package/plugin/cursor-skills/slm-recall/SKILL.md +430 -0
- package/plugin/cursor-skills/slm-remember/SKILL.md +370 -0
- package/plugin/cursor-skills/slm-scope/SKILL.md +183 -0
- package/plugin/cursor-skills/slm-session/SKILL.md +291 -0
- package/plugin/hooks/hooks.json +9 -9
- package/plugin/mcp.cursor.json +23 -0
- package/plugin/requirements-cpu-torch.txt +13 -0
- package/plugin/requirements.txt +1 -1
- package/plugin/scripts/ensure-venv.sh +45 -3
- package/plugin/scripts/slm-launch +93 -9
- package/plugin/scripts/torch-cpu-resolve.sh +86 -0
- package/plugin/skills/slm-bot-memory/SKILL.md +146 -0
- package/plugin/skills/slm-cache/SKILL.md +6 -7
- package/plugin/skills/slm-compress/SKILL.md +3 -3
- package/plugin/skills/slm-getting-started-bot/SKILL.md +146 -0
- package/plugin/skills/slm-governance/SKILL.md +126 -153
- package/plugin/skills/slm-graph/SKILL.md +27 -15
- package/plugin/skills/slm-loop/SKILL.md +74 -20
- package/plugin/skills/slm-mesh/SKILL.md +17 -12
- package/plugin/skills/slm-profile/SKILL.md +101 -91
- package/plugin/skills/slm-recall/SKILL.md +151 -54
- package/plugin/skills/slm-remember/SKILL.md +132 -35
- package/plugin/skills/slm-scope/SKILL.md +24 -17
- package/plugin/skills/slm-session/SKILL.md +77 -57
- package/plugin/skills/slm-status/SKILL.md +54 -35
- package/plugin/skills/slm-web-access/SKILL.md +91 -0
- package/plugin-src/agents/slm-governance-advisor.md +78 -0
- package/plugin-src/agents/slm-loop-runner.md +82 -0
- package/plugin-src/agents/slm-memory-advisor.md +14 -14
- package/plugin-src/agents/slm-optimize-advisor.md +5 -5
- package/plugin-src/commands/slm-loop.md +33 -0
- package/plugin-src/rules/AGENTS.md +45 -36
- package/plugin-src/rules/CLAUDE.md.fragment +47 -0
- package/plugin-src/skills/slm-bot-memory/SKILL.md +146 -0
- package/plugin-src/skills/slm-cache/SKILL.md +6 -7
- package/plugin-src/skills/slm-compress/SKILL.md +3 -3
- package/plugin-src/skills/slm-getting-started-bot/SKILL.md +146 -0
- package/plugin-src/skills/slm-governance/SKILL.md +126 -153
- package/plugin-src/skills/slm-graph/SKILL.md +27 -15
- package/plugin-src/skills/slm-loop/SKILL.md +74 -20
- package/plugin-src/skills/slm-mesh/SKILL.md +17 -12
- package/plugin-src/skills/slm-profile/SKILL.md +101 -91
- package/plugin-src/skills/slm-recall/SKILL.md +151 -54
- package/plugin-src/skills/slm-remember/SKILL.md +132 -35
- package/plugin-src/skills/slm-scope/SKILL.md +24 -17
- package/plugin-src/skills/slm-session/SKILL.md +77 -57
- package/plugin-src/skills/slm-status/SKILL.md +54 -35
- package/plugin-src/skills/slm-web-access/SKILL.md +91 -0
- package/scripts/postinstall.js +107 -0
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
"""Remote mode for the Hermes SLM plugin: /slm commands over Hermes's own MCP client.
|
|
2
|
+
|
|
3
|
+
Selected only by ``plugins.entries.superlocalmemory.settings.connection: remote``.
|
|
4
|
+
This module opens no sockets and runs no programs: every call goes through
|
|
5
|
+
``ctx.call_mcp("superlocalmemory", ...)``, i.e. the MCP server the user configured
|
|
6
|
+
in Hermes (URL, TLS verification and key all live in Hermes's config). It imports
|
|
7
|
+
no SuperLocalMemory package.
|
|
8
|
+
|
|
9
|
+
A save that the SLM server did not confirm is reported as ``NOT SAVED`` - never as
|
|
10
|
+
success. Automatic captures pause while the server is unreachable and are counted,
|
|
11
|
+
so ``/slm status`` shows what was skipped. Nothing is kept on this computer.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import argparse
|
|
17
|
+
import hashlib
|
|
18
|
+
import json
|
|
19
|
+
import logging
|
|
20
|
+
import threading
|
|
21
|
+
import time
|
|
22
|
+
from dataclasses import dataclass
|
|
23
|
+
from datetime import datetime, timezone
|
|
24
|
+
from typing import Any, Callable
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger("superlocalmemory.hermes.remote")
|
|
27
|
+
|
|
28
|
+
SERVER = "superlocalmemory"
|
|
29
|
+
CIRCUIT_SECONDS = 30.0
|
|
30
|
+
VERSION_CACHE_SECONDS = 300.0
|
|
31
|
+
MAX_CONTENT = 50_000
|
|
32
|
+
READ_ONLY_TAG = "[remote_key_read_only]"
|
|
33
|
+
#: Lifecycle tools that write. A read-only key is refused these; once that is
|
|
34
|
+
#: seen the plugin stops sending them (and counts them as skipped).
|
|
35
|
+
LIFECYCLE_WRITES = frozenset({"session_init", "log_tool_event", "observe",
|
|
36
|
+
"settle_session_outcomes", "close_session"})
|
|
37
|
+
#: Skills that work against a remote SLM (the rest manage the SLM computer).
|
|
38
|
+
REMOTE_SKILLS = ("slm-cache", "slm-compress", "slm-recall", "slm-remember", "slm-scope",
|
|
39
|
+
"slm-session", "slm-status")
|
|
40
|
+
HOST_ONLY_ROLES = frozenset({"governance", "loop"})
|
|
41
|
+
HOST_ONLY_MESSAGE = ("'/slm {cmd}' manages the SLM computer and is not available over a "
|
|
42
|
+
"remote connection. Run 'slm {cmd}' on the SLM computer.")
|
|
43
|
+
_TIMEOUTS = {"recall": 8, "remember": 15, "delete_memory": 15, "update_memory": 15}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _bounded(value: Any, limit: int = 300) -> str:
|
|
47
|
+
text = "" if value is None else str(value)
|
|
48
|
+
return text[:limit] + ("… [truncated]" if len(text) > limit else "")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass(frozen=True)
|
|
52
|
+
class Envelope:
|
|
53
|
+
ok: bool
|
|
54
|
+
payload: Any = None
|
|
55
|
+
reason: str = ""
|
|
56
|
+
truncated: bool = False
|
|
57
|
+
|
|
58
|
+
@classmethod
|
|
59
|
+
def fail(cls, code: str, reason: str) -> "Envelope":
|
|
60
|
+
return cls(False, None, f"{reason} ({code})" if code else reason)
|
|
61
|
+
|
|
62
|
+
@classmethod
|
|
63
|
+
def from_hermes(cls, raw: Any) -> "Envelope":
|
|
64
|
+
if not isinstance(raw, dict):
|
|
65
|
+
return cls.fail("bad_envelope", "Hermes returned an unexpected result")
|
|
66
|
+
if raw.get("ok") is not True:
|
|
67
|
+
return cls(False, None, _bounded(raw.get("error") or "the SLM server did not answer"))
|
|
68
|
+
payload = raw.get("structuredContent")
|
|
69
|
+
if payload is None:
|
|
70
|
+
result = raw.get("result")
|
|
71
|
+
if isinstance(result, str):
|
|
72
|
+
try:
|
|
73
|
+
payload = json.loads(result)
|
|
74
|
+
except ValueError:
|
|
75
|
+
payload = result
|
|
76
|
+
else:
|
|
77
|
+
payload = result
|
|
78
|
+
return cls(True, payload, "", raw.get("truncated") is True)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class RemoteHealth:
|
|
82
|
+
"""Outage circuit and counters for automatic captures. Thread-safe."""
|
|
83
|
+
|
|
84
|
+
def __init__(self, clock: Callable[[], float] = time.monotonic) -> None:
|
|
85
|
+
self._clock = clock
|
|
86
|
+
self._lock = threading.Lock()
|
|
87
|
+
self.failures_total = 0
|
|
88
|
+
self.dropped_while_open = 0
|
|
89
|
+
self.dropped_read_only = 0
|
|
90
|
+
self.last_error = ""
|
|
91
|
+
self.last_ok_at: str | None = None
|
|
92
|
+
self.read_only_key = False
|
|
93
|
+
self._open_until = 0.0
|
|
94
|
+
self._down = False
|
|
95
|
+
self._version: tuple[float, str | None] | None = None
|
|
96
|
+
|
|
97
|
+
def record(self, ok: bool, reason: str = "") -> None:
|
|
98
|
+
with self._lock:
|
|
99
|
+
if ok:
|
|
100
|
+
if self._down:
|
|
101
|
+
logger.info("SLM remote reachable again")
|
|
102
|
+
self._down = False
|
|
103
|
+
self._open_until = 0.0
|
|
104
|
+
self.last_ok_at = datetime.now(timezone.utc).replace(microsecond=0).isoformat()
|
|
105
|
+
return
|
|
106
|
+
self.failures_total += 1
|
|
107
|
+
self.last_error = _bounded(reason, 300)
|
|
108
|
+
self._open_until = self._clock() + CIRCUIT_SECONDS
|
|
109
|
+
if not self._down:
|
|
110
|
+
self._down = True
|
|
111
|
+
logger.warning("SLM remote unreachable: %s; automatic capture paused for %d s; "
|
|
112
|
+
"explicit /slm remember still tries every time",
|
|
113
|
+
self.last_error, int(CIRCUIT_SECONDS))
|
|
114
|
+
|
|
115
|
+
def mark_read_only(self) -> None:
|
|
116
|
+
with self._lock:
|
|
117
|
+
if not self.read_only_key:
|
|
118
|
+
logger.warning("SLM remote key is read-only: automatic capture is off; "
|
|
119
|
+
"recall still works")
|
|
120
|
+
self.read_only_key = True
|
|
121
|
+
|
|
122
|
+
def lifecycle_allowed(self, tool: str) -> bool:
|
|
123
|
+
with self._lock:
|
|
124
|
+
if self.read_only_key and tool in LIFECYCLE_WRITES:
|
|
125
|
+
self.dropped_read_only += 1
|
|
126
|
+
return False
|
|
127
|
+
if self._clock() < self._open_until:
|
|
128
|
+
self.dropped_while_open += 1
|
|
129
|
+
return False
|
|
130
|
+
return True
|
|
131
|
+
|
|
132
|
+
def status_line(self) -> str:
|
|
133
|
+
with self._lock:
|
|
134
|
+
parts = [
|
|
135
|
+
"connection: remote",
|
|
136
|
+
f"failures since start: {self.failures_total}",
|
|
137
|
+
f"lifecycle captures skipped while unreachable: {self.dropped_while_open}",
|
|
138
|
+
]
|
|
139
|
+
if self.read_only_key:
|
|
140
|
+
parts.append(f"read-only key, captures not sent: {self.dropped_read_only}")
|
|
141
|
+
parts.append(f"last error: {self.last_error or 'none'}")
|
|
142
|
+
parts.append(f"last success: {self.last_ok_at or 'never'}")
|
|
143
|
+
return " · ".join(parts)
|
|
144
|
+
|
|
145
|
+
def cached_version(self) -> str | None | bool:
|
|
146
|
+
"""The cached server version, or ``False`` when the cache is stale."""
|
|
147
|
+
with self._lock:
|
|
148
|
+
if self._version is None or self._clock() - self._version[0] > VERSION_CACHE_SECONDS:
|
|
149
|
+
return False
|
|
150
|
+
return self._version[1]
|
|
151
|
+
|
|
152
|
+
def remember_version(self, version: str | None) -> None:
|
|
153
|
+
with self._lock:
|
|
154
|
+
self._version = (self._clock(), version)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def is_read_only_refusal(text: Any) -> bool:
|
|
158
|
+
return READ_ONLY_TAG in str(text or "")
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def call(ctx: Any, tool: str, args: dict[str, Any], timeout: float) -> Envelope:
|
|
162
|
+
try:
|
|
163
|
+
raw = ctx.call_mcp(SERVER, tool, args, timeout=timeout)
|
|
164
|
+
except PermissionError:
|
|
165
|
+
return Envelope.fail("grant_missing", "Hermes has not granted the superlocalmemory MCP "
|
|
166
|
+
"server to this plugin (plugins.entries.superlocalmemory."
|
|
167
|
+
"mcp_allowlist)")
|
|
168
|
+
except Exception as exc: # noqa: BLE001 — a transport failure is a reason, not a crash
|
|
169
|
+
return Envelope.fail("transport", _bounded(exc))
|
|
170
|
+
return Envelope.from_hermes(raw)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
# -- argument builders -------------------------------------------------------------------
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
class _Usage(Exception):
|
|
177
|
+
pass
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
class _Parser(argparse.ArgumentParser):
|
|
181
|
+
def error(self, message: str) -> None: # never exit the Hermes process
|
|
182
|
+
raise _Usage(message)
|
|
183
|
+
|
|
184
|
+
def exit(self, status: int = 0, message: str | None = None) -> None:
|
|
185
|
+
raise _Usage(message or "")
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _parser(prog: str) -> _Parser:
|
|
189
|
+
return _Parser(prog=f"/slm {prog}", add_help=False, exit_on_error=False)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _int_range(low: int, high: int):
|
|
193
|
+
def convert(text: str) -> int:
|
|
194
|
+
value = int(text)
|
|
195
|
+
if not low <= value <= high:
|
|
196
|
+
raise ValueError
|
|
197
|
+
return value
|
|
198
|
+
convert.__name__ = f"integer {low}-{high}"
|
|
199
|
+
return convert
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _no_args(argv: list[str], _sid: str) -> dict | str:
|
|
203
|
+
return {} if not argv else "This command takes no arguments over a remote connection."
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _recall_args(argv: list[str], _sid: str) -> dict | str:
|
|
207
|
+
p = _parser("recall")
|
|
208
|
+
p.add_argument("query", nargs="+")
|
|
209
|
+
p.add_argument("--limit", type=_int_range(1, 50), default=10)
|
|
210
|
+
p.add_argument("--kind", default="")
|
|
211
|
+
ns = p.parse_args(argv)
|
|
212
|
+
args = {"query": " ".join(ns.query), "limit": ns.limit, "agent_id": "hermes"}
|
|
213
|
+
return {**args, "kind": ns.kind} if ns.kind else args
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _remember_args(argv: list[str], session_id: str) -> dict | str:
|
|
217
|
+
p = _parser("remember")
|
|
218
|
+
p.add_argument("content", nargs="+")
|
|
219
|
+
p.add_argument("--tags", default="")
|
|
220
|
+
p.add_argument("--kind", default="")
|
|
221
|
+
p.add_argument("--replaces", default=None)
|
|
222
|
+
ns = p.parse_args(argv)
|
|
223
|
+
content = " ".join(ns.content)
|
|
224
|
+
if len(content) > MAX_CONTENT:
|
|
225
|
+
return f"NOT SAVED: the memory is longer than {MAX_CONTENT} characters."
|
|
226
|
+
args: dict[str, Any] = {"content": content, "tags": ns.tags, "agent_id": "hermes"}
|
|
227
|
+
if ns.kind:
|
|
228
|
+
args["kind"] = ns.kind
|
|
229
|
+
if ns.replaces:
|
|
230
|
+
args["replaces"] = ns.replaces
|
|
231
|
+
seed = json.dumps({"session": session_id, **args}, sort_keys=True)
|
|
232
|
+
args["idempotency_key"] = hashlib.sha256(
|
|
233
|
+
("hermes-remote\0" + seed).encode("utf-8")).hexdigest()[:32]
|
|
234
|
+
return args
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _list_args(argv: list[str], _sid: str) -> dict | str:
|
|
238
|
+
p = _parser("list")
|
|
239
|
+
p.add_argument("-n", "--limit", type=_int_range(1, 200), default=20)
|
|
240
|
+
p.add_argument("--kind", default="")
|
|
241
|
+
ns = p.parse_args(argv)
|
|
242
|
+
return {"limit": ns.limit, **({"kind": ns.kind} if ns.kind else {})}
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _fact_id_args(argv: list[str], _sid: str) -> dict | str:
|
|
246
|
+
p = _parser("delete")
|
|
247
|
+
p.add_argument("fact_id")
|
|
248
|
+
return {"fact_id": p.parse_args(argv).fact_id, "agent_id": "hermes"}
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _update_args(argv: list[str], _sid: str) -> dict | str:
|
|
252
|
+
p = _parser("update")
|
|
253
|
+
p.add_argument("fact_id")
|
|
254
|
+
p.add_argument("content", nargs="+")
|
|
255
|
+
ns = p.parse_args(argv)
|
|
256
|
+
return {"fact_id": ns.fact_id, "content": " ".join(ns.content), "agent_id": "hermes"}
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def _summary_args(argv: list[str], _sid: str) -> dict | str:
|
|
260
|
+
p = _parser("summary")
|
|
261
|
+
p.add_argument("kind", nargs="?", default="day")
|
|
262
|
+
p.add_argument("target", nargs="?", default="")
|
|
263
|
+
ns = p.parse_args(argv)
|
|
264
|
+
return {"kind": ns.kind, "target": ns.target}
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _trace_args(argv: list[str], _sid: str) -> dict | str:
|
|
268
|
+
p = _parser("trace")
|
|
269
|
+
p.add_argument("query", nargs="+")
|
|
270
|
+
p.add_argument("--limit", type=_int_range(1, 50), default=10)
|
|
271
|
+
ns = p.parse_args(argv)
|
|
272
|
+
return {"query": " ".join(ns.query), "limit": ns.limit}
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
# -- renderers ---------------------------------------------------------------------------
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _render_json(env: Envelope) -> str:
|
|
279
|
+
if not env.ok:
|
|
280
|
+
return f"SLM server error: {env.reason}"
|
|
281
|
+
text = env.payload if isinstance(env.payload, str) else json.dumps(env.payload, indent=2,
|
|
282
|
+
sort_keys=True)
|
|
283
|
+
return _bounded(text, 8_000)
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def _render_remember(env: Envelope) -> str:
|
|
287
|
+
if not env.ok:
|
|
288
|
+
return f"NOT SAVED: {env.reason}"
|
|
289
|
+
payload = env.payload
|
|
290
|
+
if not isinstance(payload, dict) or payload.get("success") is not True:
|
|
291
|
+
detail = (payload or {}) if isinstance(payload, dict) else {}
|
|
292
|
+
reason = detail.get("error") or detail.get("code") or \
|
|
293
|
+
"the SLM server did not confirm the write"
|
|
294
|
+
return f"NOT SAVED: {_bounded(reason)}"
|
|
295
|
+
count = len(payload.get("fact_ids") or [])
|
|
296
|
+
if payload.get("pending"):
|
|
297
|
+
return f"Saved ({count} fact(s); queryable now, enrichment still running)."
|
|
298
|
+
return f"Saved ({count} fact(s))."
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def _render_mutation(env: Envelope) -> str:
|
|
302
|
+
if not env.ok:
|
|
303
|
+
return f"NOT CHANGED: {env.reason}"
|
|
304
|
+
payload = env.payload
|
|
305
|
+
if not isinstance(payload, dict) or payload.get("success") is not True:
|
|
306
|
+
detail = payload if isinstance(payload, dict) else {}
|
|
307
|
+
return f"NOT CHANGED: {_bounded(detail.get('error') or 'the SLM server did not confirm it')}"
|
|
308
|
+
return "Done."
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def remote_help() -> str:
|
|
312
|
+
names = ", ".join(sorted(REMOTE_COMMANDS))
|
|
313
|
+
return ("Remote connection: these /slm commands run on the SLM server: " + names +
|
|
314
|
+
". Commands that manage the SLM computer run there with 'slm <command>'.")
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
@dataclass(frozen=True)
|
|
318
|
+
class RemoteSpec:
|
|
319
|
+
tool: str | None
|
|
320
|
+
build: Callable[[list[str], str], dict | str]
|
|
321
|
+
render: Callable[[Envelope], str]
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
REMOTE_COMMANDS: dict[str, RemoteSpec] = {
|
|
325
|
+
"status": RemoteSpec("get_status", _no_args, _render_json),
|
|
326
|
+
"health": RemoteSpec("health", _no_args, _render_json),
|
|
327
|
+
"recall": RemoteSpec("recall", _recall_args, _render_json),
|
|
328
|
+
"remember": RemoteSpec("remember", _remember_args, _render_remember),
|
|
329
|
+
"list": RemoteSpec("list_recent", _list_args, _render_json),
|
|
330
|
+
"delete": RemoteSpec("delete_memory", _fact_id_args, _render_mutation),
|
|
331
|
+
"update": RemoteSpec("update_memory", _update_args, _render_mutation),
|
|
332
|
+
"summary": RemoteSpec("get_memory_summary", _summary_args, _render_json),
|
|
333
|
+
"trace": RemoteSpec("recall_trace", _trace_args, _render_json),
|
|
334
|
+
"kinds": RemoteSpec("memory_kinds_status", _no_args, _render_json),
|
|
335
|
+
"help": RemoteSpec(None, _no_args, lambda _env: remote_help()),
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def version_problem(ctx: Any, health: RemoteHealth, release: tuple[int, int, int]) -> str | None:
|
|
340
|
+
"""The plugin runs only with the SLM release it ships with, remote or local."""
|
|
341
|
+
wanted = ".".join(str(p) for p in release)
|
|
342
|
+
cached = health.cached_version()
|
|
343
|
+
if cached is False:
|
|
344
|
+
env = call(ctx, "get_status", {}, timeout=8)
|
|
345
|
+
if not env.ok:
|
|
346
|
+
health.record(False, env.reason)
|
|
347
|
+
return f"SLM server unavailable: {env.reason}"
|
|
348
|
+
health.record(True)
|
|
349
|
+
version = env.payload.get("version") if isinstance(env.payload, dict) else None
|
|
350
|
+
health.remember_version(str(version) if version else None)
|
|
351
|
+
cached = health.cached_version()
|
|
352
|
+
if cached != wanted:
|
|
353
|
+
return (f"Hermes SLM plugin requires exactly SLM {wanted} on the SLM server; the server "
|
|
354
|
+
f"reports {cached or 'no version'}. Upgrade one side so they match.")
|
|
355
|
+
return None
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def run_remote(ctx: Any, argv: list[str], session_id: str, health: RemoteHealth,
|
|
359
|
+
release: tuple[int, int, int]) -> str:
|
|
360
|
+
command = argv[0]
|
|
361
|
+
spec = REMOTE_COMMANDS.get(command)
|
|
362
|
+
if spec is None:
|
|
363
|
+
return HOST_ONLY_MESSAGE.format(cmd=command)
|
|
364
|
+
if spec.tool is None:
|
|
365
|
+
return remote_help()
|
|
366
|
+
try:
|
|
367
|
+
args = spec.build(argv[1:], session_id)
|
|
368
|
+
except (_Usage, argparse.ArgumentError) as exc:
|
|
369
|
+
return f"Usage error for /slm {command}: {exc}"
|
|
370
|
+
if isinstance(args, str):
|
|
371
|
+
return args
|
|
372
|
+
problem = version_problem(ctx, health, release)
|
|
373
|
+
if problem:
|
|
374
|
+
return f"NOT SAVED: {problem}" if command == "remember" else problem
|
|
375
|
+
env = call(ctx, spec.tool, args, timeout=_TIMEOUTS.get(spec.tool, 8))
|
|
376
|
+
if env.ok:
|
|
377
|
+
health.record(True)
|
|
378
|
+
elif is_read_only_refusal(env.reason):
|
|
379
|
+
health.mark_read_only()
|
|
380
|
+
else:
|
|
381
|
+
health.record(False, env.reason)
|
|
382
|
+
rendered = spec.render(env)
|
|
383
|
+
if command == "status":
|
|
384
|
+
rendered = f"{rendered}\n{health.status_line()}"
|
|
385
|
+
return rendered
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
__all__ = ["Envelope", "HOST_ONLY_MESSAGE", "HOST_ONLY_ROLES", "LIFECYCLE_WRITES",
|
|
389
|
+
"REMOTE_COMMANDS", "REMOTE_SKILLS", "RemoteHealth", "call", "is_read_only_refusal",
|
|
390
|
+
"remote_help", "run_remote", "version_problem"]
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: slm-bot-memory
|
|
3
|
+
description: Cross-bot memory on a shared computer (Grok Bot, Cursor plugin, any host where several agents share one machine). Explains agent_id attribution vs. profile/scope access, how to namespace by agent_id + profile + scope, and what must never be written to memory. Read this before the first remember/recall on a new Grok-Bot-style host.
|
|
4
|
+
when_to_use: |
|
|
5
|
+
- First session on a Grok Bot computer or any shared-bot host
|
|
6
|
+
- "Will my other bots see this memory?"
|
|
7
|
+
- "How do I keep this bot's notes separate from that one?"
|
|
8
|
+
- Deciding whether to store something that looks like a secret or personal data
|
|
9
|
+
- Setting up a second bot on a computer that already has SLM configured
|
|
10
|
+
allowed-tools: remember, recall, search, session_init, switch_profile
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# slm-bot-memory — Cross-Bot Memory on a Shared Host
|
|
14
|
+
|
|
15
|
+
SuperLocalMemory ships as an MCP server. On a single-user laptop, one bot is
|
|
16
|
+
the only thing talking to it. On a **shared-bot host** — Grok Bot's "one
|
|
17
|
+
computer, shared by all your Bots" model, several Cursor windows, or any
|
|
18
|
+
setup where more than one agent process points at the same install — several
|
|
19
|
+
agents can reach the same memory store at once. This skill is about that
|
|
20
|
+
case: what is actually isolated, what is not, and what must never be written
|
|
21
|
+
regardless.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Three things that sound like isolation and are not the same
|
|
26
|
+
|
|
27
|
+
| Setting | What it controls | Does it hide memories from another bot? |
|
|
28
|
+
|---|---|---|
|
|
29
|
+
| `SLM_AGENT_ID` | **Attribution** — who gets credit for a memory in logs and summaries | **No.** It is metadata, never an authenticated principal (verified in `mcp/agent_context.py`'s own docstring). Any bot that can query the same store can read a memory written under a different `agent_id`. |
|
|
30
|
+
| `SLM_DATA_DIR` (profile root) | **Which database file** the daemon opens | **Yes.** Two bots pointed at different `SLM_DATA_DIR` values have fully separate, independent memory stores — nothing is shared unless you explicitly configure sharing. |
|
|
31
|
+
| `scope` (`personal` / `shared` / `global`, see `slm-scope`) | **Visibility inside one store**, between profiles that share that store | Only the way you set it. `personal` (the default) stays within the writing profile; `shared`/`global` are explicit, user-initiated opt-ins. |
|
|
32
|
+
|
|
33
|
+
A memory profile (the namespace `profile_id` and `switch_profile` select) is not
|
|
34
|
+
a fourth kind of isolation between bots. Any local caller that can name a profile
|
|
35
|
+
can read it, and `switch_profile` moves the active profile for every bot on the
|
|
36
|
+
computer. Treat profiles as organisation, not privacy.
|
|
37
|
+
|
|
38
|
+
The practical rule: **`SLM_AGENT_ID` tells you who wrote something; `SLM_DATA_DIR`
|
|
39
|
+
and `scope` decide who can read it.** If two bots on the same Grok Bot computer
|
|
40
|
+
use the same `SLM_DATA_DIR` (the common case — it is a shared filesystem, and
|
|
41
|
+
nothing in Grok Bot's plugin model points different bots at different data
|
|
42
|
+
directories by default), they see each other's `personal`-scope memories too,
|
|
43
|
+
the same way two terminal sessions on one laptop would. Giving two bots
|
|
44
|
+
different `SLM_AGENT_ID` values does not change that — it only changes whose
|
|
45
|
+
name shows up on the memory.
|
|
46
|
+
|
|
47
|
+
If you need one bot's memories genuinely unreachable from another, that bot
|
|
48
|
+
needs its **own `SLM_DATA_DIR`** (or a store where company mode with roles is
|
|
49
|
+
configured — see `slm-governance`), not just its own `SLM_AGENT_ID` or its own
|
|
50
|
+
profile.
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## Namespacing that actually works
|
|
55
|
+
|
|
56
|
+
Use all three dimensions together, each for what it is good at:
|
|
57
|
+
|
|
58
|
+
1. **`agent_id`** (`SLM_AGENT_ID` env, or the `agent_id` parameter on
|
|
59
|
+
`remember`/`recall` directly) — so a human or another agent reading
|
|
60
|
+
`get_memory_summary` or the stored facts later can tell which bot said
|
|
61
|
+
what. Set it to something stable and specific: `grok_bot_support`,
|
|
62
|
+
`grok_bot_release_notes`, not a generic `bot`.
|
|
63
|
+
2. **`SLM_DATA_DIR`** — the real isolation boundary. One store per bot that
|
|
64
|
+
genuinely needs privacy from the others. A `profile_id` inside a shared store
|
|
65
|
+
groups memories but does not hide them.
|
|
66
|
+
3. **`scope`** (see `slm-scope`) — inside a store two or more bots
|
|
67
|
+
legitimately share, keep writes `personal` by default and only promote to
|
|
68
|
+
`shared`/`global` when a fact is meant for every bot on that store.
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
remember(
|
|
72
|
+
content="Deploy window for the support bot is Tue/Thu 14:00-16:00 UTC",
|
|
73
|
+
tags="ops,schedule",
|
|
74
|
+
agent_id="grok_bot_support",
|
|
75
|
+
session_id="<sid>",
|
|
76
|
+
)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
recall(
|
|
81
|
+
query="deploy window",
|
|
82
|
+
agent_id="grok_bot_support",
|
|
83
|
+
session_id="<sid>",
|
|
84
|
+
)
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Recall is not filtered by `agent_id` automatically — it is attribution on the
|
|
88
|
+
written record. If you need "only what this bot wrote", pass
|
|
89
|
+
`saved_by="grok_bot_support"` to `recall` (or `--saved-by` on `slm recall`),
|
|
90
|
+
which keeps only memories saved by that agent id. It filters; it does not
|
|
91
|
+
protect, because another bot can leave it out.
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## What never goes into memory, on any host
|
|
96
|
+
|
|
97
|
+
This is not Grok-Bot-specific — it is the baseline for every SLM host — but
|
|
98
|
+
it matters more here because a shared computer means a shared blast radius if
|
|
99
|
+
it is violated:
|
|
100
|
+
|
|
101
|
+
- **Secrets, API keys, tokens, passwords, certificates.** Never pass them to
|
|
102
|
+
`remember`, even "just this once" or "temporarily." If a fact needs to
|
|
103
|
+
reference that a credential exists, store the *fact of its existence and
|
|
104
|
+
where to rotate it*, never the value.
|
|
105
|
+
- **Personal data about identifiable people** that was not already something
|
|
106
|
+
the user explicitly asked to be remembered about themselves (names,
|
|
107
|
+
emails, addresses, health, financial details of third parties).
|
|
108
|
+
- **Anything from Grok Bot's shared filesystem you did not write yourself**
|
|
109
|
+
and were not asked to remember. "Files are visible to every Bot" on that
|
|
110
|
+
computer (Grok Bot's own docs) — a file being readable is not the same as
|
|
111
|
+
having permission to copy its contents into long-term memory that other
|
|
112
|
+
bots and later sessions will see.
|
|
113
|
+
- **Command-line credentials.** Grok Bot's own docs note these are shared
|
|
114
|
+
across bots on the computer; SLM must not become a second, more durable
|
|
115
|
+
copy of them.
|
|
116
|
+
|
|
117
|
+
If you are unsure whether something is a secret, treat it as one and ask the
|
|
118
|
+
user before storing anything derived from it.
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## Checklist for a new bot on an existing shared host
|
|
123
|
+
|
|
124
|
+
1. Confirm `SLM_DATA_DIR` — is this bot meant to share the existing store, or
|
|
125
|
+
does it need its own? (`slm status --json` on the computer shows `data.base_dir`
|
|
126
|
+
and `data.db_path`.)
|
|
127
|
+
2. Set a specific `SLM_AGENT_ID` for this bot before the first `remember`.
|
|
128
|
+
3. Read `slm-scope` before writing anything with `scope="shared"` or
|
|
129
|
+
`scope="global"` — those are machine-wide, not just cross-bot.
|
|
130
|
+
4. Read the "never goes into memory" list above once, out loud if you are
|
|
131
|
+
unsure, before the first write on this host.
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## Related skills
|
|
136
|
+
|
|
137
|
+
- `slm-getting-started-bot` — first-session setup for a headless bot host
|
|
138
|
+
- `slm-scope` — the personal/shared/global visibility model this skill
|
|
139
|
+
namespaces on top of
|
|
140
|
+
- `slm-profile` — memory profiles versus tool sets, and what switching does
|
|
141
|
+
- `slm-governance` — role-based access when a store has multiple human/bot
|
|
142
|
+
members and real access control, not just convention
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
*SuperLocalMemory v4.1.23 · Qualixar · AGPL-3.0-or-later*
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: slm-cache
|
|
3
|
+
description: KV cache for repeated reads — call slm_cache_get(key) first; on a miss do the expensive operation then slm_cache_set(key, value, ttl_seconds) to store it; on a hit use the returned value directly; always fail-open (hit:false on any error, never raises); saves tokens when the same file, query result, or tool output is read more than once in a session.
|
|
4
|
+
when_to_use: "cache file, avoid re-reading, repeated read, cache result, cache tool output, save re-read, reuse across session, cache check, cache hit, cache miss"
|
|
5
|
+
allowed-tools: slm_cache_set, slm_cache_get, Bash
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# slm-cache — KV Cache for Repeated Reads (Surface B)
|
|
9
|
+
|
|
10
|
+
## Purpose
|
|
11
|
+
|
|
12
|
+
When the same file, query result, or expensive tool output is needed more than once in a session, fetching it again wastes tokens and time. `slm_cache_set` stores a result under a stable key; `slm_cache_get` retrieves it on subsequent calls. The cache is agent-scoped (automatically namespaced by tenant/agent ID), TTL-bounded, and fail-open.
|
|
13
|
+
|
|
14
|
+
This is an agent-routed cache — it caches results the agent explicitly routes through SLM. It cannot cache Claude conversation turns.
|
|
15
|
+
|
|
16
|
+
## Tool: slm_cache_set
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
slm_cache_set(
|
|
20
|
+
key: str, # required — cache key (max 512 chars)
|
|
21
|
+
value: str, # required — value to store (max 1 MB)
|
|
22
|
+
ttl_seconds: int = 86400, # time-to-live in seconds (default 24 h)
|
|
23
|
+
) -> dict
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### Return dict
|
|
27
|
+
|
|
28
|
+
| Key | Type | Meaning |
|
|
29
|
+
|-----|------|---------|
|
|
30
|
+
| `ok` | bool | `True` on success; `False` on validation error or internal error |
|
|
31
|
+
| `stored` | bool | `True` when the value was written to the cache |
|
|
32
|
+
| `note` | str \| None | Error detail or `None` on success |
|
|
33
|
+
|
|
34
|
+
Keys are SHA-256-hashed internally per agent so they do not collide across agents. The raw key string you supply is the only handle you need. The namespace is the calling agent (`SLM_AGENT_ID`, or the `/mcp/<agent>` path), not the memory profile, so switching profiles does not give you a fresh cache. A caller on another computer gets its own namespace, keyed by its access key, and can never see a local agent's entries.
|
|
35
|
+
|
|
36
|
+
## Tool: slm_cache_get
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
slm_cache_get(
|
|
40
|
+
key: str, # required — same key used in slm_cache_set
|
|
41
|
+
) -> dict
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Return dict
|
|
45
|
+
|
|
46
|
+
| Key | Type | Meaning |
|
|
47
|
+
|-----|------|---------|
|
|
48
|
+
| `ok` | bool | `True` on clean execution (including miss); `False` on internal error |
|
|
49
|
+
| `hit` | bool | `True` when the key exists and has not expired |
|
|
50
|
+
| `value` | str \| None | The stored value on a hit; `None` on miss |
|
|
51
|
+
| `note` | str \| None | Error detail or `None` |
|
|
52
|
+
|
|
53
|
+
`value` is the string you stored. A miss returns `{"ok": true, "hit": false, "value": null, "note": null}`. `ok: false` means something went wrong internally but the miss behaviour is the same — treat both as a cache miss and proceed with the real fetch.
|
|
54
|
+
|
|
55
|
+
## Standard Pattern: Cache-Aside
|
|
56
|
+
|
|
57
|
+
Always check the cache first, then fill on miss:
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
# 1. Check cache
|
|
61
|
+
cached = await slm_cache_get(key="file:/absolute/path/to/config.json")
|
|
62
|
+
|
|
63
|
+
if cached["hit"]:
|
|
64
|
+
content = cached["value"]
|
|
65
|
+
else:
|
|
66
|
+
# 2. Expensive operation (file read, search, API call)
|
|
67
|
+
content = read_file("/absolute/path/to/config.json")
|
|
68
|
+
|
|
69
|
+
# 3. Store for the rest of the session
|
|
70
|
+
await slm_cache_set(
|
|
71
|
+
key="file:/absolute/path/to/config.json",
|
|
72
|
+
value=content,
|
|
73
|
+
ttl_seconds=3600, # 1 h — adjust to data volatility
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# 4. Use content
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Key Naming Convention
|
|
80
|
+
|
|
81
|
+
Use a stable, human-readable prefix so keys are recognisable in stats and won't collide accidentally:
|
|
82
|
+
|
|
83
|
+
| Content type | Suggested prefix | Example |
|
|
84
|
+
|---|---|---|
|
|
85
|
+
| File read | `file:` | `file:/repo/src/config.py` |
|
|
86
|
+
| Search result | `search:` | `search:recall:session_init_context` |
|
|
87
|
+
| Tool output | `tool:` | `tool:build_code_graph:/repo` |
|
|
88
|
+
| External fetch | `url:` | `url:https://api.example.com/v1/data` |
|
|
89
|
+
|
|
90
|
+
Key length cap: 512 characters. Keys longer than that are rejected (`ok: false`).
|
|
91
|
+
|
|
92
|
+
## When Caching Pays Off
|
|
93
|
+
|
|
94
|
+
**Cache when:**
|
|
95
|
+
- You will read the same file more than once in a session.
|
|
96
|
+
- A search or recall result is reused across multiple reasoning steps.
|
|
97
|
+
- An expensive MCP tool call (graph build, semantic search) produces output that is stable for the session duration.
|
|
98
|
+
|
|
99
|
+
**Do NOT cache:**
|
|
100
|
+
- Volatile data (live API responses that change minute-to-minute, current timestamps, streaming output).
|
|
101
|
+
- Secrets, credentials, tokens, or `ccr_id` values (CCR already handles its own storage).
|
|
102
|
+
- Data that must be fresh for correctness — a stale cache is worse than a cache miss.
|
|
103
|
+
- Intermediate scratchpad text you will discard.
|
|
104
|
+
|
|
105
|
+
## Fail-Open Guarantee
|
|
106
|
+
|
|
107
|
+
Neither tool raises an exception. On any internal error:
|
|
108
|
+
|
|
109
|
+
- `slm_cache_get` returns `{"ok": false, "hit": false, "value": null, ...}` — treat as a miss and proceed with the real fetch.
|
|
110
|
+
- `slm_cache_set` returns `{"ok": false, "stored": false, ...}` — log the note if useful, but continue; the value is still available in memory this step.
|
|
111
|
+
|
|
112
|
+
Never block a task on a cache failure.
|
|
113
|
+
|
|
114
|
+
## TTL Guidance
|
|
115
|
+
|
|
116
|
+
| Data type | Suggested TTL |
|
|
117
|
+
|---|---|
|
|
118
|
+
| Static config / generated file | 86400 s (24 h — the default) |
|
|
119
|
+
| Session-specific tool output | 3600 s (1 h) |
|
|
120
|
+
| Rapidly changing API data | Do not cache, or 60–300 s |
|
|
121
|
+
|
|
122
|
+
Set `ttl_seconds` to match how long the data remains valid. After expiry `slm_cache_get` returns a miss automatically.
|
|
123
|
+
|
|
124
|
+
## Secondary CLI (fallback when MCP is unavailable)
|
|
125
|
+
|
|
126
|
+
Prefer the MCP tools above. If you must use the command line:
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
slm cache status [--json] [--tenant default]
|
|
130
|
+
slm cache clear [--json] [--tenant default]
|
|
131
|
+
slm cache invalidate --tag <tag> [--json] [--tenant default]
|
|
132
|
+
slm cache ttl --set <seconds> [--semantic <seconds>] [--json] [--tenant default]
|
|
133
|
+
slm cache semantic on|off [--json] [--tenant default]
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
These subcommands control daemon-level cache settings and bulk removal. They do not read or write individual entries — use the MCP tools for that. Entries written by `slm_cache_set` carry the tag `mcp-kv`, so `slm cache invalidate --tag mcp-kv` removes them, and `slm cache clear` deletes every entry for the tenant.
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
## Related skills
|
|
141
|
+
|
|
142
|
+
- `slm-compress` — for large content reduction; cache and compress work together
|
|
143
|
+
- `slm-status` — view `cache_kv_hits`/`cache_kv_misses` counters (daemon-persisted totals) from `slm_optimize_stats`
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
SuperLocalMemory v4.1.23 · Qualixar · AGPL-3.0-or-later
|