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.
- package/CHANGELOG.md +85 -0
- package/README.md +7 -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 +62 -3
- package/src/superlocalmemory/cli/daemon.py +219 -10
- package/src/superlocalmemory/cli/setup_wizard.py +45 -1
- package/src/superlocalmemory/core/component_registry.py +25 -0
- package/src/superlocalmemory/core/config.py +35 -1
- package/src/superlocalmemory/core/engine_wiring.py +81 -5
- package/src/superlocalmemory/core/recall_pipeline.py +25 -4
- package/src/superlocalmemory/core/reranker_worker.py +23 -4
- package/src/superlocalmemory/infra/daemon_identity.py +16 -0
- package/src/superlocalmemory/infra/process_identity.py +180 -0
- package/src/superlocalmemory/infra/version_integrity.py +229 -0
- package/src/superlocalmemory/learning/feedback.py +288 -29
- package/src/superlocalmemory/learning/legacy_migration.py +45 -4
- package/src/superlocalmemory/mcp/_daemon_proxy.py +23 -1
- package/src/superlocalmemory/mcp/tools_active.py +109 -58
- package/src/superlocalmemory/mcp/tools_core.py +6 -5
- package/src/superlocalmemory/retrieval/remote_reranker.py +636 -0
- 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
|
+
)
|