superlocalmemory 4.1.2 → 4.1.4
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 +19 -1
- package/CHANGELOG.md +65 -0
- package/README.md +3 -3
- package/ide/configs/codex-mcp.toml +3 -1
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.mcp.json +1 -3
- 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/scripts/slm-launch +100 -31
- package/plugin/skills/slm-cache/SKILL.md +1 -1
- package/plugin/skills/slm-compress/SKILL.md +1 -1
- package/plugin/skills/slm-governance/SKILL.md +1 -1
- package/plugin/skills/slm-graph/SKILL.md +1 -1
- package/plugin/skills/slm-loop/SKILL.md +1 -1
- package/plugin/skills/slm-mesh/SKILL.md +1 -1
- package/plugin/skills/slm-profile/SKILL.md +1 -1
- package/plugin/skills/slm-recall/SKILL.md +1 -1
- package/plugin/skills/slm-remember/SKILL.md +1 -1
- package/plugin/skills/slm-scope/SKILL.md +1 -1
- package/plugin/skills/slm-session/SKILL.md +1 -1
- package/plugin/skills/slm-status/SKILL.md +1 -1
- package/plugin-src/rules/AGENTS.md +1 -1
- package/plugin-src/skills/slm-cache/SKILL.md +1 -1
- package/plugin-src/skills/slm-compress/SKILL.md +1 -1
- package/plugin-src/skills/slm-governance/SKILL.md +1 -1
- package/plugin-src/skills/slm-graph/SKILL.md +1 -1
- package/plugin-src/skills/slm-loop/SKILL.md +1 -1
- package/plugin-src/skills/slm-mesh/SKILL.md +1 -1
- package/plugin-src/skills/slm-profile/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-scope/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 +8 -2
- package/src/superlocalmemory/cli/daemon.py +69 -6
|
@@ -21,7 +21,25 @@
|
|
|
21
21
|
"license": "AGPL-3.0-or-later",
|
|
22
22
|
"name": "superlocalmemory",
|
|
23
23
|
"source": "./plugin",
|
|
24
|
-
"version": "4.1.
|
|
24
|
+
"version": "4.1.4"
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"author": {
|
|
28
|
+
"name": "Qualixar"
|
|
29
|
+
},
|
|
30
|
+
"description": "Local-first agent memory with auditable hybrid retrieval — the Codex build, with Codex-shaped rules, hooks and launcher.",
|
|
31
|
+
"homepage": "https://github.com/qualixar/superlocalmemory",
|
|
32
|
+
"keywords": [
|
|
33
|
+
"memory",
|
|
34
|
+
"mcp",
|
|
35
|
+
"agents",
|
|
36
|
+
"local-first",
|
|
37
|
+
"context-compression"
|
|
38
|
+
],
|
|
39
|
+
"license": "AGPL-3.0-or-later",
|
|
40
|
+
"name": "superlocalmemory-codex",
|
|
41
|
+
"source": "./codex-plugin",
|
|
42
|
+
"version": "4.1.4"
|
|
25
43
|
}
|
|
26
44
|
]
|
|
27
45
|
}
|
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,71 @@ All notable changes to SuperLocalMemory 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
|
+
## [4.1.4] — A busy daemon is not a dead daemon
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- **`slm serve stop` and `slm restart` could report a live daemon as "not
|
|
12
|
+
running."** `daemon_request()` preflights every call with `GET /health`,
|
|
13
|
+
which needs the daemon's single-threaded event loop to be free to answer.
|
|
14
|
+
`/maintenance/run` and `/consolidate/cognitive` run multi-second (sometimes
|
|
15
|
+
multi-minute) work directly inline in their handlers with no thread
|
|
16
|
+
offload — unlike `/recall`, fixed for exactly this in v3.4.52 — so every
|
|
17
|
+
request blocks for as long as they run, health included. Reproduced live: a
|
|
18
|
+
genuine `/maintenance/run` call held the loop long enough that 15 of 15
|
|
19
|
+
health polls in that window timed out at the 2s cap while the process never
|
|
20
|
+
stopped listening. `stop_daemon()` now checks process liveness directly
|
|
21
|
+
(PID plus a clock-independent start token, no HTTP) before deciding whether
|
|
22
|
+
a stop is owed, and sends the stop itself without the health preflight —
|
|
23
|
+
the daemon still authenticates the request by its capability header on
|
|
24
|
+
arrival, so no ownership guarantee is lost, only the stall-prone round
|
|
25
|
+
trip. `cmd_restart`'s Step 1 uses the same liveness check, so a busy-but-alive
|
|
26
|
+
daemon is no longer skipped as "already stopped," which previously cascaded
|
|
27
|
+
into Step 3 refusing to start a second daemon and failing the whole restart.
|
|
28
|
+
The underlying blocking handlers are unchanged by this fix — recall and
|
|
29
|
+
remember calls can still be delayed by a running maintenance pass, just not
|
|
30
|
+
misreported as a dead daemon.
|
|
31
|
+
|
|
32
|
+
### Added
|
|
33
|
+
- **A one-command Antigravity plugin install.** `agy`'s plugin installer only
|
|
34
|
+
accepts a local directory — there is no marketplace or remote-install
|
|
35
|
+
path — so Claude Code and Codex had a real one-liner and Antigravity did
|
|
36
|
+
not. `scripts/install-antigravity-plugin.sh` closes the gap: a shallow,
|
|
37
|
+
sparse checkout of `antigravity-plugin/` into a local cache, then
|
|
38
|
+
`agy plugin install` against it. Idempotent; safe to re-run to update.
|
|
39
|
+
|
|
40
|
+
## [4.1.3] — Your install, your tools, your memories
|
|
41
|
+
|
|
42
|
+
### Fixed
|
|
43
|
+
- **Installing the plugin could point your editor at a different, empty set of
|
|
44
|
+
memories.** Its server configuration pinned a data directory of its own, so on
|
|
45
|
+
a machine already using SLM the editor read somewhere else entirely — measured
|
|
46
|
+
here, that directory held 28 KB while the real one held 611 MB and 5,370
|
|
47
|
+
memories. Nothing failed and nothing warned; it simply looked like everything
|
|
48
|
+
was gone. The plugin no longer says where your memories live.
|
|
49
|
+
- **Installing the plugin could take away tools you had turned on.** Three of the
|
|
50
|
+
four editor builds forced a narrower tool set — 31 tools, without the eight for
|
|
51
|
+
coordinating between sessions. If you had deliberately enabled more, installing
|
|
52
|
+
something removed them. No build states an opinion about this any more.
|
|
53
|
+
- **Installing the plugin could give you a second copy of SuperLocalMemory.** Two
|
|
54
|
+
launchers preferred a private environment of their own over the one you already
|
|
55
|
+
had from `pip`, `pipx` or `npm`. So a machine that had ever created one kept
|
|
56
|
+
using it — including after you upgraded properly — and two copies read the same
|
|
57
|
+
memories, with whichever started first deciding which one answered. Both now
|
|
58
|
+
use what you already have, in either install order, and fall back to their own
|
|
59
|
+
only on a machine with none. When a leftover copy is found beside a real
|
|
60
|
+
install, it is named, with both version numbers, instead of being used quietly.
|
|
61
|
+
- **The plugin would not start at all outside Claude Code.** The path it built
|
|
62
|
+
came from a setting only Claude Code provides, so on Codex and Antigravity it
|
|
63
|
+
pointed nowhere and the server never came up — with no error explaining why.
|
|
64
|
+
- **Codex reported itself as Claude Code.** The catalogue offered one build, the
|
|
65
|
+
Claude Code one, so Codex installed that and every memory written from Codex was
|
|
66
|
+
filed under the wrong tool. Codex now has its own entry and its own build,
|
|
67
|
+
with the Codex-shaped rules, hooks and launcher that were never being delivered.
|
|
68
|
+
|
|
69
|
+
### Changed
|
|
70
|
+
- **Antigravity is supported.** It had no plugin at all. It now has the same
|
|
71
|
+
twelve skills, four sub-agents, command and hooks as every other editor.
|
|
72
|
+
|
|
8
73
|
## [4.1.2] — The monitor that was watching nothing
|
|
9
74
|
|
|
10
75
|
### Fixed
|
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
</picture>
|
|
6
6
|
</p>
|
|
7
7
|
|
|
8
|
-
<h1 align="center">SuperLocalMemory V4.1.
|
|
8
|
+
<h1 align="center">SuperLocalMemory V4.1.4</h1>
|
|
9
9
|
|
|
10
10
|
<h2 align="center">Rent the LLM. Own the memory.</h2>
|
|
11
11
|
|
|
@@ -27,12 +27,12 @@ guarantee here is stated as a falsifiable invariant, tested under an adversarial
|
|
|
27
27
|
negative control, and shipped with the harness that regenerates the evidence:
|
|
28
28
|
<code>python benchmark/run_all.py --trials 200 --output-dir results/</code>. What each experiment
|
|
29
29
|
does <em>not</em> exercise is stated too.</p>
|
|
30
|
-
<p align="center"><code>v4.1.
|
|
30
|
+
<p align="center"><code>v4.1.4</code> — one control plane: <strong>SLM-Mesh</strong> peer coordination · multi-scope memory (personal / shared / global) · profiles · Cache · Compress · 7-layer retrieval · code graph · Entity Explorer · skill evolution · Modes A/B/C · GDPR retention & audit chain · bounded loops — across CLI, MCP, dashboard, the <strong>Claude plugin</strong>, the <strong>Codex add-on</strong>, and documented IDE integrations.<br/>
|
|
31
31
|
Proxy: <code>slm wrap claude</code> · MCP: add <code>slm_compress</code> to your config · Skill: zero-config</p>
|
|
32
32
|
<p align="center"><strong>Four public arXiv preprints</strong> · V4: <a href="https://arxiv.org/abs/2608.08253">arXiv:2608.08253</a> · companion archive: <a href="https://zenodo.org/records/21853302">Zenodo 21853302</a> (<a href="https://doi.org/10.5281/zenodo.21853302">DOI 10.5281/zenodo.21853302</a>) · prior preprints: <a href="https://arxiv.org/abs/2603.02240">2603.02240</a> · <a href="https://arxiv.org/abs/2603.14588">2603.14588</a> · <a href="https://arxiv.org/abs/2604.04514">2604.04514</a>.</p>
|
|
33
33
|
|
|
34
34
|
<p align="center">
|
|
35
|
-
<a href="CHANGELOG.md"><img src="https://img.shields.io/badge/v4.1.
|
|
35
|
+
<a href="CHANGELOG.md"><img src="https://img.shields.io/badge/v4.1.4-Current_Release-2ea44f?style=for-the-badge&logo=checkmarx&logoColor=white" alt="v4.1.4 — Current Release"/></a>
|
|
36
36
|
<a href="https://arxiv.org/abs/2608.08253"><img src="https://img.shields.io/badge/arXiv-2608.08253-b31b1b?style=for-the-badge&logo=arxiv&logoColor=white" alt="SuperLocalMemory 4.0 paper on arXiv:2608.08253"/></a>
|
|
37
37
|
<a href="https://zenodo.org/records/21853302"><img src="https://img.shields.io/badge/Zenodo-10.5281%2Fzenodo.21853302-1682D4?style=for-the-badge&logo=zenodo&logoColor=white" alt="V4 paper on Zenodo: 10.5281/zenodo.21853302"/></a>
|
|
38
38
|
<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>
|
|
@@ -8,4 +8,6 @@
|
|
|
8
8
|
[mcp_servers.superlocalmemory]
|
|
9
9
|
command = "slm"
|
|
10
10
|
args = ["mcp"]
|
|
11
|
-
|
|
11
|
+
# Agent id only. Pinning a profile here would remove tools you had enabled;
|
|
12
|
+
# unset resolves to the full tool set.
|
|
13
|
+
env = { SLM_AGENT_ID = "codex" }
|
package/package.json
CHANGED
package/plugin/.mcp.json
CHANGED
package/plugin/CLAUDE.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!-- BEGIN SuperLocalMemory v4.1.
|
|
1
|
+
<!-- BEGIN SuperLocalMemory v4.1.4 -->
|
|
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 v4.1.
|
|
42
|
+
<!-- END SuperLocalMemory v4.1.4 -->
|
|
43
43
|
|
|
44
|
-
SuperLocalMemory v4.1.
|
|
44
|
+
SuperLocalMemory v4.1.4 · 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 v4.1.
|
|
80
|
+
SuperLocalMemory v4.1.4 · 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 v4.1.
|
|
49
|
+
SuperLocalMemory v4.1.4 · 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 v4.1.
|
|
44
|
+
SuperLocalMemory v4.1.4 · Qualixar · AGPL-3.0-or-later
|
package/plugin/requirements.txt
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
superlocalmemory==4.1.
|
|
1
|
+
superlocalmemory==4.1.4
|
|
@@ -1,62 +1,131 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# slm-launch —
|
|
2
|
+
# slm-launch — SuperLocalMemory MCP launcher (POSIX)
|
|
3
3
|
#
|
|
4
4
|
# Cross-platform counterpart: slm-launch.bat (Windows)
|
|
5
|
-
# Referenced by
|
|
5
|
+
# Referenced by .mcp.json as the MCP server command.
|
|
6
6
|
#
|
|
7
|
-
#
|
|
8
|
-
# before opening the MCP stdio transport. This preserves one writer for all
|
|
9
|
-
# parallel Claude sessions that share CLAUDE_PLUGIN_DATA.
|
|
7
|
+
# WHAT THIS RESOLVES, AND WHY THE DEFAULT CHANGED
|
|
10
8
|
#
|
|
11
|
-
#
|
|
9
|
+
# It used to default to a venv the plugin owns:
|
|
10
|
+
# "${CLAUDE_PLUGIN_DATA}/venv/bin/slm". So anyone who already had SuperLocalMemory
|
|
11
|
+
# — from pip, pipx or npm — got a SECOND installation the moment they installed
|
|
12
|
+
# the plugin, on its own schedule, upgraded separately, and easy to leave behind
|
|
13
|
+
# a release. Installing a plugin should not fork your installation.
|
|
12
14
|
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
15
|
+
# It was also Claude-Code-only. ${CLAUDE_PLUGIN_DATA} is set by Claude Code and
|
|
16
|
+
# by nothing else, so on Codex or Antigravity the path collapsed to
|
|
17
|
+
# "/venv/bin/slm" and the server simply did not start.
|
|
18
|
+
#
|
|
19
|
+
# The default is now: USE THE SLM THAT IS ALREADY INSTALLED. A plugin-owned venv
|
|
20
|
+
# is the fallback for a machine that has none, not the first choice.
|
|
21
|
+
#
|
|
22
|
+
# Environment (all optional):
|
|
23
|
+
# SLM_LAUNCHER auto (default) — prefer an installed slm, else the plugin venv
|
|
24
|
+
# system — require the slm on PATH
|
|
25
|
+
# plugin — require the plugin's own venv
|
|
26
|
+
# <path> — an explicit slm binary (leading ~ expanded)
|
|
27
|
+
# SLM_DATA_DIR SLM data dir (default: ~/.superlocalmemory)
|
|
28
|
+
# CLAUDE_PLUGIN_DATA where a plugin-owned venv lives, when there is one
|
|
29
|
+
#
|
|
30
|
+
# Deliberately NOT set here: the MCP profile. Which tools a host exposes is the
|
|
31
|
+
# user's decision, and a plugin that narrows it takes away tools they configured.
|
|
20
32
|
|
|
21
33
|
_SLM_DATA="${SLM_DATA_DIR:-$HOME/.superlocalmemory}"
|
|
34
|
+
_PLUGIN_BIN="${CLAUDE_PLUGIN_DATA:+${CLAUDE_PLUGIN_DATA}/venv/bin/slm}"
|
|
22
35
|
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
# writer against the daemon that is already up. This skips a redundant, slow
|
|
26
|
-
# venv bootstrap on every session start.
|
|
36
|
+
# Is a daemon already up for this data dir? If so we join it rather than boot a
|
|
37
|
+
# second one — one writer for every session that shares the store.
|
|
27
38
|
_daemon_running=0
|
|
28
39
|
if [ -f "${_SLM_DATA}/daemon.pid" ] \
|
|
29
40
|
&& kill -0 "$(cat "${_SLM_DATA}/daemon.pid" 2>/dev/null)" 2>/dev/null; then
|
|
30
41
|
_daemon_running=1
|
|
31
42
|
fi
|
|
32
43
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
44
|
+
# Expand a leading ~ WITHOUT eval — eval on an environment value is a
|
|
45
|
+
# command-injection foot-gun. Only tilde expansion; pass expanded paths for vars.
|
|
46
|
+
_expand_tilde() {
|
|
47
|
+
case "$1" in
|
|
48
|
+
"~"*) printf '%s' "${HOME}${1#\~}" ;;
|
|
49
|
+
*) printf '%s' "$1" ;;
|
|
50
|
+
esac
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
# Start a daemon with the chosen binary unless one is already serving.
|
|
54
|
+
_ensure_daemon() {
|
|
55
|
+
[ "${_daemon_running}" -eq 1 ] && return 0
|
|
56
|
+
"$1" serve start >&2
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
case "${SLM_LAUNCHER:-auto}" in
|
|
60
|
+
auto)
|
|
61
|
+
# 1. An slm already on PATH — the pip, pipx or npm install the user has.
|
|
62
|
+
# This is the common case and the one that must not be forked.
|
|
63
|
+
if command -v slm >/dev/null 2>&1; then
|
|
64
|
+
# Both orderings end up here, and both must converge on ONE
|
|
65
|
+
# environment:
|
|
66
|
+
# installed first, plugin second -> nothing was ever forked;
|
|
67
|
+
# plugin first, installed second -> the plugin venv is now the
|
|
68
|
+
# older copy and must stop being used.
|
|
69
|
+
# They already share one store, because this launcher sets no
|
|
70
|
+
# SLM_DATA_DIR and both resolve ~/.superlocalmemory. What is left to
|
|
71
|
+
# go wrong is two DIFFERENT VERSIONS serving that one store: whoever
|
|
72
|
+
# starts the daemon first decides which code the other one talks to.
|
|
73
|
+
# So when a leftover plugin venv is found beside a real install, say
|
|
74
|
+
# so, with both versions named. Silent duplication is how a store
|
|
75
|
+
# ends up served by a release nobody thinks they are running.
|
|
76
|
+
if [ -n "${_PLUGIN_BIN}" ] && [ -x "${_PLUGIN_BIN}" ]; then
|
|
77
|
+
_sys_v="$(slm --version 2>/dev/null | awk '{print $NF}')"
|
|
78
|
+
_plg_v="$("${_PLUGIN_BIN}" --version 2>/dev/null | awk '{print $NF}')"
|
|
79
|
+
if [ -n "${_plg_v}" ] && [ "${_sys_v}" != "${_plg_v}" ]; then
|
|
80
|
+
echo "SLM plugin: using the installed slm ${_sys_v:-?} and IGNORING a leftover plugin venv at ${_PLUGIN_BIN} (${_plg_v})." >&2
|
|
81
|
+
echo " One store, one version. Remove the venv to stop it drifting: rm -rf \"${CLAUDE_PLUGIN_DATA}/venv\"" >&2
|
|
82
|
+
fi
|
|
83
|
+
fi
|
|
84
|
+
_ensure_daemon "$(command -v slm)" || \
|
|
85
|
+
echo "SLM plugin: daemon did not start; continuing as a direct MCP writer." >&2
|
|
86
|
+
exec slm mcp
|
|
87
|
+
fi
|
|
88
|
+
# 2. No system install. Use the plugin's own venv if this host gave us
|
|
89
|
+
# somewhere to keep one.
|
|
90
|
+
if [ -n "${_PLUGIN_BIN}" ] && [ -x "${_PLUGIN_BIN}" ]; then
|
|
91
|
+
if ! _ensure_daemon "${_PLUGIN_BIN}"; then
|
|
39
92
|
echo "SLM plugin: unable to start the owned daemon; refusing a direct MCP writer." >&2
|
|
40
93
|
exit 1
|
|
41
94
|
fi
|
|
95
|
+
exec "${_PLUGIN_BIN}" mcp
|
|
42
96
|
fi
|
|
43
|
-
|
|
97
|
+
# 3. Neither. Say which two things were looked for, so the fix is obvious.
|
|
98
|
+
echo "SLM plugin: no SuperLocalMemory found." >&2
|
|
99
|
+
echo " Looked for: 'slm' on PATH, and ${_PLUGIN_BIN:-a plugin venv (this host sets no plugin data dir)}." >&2
|
|
100
|
+
echo " Install it with: pipx install superlocalmemory" >&2
|
|
101
|
+
exit 1
|
|
44
102
|
;;
|
|
45
103
|
system)
|
|
104
|
+
if ! command -v slm >/dev/null 2>&1; then
|
|
105
|
+
echo "SLM_LAUNCHER=system but no 'slm' on PATH." >&2
|
|
106
|
+
exit 1
|
|
107
|
+
fi
|
|
108
|
+
_ensure_daemon "$(command -v slm)" || true
|
|
46
109
|
exec slm mcp
|
|
47
110
|
;;
|
|
111
|
+
plugin)
|
|
112
|
+
if [ -z "${_PLUGIN_BIN}" ]; then
|
|
113
|
+
echo "SLM_LAUNCHER=plugin but this host sets no CLAUDE_PLUGIN_DATA." >&2
|
|
114
|
+
exit 1
|
|
115
|
+
fi
|
|
116
|
+
if ! _ensure_daemon "${_PLUGIN_BIN}"; then
|
|
117
|
+
echo "SLM plugin: unable to start the owned daemon; refusing a direct MCP writer." >&2
|
|
118
|
+
exit 1
|
|
119
|
+
fi
|
|
120
|
+
exec "${_PLUGIN_BIN}" mcp
|
|
121
|
+
;;
|
|
48
122
|
*)
|
|
49
|
-
|
|
50
|
-
# on an environment value is a command-injection foot-gun. Only tilde
|
|
51
|
-
# expansion is supported here; pass an already-expanded path for $VARs.
|
|
52
|
-
case "${SLM_LAUNCHER}" in
|
|
53
|
-
"~"*) _SLM_BIN="${HOME}${SLM_LAUNCHER#\~}" ;;
|
|
54
|
-
*) _SLM_BIN="${SLM_LAUNCHER}" ;;
|
|
55
|
-
esac
|
|
123
|
+
_SLM_BIN="$(_expand_tilde "${SLM_LAUNCHER}")"
|
|
56
124
|
if [ ! -x "${_SLM_BIN}" ]; then
|
|
57
125
|
echo "SLM_LAUNCHER is not an executable slm binary: ${_SLM_BIN}" >&2
|
|
58
126
|
exit 1
|
|
59
127
|
fi
|
|
128
|
+
_ensure_daemon "${_SLM_BIN}" || true
|
|
60
129
|
exec "${_SLM_BIN}" mcp
|
|
61
130
|
;;
|
|
62
131
|
esac
|
|
@@ -137,4 +137,4 @@ When the SLM MCP server is unavailable, use these CLI equivalents:
|
|
|
137
137
|
- **slm-optimize-advisor** — context compression and KV cache
|
|
138
138
|
- **slm-governance-advisor** — scope/role compliance, retention policies, GDPR
|
|
139
139
|
|
|
140
|
-
SuperLocalMemory v4.1.
|
|
140
|
+
SuperLocalMemory v4.1.4 · 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__ = "4.1.
|
|
35
|
+
__version__ = "4.1.4"
|
|
36
36
|
|
|
37
37
|
_REQUIRED_VERSIONS = {
|
|
38
38
|
"sentence_transformers": "5.3.0",
|
|
@@ -754,13 +754,19 @@ def cmd_restart(args: Namespace) -> None:
|
|
|
754
754
|
# Step 1: stop only the descriptor-owned daemon. Its graceful shutdown
|
|
755
755
|
# owns worker termination; process-name-wide scans are forbidden.
|
|
756
756
|
from superlocalmemory.cli.daemon import (
|
|
757
|
-
|
|
757
|
+
owned_daemon_process_alive,
|
|
758
758
|
read_descriptor,
|
|
759
759
|
stop_daemon,
|
|
760
760
|
wait_for_owned_daemon_shutdown,
|
|
761
761
|
)
|
|
762
762
|
|
|
763
|
-
|
|
763
|
+
# v4.1.4: process liveness, not HTTP health-readiness, decides whether a
|
|
764
|
+
# stop is owed. is_daemon_running() additionally requires a live /health
|
|
765
|
+
# response, which a daemon busy inside /maintenance/run or
|
|
766
|
+
# /consolidate/cognitive cannot give for the duration of that call even
|
|
767
|
+
# though it is fully alive and holding the port — see stop_daemon()'s
|
|
768
|
+
# docstring for the reproduced failure this fixes.
|
|
769
|
+
was_running = owned_daemon_process_alive()
|
|
764
770
|
owned_descriptor = read_descriptor() if was_running else None
|
|
765
771
|
stopped = stop_daemon() if was_running else True
|
|
766
772
|
if stopped and was_running:
|
|
@@ -254,6 +254,29 @@ def is_daemon_running() -> bool:
|
|
|
254
254
|
return legacy is not None
|
|
255
255
|
|
|
256
256
|
|
|
257
|
+
def owned_daemon_process_alive() -> bool:
|
|
258
|
+
"""Return whether an owned daemon *process* is alive, HTTP aside.
|
|
259
|
+
|
|
260
|
+
``is_daemon_running()`` additionally requires a live, matching
|
|
261
|
+
``/health`` response, which conflates two different questions: "is there
|
|
262
|
+
a process I need to stop" and "is it ready to serve requests right now."
|
|
263
|
+
A daemon whose event loop is synchronously blocked by a long-running
|
|
264
|
+
handler (``/maintenance/run``, ``/consolidate/cognitive``) cannot answer
|
|
265
|
+
the second for the duration of that call, but the answer to the first is
|
|
266
|
+
still yes. Callers that only need to decide whether a stop is owed
|
|
267
|
+
(``slm restart`` Step 1) should use this instead, so a transiently busy
|
|
268
|
+
daemon is not skipped as "already stopped" while it keeps running and
|
|
269
|
+
holding the port — which then made Step 3 refuse to start a second
|
|
270
|
+
daemon on the still-occupied port and fail the whole restart.
|
|
271
|
+
"""
|
|
272
|
+
descriptor = read_descriptor()
|
|
273
|
+
if descriptor is not None:
|
|
274
|
+
return _descriptor_process_is_alive(descriptor)
|
|
275
|
+
if descriptor_path().exists():
|
|
276
|
+
return False
|
|
277
|
+
return _verified_legacy_health() is not None
|
|
278
|
+
|
|
279
|
+
|
|
257
280
|
def _fetch_health(port: int) -> dict | None:
|
|
258
281
|
"""Fetch loopback health without following cross-namespace discovery."""
|
|
259
282
|
try:
|
|
@@ -349,8 +372,25 @@ def daemon_request(
|
|
|
349
372
|
timeout_seconds: float = 30.0,
|
|
350
373
|
expected_descriptor=_EXPECTED_DESCRIPTOR_UNSET,
|
|
351
374
|
expected_legacy: dict | None = None,
|
|
375
|
+
verify_health: bool = True,
|
|
352
376
|
) -> dict | None:
|
|
353
|
-
"""Send a request only after validating the owned daemon identity.
|
|
377
|
+
"""Send a request only after validating the owned daemon identity.
|
|
378
|
+
|
|
379
|
+
``verify_health`` — when True (the default), a ``GET /health`` preflight
|
|
380
|
+
must succeed and match the descriptor before the real request is sent.
|
|
381
|
+
That preflight needs the daemon's event loop to be free to answer HTTP,
|
|
382
|
+
which is a *readiness* question, not a *liveness* one: a daemon whose
|
|
383
|
+
loop is synchronously blocked by a long-running handler (e.g.
|
|
384
|
+
``/maintenance/run``, ``/consolidate/cognitive``, neither of which is
|
|
385
|
+
offloaded to a thread the way ``/recall`` was for exactly this reason in
|
|
386
|
+
v3.4.52) cannot answer /health for the duration of that call even though
|
|
387
|
+
the process is fully alive and listening. Callers that have already
|
|
388
|
+
proven process-level ownership some other way (e.g. ``stop_daemon()`` via
|
|
389
|
+
``_descriptor_process_is_alive``) should pass ``verify_health=False`` so a
|
|
390
|
+
busy-but-alive daemon does not get misreported as not running. Only
|
|
391
|
+
meaningful for the descriptor path — the legacy bridge has no capability
|
|
392
|
+
header and still needs health to identify its target.
|
|
393
|
+
"""
|
|
354
394
|
legacy = None
|
|
355
395
|
if expected_legacy is not None:
|
|
356
396
|
# Legacy daemons have no capability header. Bind the compatibility
|
|
@@ -377,11 +417,12 @@ def daemon_request(
|
|
|
377
417
|
capability: str | None = None
|
|
378
418
|
target_instance: str | None = None
|
|
379
419
|
if descriptor is not None:
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
420
|
+
if verify_health:
|
|
421
|
+
health = _fetch_health(descriptor.port)
|
|
422
|
+
if health is None or not descriptor_matches_health(descriptor, health):
|
|
423
|
+
return None
|
|
424
|
+
if method.upper() == "GET" and path == "/health":
|
|
425
|
+
return health
|
|
385
426
|
port = descriptor.port
|
|
386
427
|
capability = descriptor.capability
|
|
387
428
|
target_instance = descriptor.instance_id
|
|
@@ -796,16 +837,38 @@ def stop_daemon() -> bool:
|
|
|
796
837
|
HTTP capability; the daemon itself terminates its child process tree.
|
|
797
838
|
Success means the owned process exited and released its listener, not just
|
|
798
839
|
that the asynchronous stop request was accepted.
|
|
840
|
+
|
|
841
|
+
A busy daemon is not a dead daemon. ``daemon_request()`` normally
|
|
842
|
+
preflights every call with ``GET /health`` before sending it, but that
|
|
843
|
+
preflight needs the daemon's single-threaded event loop to be free to
|
|
844
|
+
answer HTTP. ``/maintenance/run`` and ``/consolidate/cognitive`` run
|
|
845
|
+
multi-second (sometimes multi-minute) synchronous work directly inline in
|
|
846
|
+
their handlers with no thread offload, which blocks *every* request on
|
|
847
|
+
that loop, health included, for as long as they run. Reproduced live: a
|
|
848
|
+
genuine ``/maintenance/run`` call held the loop long enough that 15/15
|
|
849
|
+
health polls during the window timed out at exactly the 2s cap while
|
|
850
|
+
``ps``/``lsof`` proved the process never stopped listening — which is
|
|
851
|
+
exactly the "Daemon was not running" false report this fixes. Process
|
|
852
|
+
liveness (PID + clock-independent start token, proven below via
|
|
853
|
+
``_descriptor_process_is_alive``) is the fact that actually matters for
|
|
854
|
+
"should I try to stop this," so it is checked directly and the mutating
|
|
855
|
+
``/stop`` POST is sent with ``verify_health=False`` once that is proven —
|
|
856
|
+
the daemon still authenticates the request by its capability header on
|
|
857
|
+
arrival, so this loses no ownership guarantee, only the redundant,
|
|
858
|
+
stall-prone preflight round trip.
|
|
799
859
|
"""
|
|
800
860
|
descriptor = read_descriptor()
|
|
801
861
|
legacy = _verified_legacy_health() if descriptor is None else None
|
|
802
862
|
if descriptor is None and legacy is None:
|
|
803
863
|
return False
|
|
804
864
|
if descriptor is not None:
|
|
865
|
+
if not _descriptor_process_is_alive(descriptor):
|
|
866
|
+
return False
|
|
805
867
|
response = daemon_request(
|
|
806
868
|
"POST",
|
|
807
869
|
"/stop",
|
|
808
870
|
expected_descriptor=descriptor,
|
|
871
|
+
verify_health=False,
|
|
809
872
|
)
|
|
810
873
|
else:
|
|
811
874
|
if legacy is None:
|