superlocalmemory 3.8.12 → 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.
- package/CHANGELOG.md +30 -0
- package/README.md +3 -3
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/CLAUDE.md +3 -3
- package/plugin/agents/slm-governance-advisor.md +1 -1
- package/plugin/agents/slm-loop-runner.md +1 -1
- package/plugin/agents/slm-memory-advisor.md +1 -1
- package/plugin/agents/slm-optimize-advisor.md +1 -1
- package/plugin/requirements.txt +1 -1
- package/plugin/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-graph/SKILL.md +1 -1
- 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-session/SKILL.md +1 -1
- package/plugin-src/skills/slm-status/SKILL.md +1 -1
- package/pyproject.toml +1 -1
- package/src/superlocalmemory/__init__.py +1 -1
- package/src/superlocalmemory/cli/commands.py +47 -0
- package/src/superlocalmemory/infra/version_integrity.py +229 -0
- package/src/superlocalmemory/server/unified_daemon.py +27 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,36 @@ All notable changes to SuperLocalMemory V3 will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [3.8.13] - 2026-08-03 — Stale-process detection
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- A running process can now tell when it is serving superseded code. Python
|
|
12
|
+
imports a module once, so `__version__` is frozen at process start and
|
|
13
|
+
upgrading the package underneath a long-lived `slm mcp` server changes
|
|
14
|
+
nothing for that server — it keeps serving the code it read at startup,
|
|
15
|
+
indefinitely. Nothing detected this. The stale process did not error; it
|
|
16
|
+
returned confident, plausible, wrong answers and reported a
|
|
17
|
+
`serverInfo.version` matching the code it had loaded, which is
|
|
18
|
+
self-consistent and therefore useless as a staleness signal. During the
|
|
19
|
+
3.8.12 work one machine had eighteen `slm mcp` processes alive at once
|
|
20
|
+
spanning four days and two releases, and one of them made issue #106 look
|
|
21
|
+
unfixed across two debugging sessions. (#107)
|
|
22
|
+
|
|
23
|
+
The existing process reaper does not cover this: it kills *orphans*, whose
|
|
24
|
+
parent has died. A server whose IDE is still running is never an orphan.
|
|
25
|
+
|
|
26
|
+
Staleness is now reported by `slm doctor` (as a warning, with a restart
|
|
27
|
+
hint), on the loopback `/health` payload as `version_integrity`, and in the
|
|
28
|
+
`slm mcp` startup log. The MCP path logs to stderr only — that transport is
|
|
29
|
+
JSON-RPC over stdio, where a printed warning would corrupt the protocol and
|
|
30
|
+
turn a cosmetic problem into a dead session.
|
|
31
|
+
|
|
32
|
+
Running *ahead* of the installed distribution — normal for an editable
|
|
33
|
+
checkout — is deliberately reported separately and does not warn. A warning
|
|
34
|
+
that fires on every developer machine is one everybody learns to ignore, and
|
|
35
|
+
then it goes unread on the day it matters. Every failure path resolves to
|
|
36
|
+
`unknown` rather than to a false `current`.
|
|
37
|
+
|
|
8
38
|
## [3.8.12] - 2026-08-03 — Canonical learning signals, clock-independent daemon identity, remote reranker
|
|
9
39
|
|
|
10
40
|
### Fixed
|
package/README.md
CHANGED
|
@@ -5,15 +5,15 @@
|
|
|
5
5
|
</picture>
|
|
6
6
|
</p>
|
|
7
7
|
|
|
8
|
-
<h1 align="center">SuperLocalMemory V3.8.
|
|
8
|
+
<h1 align="center">SuperLocalMemory V3.8.13</h1>
|
|
9
9
|
<p align="center"><strong>Enterprise-grade, local-first memory for AI agents and teams.</strong><br/>
|
|
10
10
|
<em>A persistent, auditable long-term brain for your agents that runs on your own infrastructure — with multi-workspace isolation, role-based access, and GDPR + EU AI Act governance controls built in.</em></p>
|
|
11
|
-
<p align="center"><code>v3.8.
|
|
11
|
+
<p align="center"><code>v3.8.13</code> — one control plane: auditable retrieval · multi-scope memory (personal / shared / global) · Cache · Compress · trusted-peer Mesh · bounded loops — across CLI, MCP, dashboard, the <strong>Claude plugin</strong>, the <strong>Codex add-on</strong>, and documented IDE integrations.<br/>
|
|
12
12
|
Proxy: <code>slm wrap claude</code> · MCP: add <code>slm_compress</code> to your config · Skill: zero-config</p>
|
|
13
13
|
<p align="center"><strong>3 public research preprints</strong> (arXiv + Zenodo archives) · <a href="https://arxiv.org/abs/2603.02240">arXiv:2603.02240</a> · <a href="https://arxiv.org/abs/2603.14588">arXiv:2603.14588</a> · <a href="https://arxiv.org/abs/2604.04514">arXiv:2604.04514</a></p>
|
|
14
14
|
|
|
15
15
|
<p align="center">
|
|
16
|
-
<a href="CHANGELOG.md"><img src="https://img.shields.io/badge/v3.8.
|
|
16
|
+
<a href="CHANGELOG.md"><img src="https://img.shields.io/badge/v3.8.13-Current_Release-2ea44f?style=for-the-badge&logo=checkmarx&logoColor=white" alt="v3.8.13 — Current Release"/></a>
|
|
17
17
|
<a href="https://arxiv.org/abs/2603.14588"><img src="https://img.shields.io/badge/arXiv-2603.14588-b31b1b?style=for-the-badge&logo=arxiv&logoColor=white" alt="arXiv Paper"/></a>
|
|
18
18
|
<a href="#three-surfaces-proxy--mcp-tools--skill"><img src="https://img.shields.io/badge/Proxy_|_MCP_|_Skill-22c55e?style=for-the-badge" alt="Three Surfaces: Proxy, MCP Tools, Skill"/></a>
|
|
19
19
|
<a href="https://pypi.org/project/superlocalmemory/"><img src="https://img.shields.io/pypi/v/superlocalmemory?style=for-the-badge&logo=pypi&logoColor=white" alt="PyPI"/></a>
|
package/package.json
CHANGED
package/plugin/CLAUDE.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!-- BEGIN SuperLocalMemory v3.8.
|
|
1
|
+
<!-- BEGIN SuperLocalMemory v3.8.13 -->
|
|
2
2
|
|
|
3
3
|
## SuperLocalMemory (SLM) — Agent Rules
|
|
4
4
|
|
|
@@ -39,6 +39,6 @@ slm-recall · slm-remember · slm-session · slm-status · slm-cache · slm-comp
|
|
|
39
39
|
### Subagents
|
|
40
40
|
slm-memory-advisor (memory decisions, session hygiene, scope/profile guidance) · slm-optimize-advisor (context compression + KV cache) · slm-governance-advisor (scope/roles/compliance/GDPR)
|
|
41
41
|
|
|
42
|
-
<!-- END SuperLocalMemory v3.8.
|
|
42
|
+
<!-- END SuperLocalMemory v3.8.13 -->
|
|
43
43
|
|
|
44
|
-
SuperLocalMemory v3.8.
|
|
44
|
+
SuperLocalMemory v3.8.13 · Qualixar · AGPL-3.0-or-later
|
|
@@ -77,4 +77,4 @@ slm-scope · slm-governance · slm-profile · slm-remember · slm-recall
|
|
|
77
77
|
# What NOT to do
|
|
78
78
|
Never session_init twice; never forget without dry-run preview; never store secrets; never bypass role checks; never claim an erasure succeeded without verifying via recall.
|
|
79
79
|
|
|
80
|
-
SuperLocalMemory v3.8.
|
|
80
|
+
SuperLocalMemory v3.8.13 · Qualixar · AGPL-3.0-or-later
|
|
@@ -46,4 +46,4 @@ slm-recall · slm-remember · slm-session · slm-scope · slm-profile · slm-gov
|
|
|
46
46
|
# What NOT to do
|
|
47
47
|
Never session_init twice; never forget dry_run=False without reporting preview; never dump a whole file into remember; never invent a memory; never claim "saved" without success:true / clean CLI exit; never bypass scope or governance restrictions.
|
|
48
48
|
|
|
49
|
-
SuperLocalMemory v3.8.
|
|
49
|
+
SuperLocalMemory v3.8.13 · Qualixar · AGPL-3.0-or-later
|
|
@@ -41,4 +41,4 @@ slm-compress · slm-cache · slm-status · slm-profile
|
|
|
41
41
|
# What NOT to do
|
|
42
42
|
Never compress code-for-edit/JSON-to-parse/<500 chars; never store secrets/ccr_ids; never let optimize failure block/alter the task; never claim a specific savings %; never carry ccr_ids across profile switches.
|
|
43
43
|
|
|
44
|
-
SuperLocalMemory v3.8.
|
|
44
|
+
SuperLocalMemory v3.8.13 · Qualixar · AGPL-3.0-or-later
|
package/plugin/requirements.txt
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
superlocalmemory==3.8.
|
|
1
|
+
superlocalmemory==3.8.13
|
|
@@ -128,4 +128,4 @@ When the SLM MCP server is unavailable, use these CLI equivalents:
|
|
|
128
128
|
- **slm-optimize-advisor** — context compression and KV cache
|
|
129
129
|
- **slm-governance-advisor** — scope/role compliance, retention policies, GDPR
|
|
130
130
|
|
|
131
|
-
SuperLocalMemory v3.8.
|
|
131
|
+
SuperLocalMemory v3.8.13 · Qualixar · AGPL-3.0-or-later
|
package/pyproject.toml
CHANGED
|
@@ -32,7 +32,7 @@ if "OMP_NUM_THREADS" not in os.environ:
|
|
|
32
32
|
os.environ["OMP_NUM_THREADS"] = "2"
|
|
33
33
|
# ---------------------------------------------------------------------------
|
|
34
34
|
|
|
35
|
-
__version__ = "3.8.
|
|
35
|
+
__version__ = "3.8.13"
|
|
36
36
|
|
|
37
37
|
_REQUIRED_VERSIONS = {
|
|
38
38
|
"sentence_transformers": "5.3.0",
|
|
@@ -2294,6 +2294,28 @@ def cmd_doctor(args: Namespace) -> None:
|
|
|
2294
2294
|
_check("Python", "FAIL", f"{v.major}.{v.minor}.{v.micro} (need >= 3.11)",
|
|
2295
2295
|
"Install Python 3.11+ from https://python.org/downloads/")
|
|
2296
2296
|
|
|
2297
|
+
# 1b. Version integrity (issue #107). Doctor is what a user runs when
|
|
2298
|
+
# something seems wrong, so it is exactly where "the code you are running
|
|
2299
|
+
# is not the code you installed" has to appear. Reported as WARN rather
|
|
2300
|
+
# than FAIL: the installation is sound, it is this *process* that is
|
|
2301
|
+
# behind, and `slm doctor` itself is short-lived so it is rarely the
|
|
2302
|
+
# stale one -- it is reporting on behalf of the long-lived servers.
|
|
2303
|
+
try:
|
|
2304
|
+
from superlocalmemory.infra.version_integrity import check_version_integrity
|
|
2305
|
+
|
|
2306
|
+
_vi = check_version_integrity()
|
|
2307
|
+
if _vi.is_stale:
|
|
2308
|
+
_check("Version integrity", "WARN", _vi.detail, _vi.hint)
|
|
2309
|
+
elif _vi.state == "mismatch":
|
|
2310
|
+
_check("Version integrity", "WARN", _vi.detail, _vi.hint)
|
|
2311
|
+
elif _vi.state == "unknown":
|
|
2312
|
+
_check("Version integrity", "WARN", _vi.detail,
|
|
2313
|
+
"Reinstall so distribution metadata is readable.")
|
|
2314
|
+
else:
|
|
2315
|
+
_check("Version integrity", "PASS", _vi.detail)
|
|
2316
|
+
except Exception as _vi_exc: # noqa: BLE001 - never break doctor
|
|
2317
|
+
_check("Version integrity", "WARN", f"could not verify: {_vi_exc}")
|
|
2318
|
+
|
|
2297
2319
|
# 2. Core deps
|
|
2298
2320
|
core_modules = {
|
|
2299
2321
|
"numpy": "numpy", "scipy": "scipy", "networkx": "networkx",
|
|
@@ -2808,6 +2830,31 @@ def cmd_mcp(_args: Namespace) -> None:
|
|
|
2808
2830
|
except Exception:
|
|
2809
2831
|
pass # Never block MCP startup on cleanup failure
|
|
2810
2832
|
|
|
2833
|
+
# Version integrity (issue #107). The reaper above only kills *orphans* —
|
|
2834
|
+
# servers whose parent died. A server whose IDE is still alive is never an
|
|
2835
|
+
# orphan, so it survives upgrades indefinitely and keeps serving the code
|
|
2836
|
+
# it imported at startup. That is how eighteen `slm mcp` processes spanning
|
|
2837
|
+
# four days and two releases stayed alive on one machine, and how a stale
|
|
2838
|
+
# one made issue #106 look unfixed across two debugging sessions.
|
|
2839
|
+
#
|
|
2840
|
+
# This runs at startup, so a server launched *after* an upgrade is correct
|
|
2841
|
+
# by construction and stays silent. It fires for a server that started
|
|
2842
|
+
# before its own package was upgraded — which is possible when the client
|
|
2843
|
+
# respawns it from an old cached path.
|
|
2844
|
+
#
|
|
2845
|
+
# CRITICAL: logging only, never stdout — MCP speaks JSON-RPC over stdio and
|
|
2846
|
+
# any print corrupts the protocol. The logger writes to stderr.
|
|
2847
|
+
try:
|
|
2848
|
+
from superlocalmemory.infra.version_integrity import check_version_integrity
|
|
2849
|
+
|
|
2850
|
+
_vi = check_version_integrity()
|
|
2851
|
+
if _vi.is_stale:
|
|
2852
|
+
logger.warning("MCP server version drift: %s. %s", _vi.detail, _vi.hint)
|
|
2853
|
+
elif _vi.differs:
|
|
2854
|
+
logger.info("MCP server version note: %s", _vi.detail)
|
|
2855
|
+
except Exception:
|
|
2856
|
+
pass # A diagnostic must never prevent the server from starting.
|
|
2857
|
+
|
|
2811
2858
|
# Auto-install hooks on MCP startup (fast path: ~0.1ms if already current)
|
|
2812
2859
|
# CRITICAL: No stdout — MCP uses stdio transport, any print corrupts protocol
|
|
2813
2860
|
try:
|
|
@@ -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
|
+
)
|
|
@@ -3272,6 +3272,12 @@ def _register_daemon_routes(application: FastAPI) -> None:
|
|
|
3272
3272
|
"runtime_state": runtime_state,
|
|
3273
3273
|
"active_profile": profile_snapshot.profile_id,
|
|
3274
3274
|
"profile_generation": profile_snapshot.generation,
|
|
3275
|
+
# issue #107: does this daemon's *imported* code still match the
|
|
3276
|
+
# installed distribution? ``version`` above reports what this
|
|
3277
|
+
# process loaded, which is self-consistent and therefore cannot
|
|
3278
|
+
# reveal staleness on its own. Loopback-only, alongside the other
|
|
3279
|
+
# operational metadata.
|
|
3280
|
+
"version_integrity": _version_integrity_payload(),
|
|
3275
3281
|
}
|
|
3276
3282
|
|
|
3277
3283
|
@application.get("/recall")
|
|
@@ -4016,6 +4022,27 @@ class _PendingProfileMismatchError(RuntimeError):
|
|
|
4016
4022
|
"""A legacy pending row no longer matches the admitted profile lease."""
|
|
4017
4023
|
|
|
4018
4024
|
|
|
4025
|
+
def _version_integrity_payload() -> dict:
|
|
4026
|
+
"""Report whether this daemon's imported code matches what is installed.
|
|
4027
|
+
|
|
4028
|
+
Issue #107. ``/health``'s ``version`` field reports the version this
|
|
4029
|
+
process loaded at import, so a stale daemon reports its *own* stale version
|
|
4030
|
+
perfectly happily -- self-consistent and useless as a staleness signal.
|
|
4031
|
+
This compares that against the distribution metadata on disk.
|
|
4032
|
+
|
|
4033
|
+
Fail-open by construction: any error degrades to a ``state`` of
|
|
4034
|
+
``"unknown"`` rather than raising, because ``/health`` is what clients poll
|
|
4035
|
+
to decide whether the daemon is usable and must not start returning 500s
|
|
4036
|
+
over a diagnostic.
|
|
4037
|
+
"""
|
|
4038
|
+
try:
|
|
4039
|
+
from superlocalmemory.infra.version_integrity import check_version_integrity
|
|
4040
|
+
|
|
4041
|
+
return check_version_integrity().as_dict()
|
|
4042
|
+
except Exception as exc: # noqa: BLE001 - health must never fail on this
|
|
4043
|
+
return {"state": "unknown", "detail": f"version check failed: {exc}"}
|
|
4044
|
+
|
|
4045
|
+
|
|
4019
4046
|
def _materializer_actor_id() -> str:
|
|
4020
4047
|
"""Return the process-owned actor identity used by background writes."""
|
|
4021
4048
|
descriptor = _ACTIVE_DAEMON_DESCRIPTOR
|