superlocalmemory 3.8.11 → 3.8.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/CHANGELOG.md +85 -0
  2. package/README.md +7 -3
  3. package/package.json +1 -1
  4. package/plugin/.claude-plugin/plugin.json +1 -1
  5. package/plugin/CLAUDE.md +3 -3
  6. package/plugin/agents/slm-governance-advisor.md +1 -1
  7. package/plugin/agents/slm-loop-runner.md +1 -1
  8. package/plugin/agents/slm-memory-advisor.md +1 -1
  9. package/plugin/agents/slm-optimize-advisor.md +1 -1
  10. package/plugin/requirements.txt +1 -1
  11. package/plugin/skills/slm-cache/SKILL.md +1 -1
  12. package/plugin/skills/slm-compress/SKILL.md +1 -1
  13. package/plugin/skills/slm-governance/SKILL.md +1 -1
  14. package/plugin/skills/slm-graph/SKILL.md +1 -1
  15. package/plugin/skills/slm-loop/SKILL.md +1 -1
  16. package/plugin/skills/slm-mesh/SKILL.md +1 -1
  17. package/plugin/skills/slm-profile/SKILL.md +1 -1
  18. package/plugin/skills/slm-recall/SKILL.md +1 -1
  19. package/plugin/skills/slm-remember/SKILL.md +1 -1
  20. package/plugin/skills/slm-scope/SKILL.md +1 -1
  21. package/plugin/skills/slm-session/SKILL.md +1 -1
  22. package/plugin/skills/slm-status/SKILL.md +1 -1
  23. package/plugin-src/rules/AGENTS.md +1 -1
  24. package/plugin-src/skills/slm-cache/SKILL.md +1 -1
  25. package/plugin-src/skills/slm-compress/SKILL.md +1 -1
  26. package/plugin-src/skills/slm-graph/SKILL.md +1 -1
  27. package/plugin-src/skills/slm-recall/SKILL.md +1 -1
  28. package/plugin-src/skills/slm-remember/SKILL.md +1 -1
  29. package/plugin-src/skills/slm-session/SKILL.md +1 -1
  30. package/plugin-src/skills/slm-status/SKILL.md +1 -1
  31. package/pyproject.toml +1 -1
  32. package/src/superlocalmemory/__init__.py +1 -1
  33. package/src/superlocalmemory/cli/commands.py +62 -3
  34. package/src/superlocalmemory/cli/daemon.py +219 -10
  35. package/src/superlocalmemory/cli/setup_wizard.py +45 -1
  36. package/src/superlocalmemory/core/component_registry.py +25 -0
  37. package/src/superlocalmemory/core/config.py +35 -1
  38. package/src/superlocalmemory/core/engine_wiring.py +81 -5
  39. package/src/superlocalmemory/core/recall_pipeline.py +25 -4
  40. package/src/superlocalmemory/core/reranker_worker.py +23 -4
  41. package/src/superlocalmemory/infra/daemon_identity.py +16 -0
  42. package/src/superlocalmemory/infra/process_identity.py +180 -0
  43. package/src/superlocalmemory/infra/version_integrity.py +229 -0
  44. package/src/superlocalmemory/learning/feedback.py +288 -29
  45. package/src/superlocalmemory/learning/legacy_migration.py +45 -4
  46. package/src/superlocalmemory/mcp/_daemon_proxy.py +23 -1
  47. package/src/superlocalmemory/mcp/tools_active.py +109 -58
  48. package/src/superlocalmemory/mcp/tools_core.py +6 -5
  49. package/src/superlocalmemory/retrieval/remote_reranker.py +636 -0
  50. package/src/superlocalmemory/server/unified_daemon.py +27 -0
@@ -0,0 +1,180 @@
1
+ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
2
+ # Licensed under AGPL-3.0-or-later - see LICENSE file
3
+ # Part of SuperLocalMemory V3 | https://qualixar.com | https://varunpratap.com
4
+
5
+ """Clock-independent identity for one running local process.
6
+
7
+ Why this module exists
8
+ ----------------------
9
+ A process's *wall-clock* creation time is not a stable identifier on every
10
+ platform. On Linux -- and therefore inside WSL2 -- psutil derives it as::
11
+
12
+ create_time = /proc/<pid>/stat:starttime / CLOCK_TICKS + /proc/stat:btime
13
+
14
+ ``starttime`` is boot-relative and never changes for the life of the process.
15
+ ``btime`` is the kernel's *estimate* of the boot instant, recomputed from the
16
+ current wall clock, so any clock step moves ``btime`` and moves every process's
17
+ computed ``create_time`` with it -- retroactively. WSL2 periodically
18
+ resynchronises its VM clock against the Windows host, so a ``create_time``
19
+ recorded when the daemon started stops matching the ``create_time`` computed
20
+ for that very same process minutes later. Issue #104 measured a ~35 second
21
+ divergence after roughly four minutes of uptime.
22
+
23
+ Any *constant* tolerance on that comparison is a delay, not a fix: the
24
+ divergence is unbounded and keeps growing. The correct identifier is one the
25
+ wall clock cannot move at all, which is what this module produces.
26
+
27
+ What a start token is
28
+ ---------------------
29
+ ``process_start_token_for(pid)`` returns an opaque string identifying one
30
+ process *instance*, derived without reference to wall-clock time, or ``None``
31
+ when the platform cannot supply one. Tokens are only comparable when they use
32
+ the same scheme, so :func:`compare_start_tokens` is deliberately tri-state --
33
+ callers fall back to a weaker signal instead of guessing.
34
+
35
+ Schemes
36
+ -------
37
+ ``lx1``
38
+ Linux/WSL2: ``lx1:<boot_id>:<starttime_ticks>``. Both halves come straight
39
+ from procfs and are an opaque UUID and an integer tick count rather than
40
+ timestamps, so a clock adjustment cannot rewrite either. ``boot_id``
41
+ differs across reboots, so a post-reboot PID collision can never look like
42
+ a match.
43
+ ``mn1``
44
+ Platforms where psutil exposes a monotonic creation time (macOS, NetBSD,
45
+ and Linux if procfs is unreadable): ``mn1:<value>``. psutil builds its own
46
+ PID-reuse identity from exactly this value for exactly this reason.
47
+
48
+ Windows has no monotonic variant, and needs none: its creation time comes from
49
+ ``GetProcessTimes``, a kernel timestamp that a clock adjustment does not
50
+ rewrite. ``process_start_token_for`` returns ``None`` there and the caller's
51
+ creation-time comparison stays correct.
52
+ """
53
+
54
+ from __future__ import annotations
55
+
56
+ import logging
57
+ import sys
58
+ from pathlib import Path
59
+
60
+ logger = logging.getLogger(__name__)
61
+
62
+ LINUX_SCHEME = "lx1"
63
+ MONOTONIC_SCHEME = "mn1"
64
+
65
+ _PROCFS = Path("/proc")
66
+ # "man proc" numbers /proc/<pid>/stat fields from 1 and starttime is field 22.
67
+ # The comm field can contain spaces and parentheses, so parsing starts after
68
+ # the last ')', which drops fields 1 and 2 -- hence 22 - 3 == 19.
69
+ _STARTTIME_INDEX = 19
70
+
71
+
72
+ def _boot_id() -> str | None:
73
+ """Return this boot's opaque kernel identifier, or None when unavailable.
74
+
75
+ Deliberately strict: without a boot id, two processes from different boots
76
+ could share a PID *and* a tick count, so the token would be unsound. A
77
+ missing boot id therefore means "no token" rather than a weaker token.
78
+ """
79
+ try:
80
+ value = (_PROCFS / "sys" / "kernel" / "random" / "boot_id").read_text(
81
+ encoding="utf-8",
82
+ ).strip()
83
+ except OSError:
84
+ return None
85
+ return value or None
86
+
87
+
88
+ def _linux_start_ticks(pid: int) -> int | None:
89
+ """Return boot-relative start ticks for a PID from procfs, or None."""
90
+ try:
91
+ data = (_PROCFS / str(pid) / "stat").read_bytes()
92
+ except OSError:
93
+ return None
94
+ closing = data.rfind(b")")
95
+ if closing < 0:
96
+ return None
97
+ fields = data[closing + 2:].split()
98
+ if len(fields) <= _STARTTIME_INDEX:
99
+ return None
100
+ try:
101
+ return int(fields[_STARTTIME_INDEX])
102
+ except ValueError:
103
+ return None
104
+
105
+
106
+ def _linux_start_token(pid: int) -> str | None:
107
+ boot_id = _boot_id()
108
+ if boot_id is None:
109
+ return None
110
+ ticks = _linux_start_ticks(pid)
111
+ if ticks is None:
112
+ return None
113
+ return f"{LINUX_SCHEME}:{boot_id}:{ticks}"
114
+
115
+
116
+ def _monotonic_start_token(pid: int) -> str | None:
117
+ """Return psutil's monotonic creation time as a token, or None.
118
+
119
+ ``Process._proc.create_time(monotonic=True)`` is the same private accessor
120
+ psutil uses internally to build ``Process._ident``. It is guarded on every
121
+ axis -- missing psutil, missing attribute, platforms whose implementation
122
+ takes no ``monotonic`` keyword (Windows) -- and degrades to ``None``.
123
+ """
124
+ try:
125
+ import psutil
126
+
127
+ platform_process = getattr(psutil.Process(pid), "_proc", None)
128
+ except Exception: # noqa: BLE001 - identity probing must never raise
129
+ return None
130
+ create_time = getattr(platform_process, "create_time", None)
131
+ if create_time is None:
132
+ return None
133
+ try:
134
+ raw = create_time(monotonic=True)
135
+ except TypeError:
136
+ # No monotonic variant on this platform (Windows).
137
+ return None
138
+ except Exception: # noqa: BLE001
139
+ return None
140
+ try:
141
+ return f"{MONOTONIC_SCHEME}:{float(raw)!r}"
142
+ except (TypeError, ValueError):
143
+ return None
144
+
145
+
146
+ def process_start_token_for(pid: int) -> str | None:
147
+ """Return a clock-independent identity token for ``pid``, or None.
148
+
149
+ ``None`` is a normal answer, not an error: it means "this platform cannot
150
+ prove process identity without the wall clock", and the caller should fall
151
+ back to comparing creation times.
152
+ """
153
+ try:
154
+ pid = int(pid)
155
+ except (TypeError, ValueError):
156
+ return None
157
+ if pid <= 0:
158
+ return None
159
+ if sys.platform.startswith("linux"):
160
+ token = _linux_start_token(pid)
161
+ if token is not None:
162
+ return token
163
+ return _monotonic_start_token(pid)
164
+
165
+
166
+ def compare_start_tokens(recorded: str | None, observed: str | None) -> bool | None:
167
+ """Tri-state comparison of two start tokens.
168
+
169
+ ``True`` -- same scheme, identical value: proven the same process instance.
170
+ ``False`` -- same scheme, different value: proven a different instance.
171
+ ``None`` -- not comparable (either side missing, or different schemes);
172
+ the caller must fall back rather than assume either way.
173
+ """
174
+ if not recorded or not observed:
175
+ return None
176
+ if not isinstance(recorded, str) or not isinstance(observed, str):
177
+ return None
178
+ if recorded.split(":", 1)[0] != observed.split(":", 1)[0]:
179
+ return None
180
+ return recorded == observed
@@ -0,0 +1,229 @@
1
+ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
2
+ # Licensed under AGPL-3.0-or-later - see LICENSE file
3
+ # Part of SuperLocalMemory V3 | https://qualixar.com | https://varunpratap.com
4
+
5
+ """Does this process still match what is installed on disk? (issue #107)
6
+
7
+ Why this module exists
8
+ ----------------------
9
+ Python imports a module once. ``superlocalmemory.__version__`` is therefore
10
+ frozen at the instant a process started, and upgrading the package underneath a
11
+ long-lived ``slm mcp`` server changes nothing for that server -- it keeps
12
+ serving the code it read at startup, forever.
13
+
14
+ Nothing detected that. The stale process did not error; it returned confident,
15
+ plausible, *wrong* answers, and reported a ``serverInfo.version`` matching the
16
+ code it had loaded, which is self-consistent and therefore useless as a
17
+ staleness signal. During the v3.8.12 work this machine had eighteen ``slm mcp``
18
+ processes alive at once, spanning four days and two releases. One of them made
19
+ issue #106 look unfixed across two debugging sessions and contributed to v3.8.11
20
+ shipping a wrong fix, because the "evidence" that the fix had failed was really
21
+ a four-day-old process.
22
+
23
+ The asymmetry that makes this dangerous
24
+ ---------------------------------------
25
+ A *loud* failure costs a user one confused minute. A *silent* one costs
26
+ whoever debugs it their entire session, because it actively argues that correct
27
+ code is broken. Everything here is therefore built so that no failure mode can
28
+ produce a false :data:`STATE_CURRENT`. Unreadable metadata, a hostile reader,
29
+ a non-string return -- all resolve to :data:`STATE_UNKNOWN`, which reports
30
+ "I could not tell" rather than "all is well".
31
+
32
+ Why ``importlib.metadata`` is the right source
33
+ ----------------------------------------------
34
+ It reads the ``*.dist-info`` directory from disk on each call rather than
35
+ returning a value captured at import. Verified empirically: rewriting a
36
+ distribution's metadata underneath a live process and re-reading returns the
37
+ *new* version, with no ``importlib.invalidate_caches()`` needed. That is the
38
+ one property this whole module rests on, so it is pinned by a test.
39
+ """
40
+
41
+ from __future__ import annotations
42
+
43
+ from dataclasses import dataclass
44
+ from typing import Callable, Optional
45
+
46
+ from superlocalmemory import __version__
47
+
48
+ __all__ = (
49
+ "STATE_AHEAD",
50
+ "STATE_CURRENT",
51
+ "STATE_MISMATCH",
52
+ "STATE_STALE",
53
+ "STATE_UNKNOWN",
54
+ "VersionIntegrity",
55
+ "check_version_integrity",
56
+ "installed_distribution_version",
57
+ )
58
+
59
+ #: Imported code matches the installed distribution.
60
+ STATE_CURRENT = "current"
61
+ #: Imported code is *older* than what is installed -- the #107 failure.
62
+ STATE_STALE = "stale"
63
+ #: Imported code is *newer* than the installed distribution. Normal for an
64
+ #: editable checkout; deliberately not reported as a problem, because a warning
65
+ #: that fires on every maintainer's machine is a warning everyone learns to
66
+ #: ignore, and then it will not be read on the day it matters.
67
+ STATE_AHEAD = "ahead"
68
+ #: The two differ but cannot be ordered (local labels, unexpected formats).
69
+ #: Still surfaced -- a difference we cannot rank is not a difference we hide.
70
+ STATE_MISMATCH = "mismatch"
71
+ #: The installed version could not be determined at all.
72
+ STATE_UNKNOWN = "unknown"
73
+
74
+ _DISTRIBUTION_NAME = "superlocalmemory"
75
+
76
+ _RESTART_HINT = (
77
+ "Restart this process to load the installed code "
78
+ "(`slm restart` for the daemon; restart your MCP client for `slm mcp`)."
79
+ )
80
+
81
+
82
+ @dataclass(frozen=True)
83
+ class VersionIntegrity:
84
+ """The outcome of comparing imported code against the installed dist."""
85
+
86
+ running: str
87
+ installed: Optional[str]
88
+ state: str
89
+ detail: str
90
+ hint: str = ""
91
+
92
+ @property
93
+ def is_stale(self) -> bool:
94
+ """True only for the #107 failure: running behind what is installed.
95
+
96
+ Deliberately narrow. Callers gate warnings on this, and widening it to
97
+ mean "anything unusual" would make an editable checkout look broken.
98
+ """
99
+ return self.state == STATE_STALE
100
+
101
+ @property
102
+ def differs(self) -> bool:
103
+ """True whenever imported and installed are known to be different."""
104
+ return self.state in (STATE_STALE, STATE_AHEAD, STATE_MISMATCH)
105
+
106
+ def as_dict(self) -> dict:
107
+ """JSON-safe payload for ``/health``, ``slm status --json``, doctor."""
108
+ return {
109
+ "running": self.running,
110
+ "installed": self.installed,
111
+ "state": self.state,
112
+ "detail": self.detail,
113
+ "hint": self.hint,
114
+ "is_stale": self.is_stale,
115
+ }
116
+
117
+
118
+ def installed_distribution_version() -> str:
119
+ """Return the on-disk version of the installed distribution.
120
+
121
+ Raises whatever ``importlib.metadata`` raises; :func:`check_version_integrity`
122
+ is the layer that turns failure into :data:`STATE_UNKNOWN`. Keeping the
123
+ raise here means a caller that genuinely wants the error can have it.
124
+ """
125
+ from importlib.metadata import version as _version
126
+
127
+ return _version(_DISTRIBUTION_NAME)
128
+
129
+
130
+ def _version_tuple(raw: str) -> Optional[tuple[int, ...]]:
131
+ """Parse a plain dotted release into ints, or ``None`` if it is not one.
132
+
133
+ Intentionally strict and dependency-free: anything carrying a local label,
134
+ pre-release marker, or non-numeric field returns ``None`` and is reported as
135
+ :data:`STATE_MISMATCH`. Guessing an order for such versions could mask a
136
+ real drift behind a confident-looking "current".
137
+ """
138
+ parts = raw.strip().split(".")
139
+ if not parts or any(not p.isdigit() for p in parts):
140
+ return None
141
+ return tuple(int(p) for p in parts)
142
+
143
+
144
+ def check_version_integrity(
145
+ *,
146
+ running: Optional[str] = None,
147
+ installed_reader: Optional[Callable[[], str]] = None,
148
+ ) -> VersionIntegrity:
149
+ """Compare imported code against the installed distribution.
150
+
151
+ Never raises. Both sides are injectable so tests can drive every branch
152
+ without touching the real environment.
153
+
154
+ Args:
155
+ running: Version of the *imported* code. Defaults to
156
+ ``superlocalmemory.__version__``, which is frozen at import.
157
+ installed_reader: Callable returning the on-disk version. Defaults to
158
+ reading the installed distribution metadata.
159
+ """
160
+ running_version = running if running is not None else __version__
161
+
162
+ reader = installed_reader or installed_distribution_version
163
+ installed: Optional[str] = None
164
+ try:
165
+ candidate = reader()
166
+ except BaseException: # noqa: BLE001 - staleness reporting must never raise
167
+ # BaseException, not Exception: this runs on daemon and MCP startup
168
+ # paths, and a diagnostic must never be the reason a process dies.
169
+ candidate = None
170
+
171
+ if isinstance(candidate, str) and candidate.strip():
172
+ installed = candidate.strip()
173
+
174
+ if installed is None:
175
+ return VersionIntegrity(
176
+ running=running_version,
177
+ installed=None,
178
+ state=STATE_UNKNOWN,
179
+ detail=(
180
+ f"running {running_version}; could not read the installed "
181
+ f"distribution version, so staleness is undetermined"
182
+ ),
183
+ )
184
+
185
+ if running_version == installed:
186
+ return VersionIntegrity(
187
+ running=running_version,
188
+ installed=installed,
189
+ state=STATE_CURRENT,
190
+ detail=f"running {running_version}, matching the installed distribution",
191
+ )
192
+
193
+ running_parts = _version_tuple(running_version)
194
+ installed_parts = _version_tuple(installed)
195
+
196
+ if running_parts is None or installed_parts is None:
197
+ return VersionIntegrity(
198
+ running=running_version,
199
+ installed=installed,
200
+ state=STATE_MISMATCH,
201
+ detail=(
202
+ f"running {running_version} but {installed} is installed; "
203
+ f"the two cannot be ordered"
204
+ ),
205
+ hint=_RESTART_HINT,
206
+ )
207
+
208
+ if running_parts < installed_parts:
209
+ return VersionIntegrity(
210
+ running=running_version,
211
+ installed=installed,
212
+ state=STATE_STALE,
213
+ detail=(
214
+ f"running {running_version} but {installed} is installed — this "
215
+ f"process loaded its code before the upgrade and will keep "
216
+ f"serving {running_version} until it restarts"
217
+ ),
218
+ hint=_RESTART_HINT,
219
+ )
220
+
221
+ return VersionIntegrity(
222
+ running=running_version,
223
+ installed=installed,
224
+ state=STATE_AHEAD,
225
+ detail=(
226
+ f"running {running_version}, ahead of the installed {installed} "
227
+ f"(normal for an editable or source checkout)"
228
+ ),
229
+ )