superlocalmemory 3.8.0 → 3.8.1
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 +60 -0
- package/README.md +32 -120
- package/package.json +9 -2
- package/plugin/.claude-plugin/plugin.json +1 -2
- 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 +2 -1
- package/src/superlocalmemory/__init__.py +1 -1
- package/src/superlocalmemory/cli/commands.py +134 -7
- package/src/superlocalmemory/cli/daemon.py +7 -0
- package/src/superlocalmemory/cli/loop_cmd.py +2 -7
- package/src/superlocalmemory/cli/main.py +10 -4
- package/src/superlocalmemory/cli/version_banner.py +17 -3
- package/src/superlocalmemory/core/backend_orchestrator.py +18 -16
- package/src/superlocalmemory/core/embedding_worker.py +4 -5
- package/src/superlocalmemory/core/embeddings.py +119 -45
- package/src/superlocalmemory/core/engine.py +24 -21
- package/src/superlocalmemory/core/engine_ingestion.py +332 -45
- package/src/superlocalmemory/core/ingestion_command.py +134 -25
- package/src/superlocalmemory/core/injection.py +12 -7
- package/src/superlocalmemory/core/maintenance_scheduler.py +17 -7
- package/src/superlocalmemory/core/recall_pipeline.py +5 -3
- package/src/superlocalmemory/core/store_pipeline.py +153 -20
- package/src/superlocalmemory/hooks/hook_handlers.py +6 -1
- package/src/superlocalmemory/hooks/portable_kit.py +34 -2
- package/src/superlocalmemory/learning/model_rollback.py +3 -0
- package/src/superlocalmemory/learning/ranker_retrain_online.py +2 -0
- package/src/superlocalmemory/learning/reward.py +50 -0
- package/src/superlocalmemory/learning/source_quality.py +523 -1
- package/src/superlocalmemory/loops/ledger.py +25 -5
- package/src/superlocalmemory/mcp/server.py +11 -30
- package/src/superlocalmemory/mcp/tools_core.py +4 -3
- package/src/superlocalmemory/mcp/tools_learning.py +2 -2
- package/src/superlocalmemory/retrieval/engine.py +53 -21
- package/src/superlocalmemory/retrieval/reranker.py +3 -4
- package/src/superlocalmemory/server/config_file.py +90 -0
- package/src/superlocalmemory/server/origin.py +50 -0
- package/src/superlocalmemory/server/routes/backup.py +293 -70
- package/src/superlocalmemory/server/routes/behavioral.py +336 -59
- package/src/superlocalmemory/server/routes/brain.py +57 -16
- package/src/superlocalmemory/server/routes/config_api.py +84 -82
- package/src/superlocalmemory/server/routes/entity.py +100 -23
- package/src/superlocalmemory/server/routes/evolution.py +103 -100
- package/src/superlocalmemory/server/routes/learning.py +276 -105
- package/src/superlocalmemory/server/routes/learning_telemetry.py +153 -0
- package/src/superlocalmemory/server/routes/mesh.py +121 -32
- package/src/superlocalmemory/server/routes/ratelimit.py +33 -25
- package/src/superlocalmemory/server/routes/stats.py +93 -155
- package/src/superlocalmemory/server/routes/token.py +3 -13
- package/src/superlocalmemory/server/routes/v3_api.py +64 -20
- package/src/superlocalmemory/server/unified_daemon.py +467 -40
- package/src/superlocalmemory/storage/migration_runner.py +79 -1
- package/src/superlocalmemory/storage/migrations/M010_evolution_config.py +5 -0
- package/src/superlocalmemory/storage/migrations/M028_fact_entity_associations.py +270 -0
- package/src/superlocalmemory/storage/migrations/M029_behavioral_history_indexes.py +137 -0
- package/src/superlocalmemory/storage/migrations/M030_entity_explorer_indexes.py +93 -0
- package/src/superlocalmemory/storage/migrations/__init__.py +4 -0
- package/src/superlocalmemory/storage/schema.py +49 -1
- package/src/superlocalmemory/storage/schema_v32.py +2 -0
- package/src/superlocalmemory/storage/schema_v347.py +4 -0
- package/src/superlocalmemory/ui/index.html +3 -6
- package/src/superlocalmemory/ui/js/core.js +52 -9
- package/src/superlocalmemory/ui/js/dashboard.js +169 -82
- package/src/superlocalmemory/ui/js/od-backup.js +156 -65
- package/src/superlocalmemory/ui/js/od-brain.js +88 -51
- package/src/superlocalmemory/ui/js/od-entities.js +22 -22
- package/src/superlocalmemory/ui/js/od-graph.js +11 -4
- package/src/superlocalmemory/ui/js/od-memories.js +47 -5
- package/src/superlocalmemory/ui/js/od-mesh.js +23 -9
- package/src/superlocalmemory/ui/js/od-settings.js +113 -59
- package/src/superlocalmemory/ui/js/od-shell.js +249 -33
- package/src/superlocalmemory/ui/js/od-skills.js +44 -17
- package/src/superlocalmemory/ui/js/settings.js +15 -1
- package/plugin-src/.mcp.json +0 -12
- package/plugin-src/agents/slm-governance-advisor.md +0 -80
- package/plugin-src/agents/slm-loop-runner.md +0 -71
- package/plugin-src/agents/slm-memory-advisor.md +0 -49
- package/plugin-src/agents/slm-optimize-advisor.md +0 -44
- package/plugin-src/commands/slm-loop.md +0 -31
- package/plugin-src/hooks/.gitkeep +0 -0
- package/plugin-src/hooks/hooks.json +0 -102
- package/plugin-src/manifest.json +0 -30
- package/plugin-src/requirements.txt +0 -1
- package/plugin-src/rules/CLAUDE.md.fragment +0 -44
- package/plugin-src/scripts/ensure-venv.bat +0 -122
- package/plugin-src/scripts/ensure-venv.sh +0 -105
- package/plugin-src/scripts/slm-launch +0 -62
- package/plugin-src/scripts/slm-launch.bat +0 -23
- package/plugin-src/settings.json +0 -25
- package/plugin-src/skills/slm-governance/SKILL.md +0 -248
- package/plugin-src/skills/slm-loop/SKILL.md +0 -99
- package/plugin-src/skills/slm-mesh/SKILL.md +0 -282
- package/plugin-src/skills/slm-profile/SKILL.md +0 -148
- package/plugin-src/skills/slm-scope/SKILL.md +0 -176
|
@@ -159,7 +159,10 @@ def cmd_session(args: Namespace) -> None:
|
|
|
159
159
|
def dispatch(args: Namespace) -> None:
|
|
160
160
|
"""Route CLI command to the appropriate handler."""
|
|
161
161
|
# Auto-install/upgrade hooks on version change (single file read, ~0.1ms)
|
|
162
|
-
if
|
|
162
|
+
if (
|
|
163
|
+
args.command not in ("hooks", "codex", "init", "mcp")
|
|
164
|
+
and not getattr(args, "dry_run", False)
|
|
165
|
+
):
|
|
163
166
|
try:
|
|
164
167
|
from superlocalmemory.hooks.claude_code_hooks import auto_install_if_needed
|
|
165
168
|
auto_install_if_needed()
|
|
@@ -958,16 +961,22 @@ def _agents_md_source_factory():
|
|
|
958
961
|
Gracefully skips if absent — never fails the MCP write.
|
|
959
962
|
"""
|
|
960
963
|
from pathlib import Path
|
|
964
|
+
import sysconfig
|
|
961
965
|
|
|
962
966
|
# Resolve relative to the package root (src/superlocalmemory/../../)
|
|
963
967
|
_pkg_root = Path(__file__).resolve().parents[3]
|
|
964
|
-
|
|
968
|
+
_candidates = (
|
|
969
|
+
_pkg_root / "plugin-src" / "rules" / "AGENTS.md",
|
|
970
|
+
Path(sysconfig.get_path("data")) / "share" / "superlocalmemory"
|
|
971
|
+
/ "portable-kit" / "rules" / "AGENTS.md",
|
|
972
|
+
)
|
|
965
973
|
|
|
966
974
|
def _read() -> str | None:
|
|
967
|
-
|
|
968
|
-
|
|
975
|
+
for candidate in _candidates:
|
|
976
|
+
if candidate.exists():
|
|
977
|
+
return candidate.read_text(encoding="utf-8")
|
|
969
978
|
logger.warning(
|
|
970
|
-
"WP-05 AGENTS.md not
|
|
979
|
+
"WP-05 AGENTS.md is not bundled — skipping AGENTS.md write"
|
|
971
980
|
)
|
|
972
981
|
return None
|
|
973
982
|
|
|
@@ -1008,9 +1017,10 @@ def cmd_connect(args: Namespace) -> None:
|
|
|
1008
1017
|
here=here,
|
|
1009
1018
|
profile=profile,
|
|
1010
1019
|
agents_md_source=_agents_md_source_factory(),
|
|
1020
|
+
dry_run=getattr(args, "dry_run", False),
|
|
1011
1021
|
)
|
|
1012
1022
|
|
|
1013
|
-
if not result.get("error"):
|
|
1023
|
+
if not result.get("error") and not getattr(args, "dry_run", False):
|
|
1014
1024
|
from superlocalmemory.infra.local_diagnostics import record_operation
|
|
1015
1025
|
|
|
1016
1026
|
record_operation("activation", client=ide_arg)
|
|
@@ -1029,6 +1039,7 @@ def cmd_connect(args: Namespace) -> None:
|
|
|
1029
1039
|
sys.exit(1)
|
|
1030
1040
|
|
|
1031
1041
|
status_sym = {"wrote": "[+]", "merged": "[~]", "unchanged": "[=]",
|
|
1042
|
+
"would_write": "[~]",
|
|
1032
1043
|
"skipped": "[s]", "error": "[!]"}.get(
|
|
1033
1044
|
result["mcp_config"], "[?]"
|
|
1034
1045
|
)
|
|
@@ -1057,6 +1068,7 @@ def cmd_connect(args: Namespace) -> None:
|
|
|
1057
1068
|
from superlocalmemory.hooks.ide_connector import IDEConnector
|
|
1058
1069
|
|
|
1059
1070
|
connector = IDEConnector()
|
|
1071
|
+
dry_run = bool(getattr(args, "dry_run", False))
|
|
1060
1072
|
|
|
1061
1073
|
if getattr(args, 'json', False):
|
|
1062
1074
|
from superlocalmemory.cli.json_output import json_print
|
|
@@ -1068,6 +1080,14 @@ def cmd_connect(args: Namespace) -> None:
|
|
|
1068
1080
|
elif getattr(args, "ide", None):
|
|
1069
1081
|
success = connector.connect(args.ide)
|
|
1070
1082
|
json_print("connect", data={"ide": args.ide, "connected": success})
|
|
1083
|
+
elif dry_run:
|
|
1084
|
+
json_print(
|
|
1085
|
+
"connect",
|
|
1086
|
+
data={
|
|
1087
|
+
"dry_run": True,
|
|
1088
|
+
"would_configure": connector.get_status(),
|
|
1089
|
+
},
|
|
1090
|
+
)
|
|
1071
1091
|
else:
|
|
1072
1092
|
json_print("connect", data={"results": connector.connect_all()},
|
|
1073
1093
|
next_actions=[
|
|
@@ -1081,6 +1101,12 @@ def cmd_connect(args: Namespace) -> None:
|
|
|
1081
1101
|
mark = "[+]" if s["installed"] else "[-]"
|
|
1082
1102
|
print(f" {mark} {s['name']:20s} {s['config_path']}")
|
|
1083
1103
|
return
|
|
1104
|
+
if dry_run:
|
|
1105
|
+
status = connector.get_status()
|
|
1106
|
+
print("Dry run — no IDE, hook, config, or SLM data files were written.")
|
|
1107
|
+
for item in status:
|
|
1108
|
+
print(f" would inspect/configure: {item['name']} ({item['config_path']})")
|
|
1109
|
+
return
|
|
1084
1110
|
if getattr(args, "ide", None):
|
|
1085
1111
|
success = connector.connect(args.ide)
|
|
1086
1112
|
print(f"{'Connected' if success else 'Failed'}: {args.ide}")
|
|
@@ -1563,16 +1589,86 @@ def cmd_forget(args: Namespace) -> None:
|
|
|
1563
1589
|
|
|
1564
1590
|
def cmd_delete(args: Namespace) -> None:
|
|
1565
1591
|
"""Delete a specific memory by exact fact ID."""
|
|
1592
|
+
import urllib.parse
|
|
1593
|
+
|
|
1594
|
+
from superlocalmemory.cli.daemon import daemon_request, is_daemon_running
|
|
1566
1595
|
from superlocalmemory.core.config import SLMConfig
|
|
1567
1596
|
from superlocalmemory.core.engine import MemoryEngine
|
|
1568
1597
|
|
|
1569
1598
|
use_json = getattr(args, 'json', False)
|
|
1599
|
+
fact_id = args.fact_id.strip()
|
|
1600
|
+
if is_daemon_running():
|
|
1601
|
+
path = "/api/memories/" + urllib.parse.quote(fact_id, safe="")
|
|
1602
|
+
confirmed = getattr(args, "yes", False)
|
|
1603
|
+
content = ""
|
|
1604
|
+
if not confirmed:
|
|
1605
|
+
detail = daemon_request(
|
|
1606
|
+
"GET",
|
|
1607
|
+
"/api/facts/" + urllib.parse.quote(fact_id, safe=""),
|
|
1608
|
+
)
|
|
1609
|
+
if not isinstance(detail, dict):
|
|
1610
|
+
if use_json:
|
|
1611
|
+
from superlocalmemory.cli.json_output import json_print
|
|
1612
|
+
json_print("delete", error={
|
|
1613
|
+
"code": "DAEMON_MUTATION_FAILED",
|
|
1614
|
+
"message": "Resident daemon could not resolve the memory.",
|
|
1615
|
+
})
|
|
1616
|
+
sys.exit(1)
|
|
1617
|
+
raise RuntimeError("Resident daemon could not resolve the memory.")
|
|
1618
|
+
content = str(detail.get("content") or "")
|
|
1619
|
+
if use_json:
|
|
1620
|
+
from superlocalmemory.cli.json_output import json_print
|
|
1621
|
+
json_print("delete", data={
|
|
1622
|
+
"fact_id": fact_id,
|
|
1623
|
+
"content": content[:120],
|
|
1624
|
+
"deleted": False,
|
|
1625
|
+
"hint": "Add --yes to confirm deletion",
|
|
1626
|
+
}, next_actions=[
|
|
1627
|
+
{
|
|
1628
|
+
"command": f"slm delete {fact_id} --json --yes",
|
|
1629
|
+
"description": "Confirm deletion",
|
|
1630
|
+
},
|
|
1631
|
+
])
|
|
1632
|
+
return
|
|
1633
|
+
print(f"Memory: {content[:120]}")
|
|
1634
|
+
confirmed = input("Delete this memory? [y/N] ").strip().lower() in (
|
|
1635
|
+
"y", "yes",
|
|
1636
|
+
)
|
|
1637
|
+
if not confirmed:
|
|
1638
|
+
print("Cancelled.")
|
|
1639
|
+
return
|
|
1640
|
+
|
|
1641
|
+
result = daemon_request("DELETE", path)
|
|
1642
|
+
if not isinstance(result, dict) or not result.get("success"):
|
|
1643
|
+
if use_json:
|
|
1644
|
+
from superlocalmemory.cli.json_output import json_print
|
|
1645
|
+
json_print("delete", error={
|
|
1646
|
+
"code": "DAEMON_MUTATION_FAILED",
|
|
1647
|
+
"message": "Resident daemon rejected the delete operation.",
|
|
1648
|
+
})
|
|
1649
|
+
sys.exit(1)
|
|
1650
|
+
raise RuntimeError("Resident daemon rejected the delete operation.")
|
|
1651
|
+
if use_json:
|
|
1652
|
+
from superlocalmemory.cli.json_output import json_print
|
|
1653
|
+
json_print(
|
|
1654
|
+
"delete",
|
|
1655
|
+
data={"deleted": fact_id, "content": content[:120]},
|
|
1656
|
+
next_actions=[
|
|
1657
|
+
{
|
|
1658
|
+
"command": "slm list --json",
|
|
1659
|
+
"description": "Verify remaining memories",
|
|
1660
|
+
},
|
|
1661
|
+
],
|
|
1662
|
+
)
|
|
1663
|
+
else:
|
|
1664
|
+
print(f"Deleted: {fact_id}")
|
|
1665
|
+
return
|
|
1666
|
+
|
|
1570
1667
|
try:
|
|
1571
1668
|
config = SLMConfig.load()
|
|
1572
1669
|
engine = MemoryEngine(config)
|
|
1573
1670
|
engine.initialize()
|
|
1574
1671
|
|
|
1575
|
-
fact_id = args.fact_id.strip()
|
|
1576
1672
|
rows = engine._db.execute(
|
|
1577
1673
|
"SELECT content FROM atomic_facts WHERE fact_id = ? AND profile_id = ?",
|
|
1578
1674
|
(fact_id, engine.profile_id),
|
|
@@ -1642,6 +1738,9 @@ def cmd_delete(args: Namespace) -> None:
|
|
|
1642
1738
|
|
|
1643
1739
|
def cmd_update(args: Namespace) -> None:
|
|
1644
1740
|
"""Update the content of a specific memory by exact fact ID."""
|
|
1741
|
+
import urllib.parse
|
|
1742
|
+
|
|
1743
|
+
from superlocalmemory.cli.daemon import daemon_request, is_daemon_running
|
|
1645
1744
|
from superlocalmemory.core.config import SLMConfig
|
|
1646
1745
|
from superlocalmemory.core.engine import MemoryEngine
|
|
1647
1746
|
|
|
@@ -1657,6 +1756,34 @@ def cmd_update(args: Namespace) -> None:
|
|
|
1657
1756
|
print("Error: content cannot be empty")
|
|
1658
1757
|
return
|
|
1659
1758
|
|
|
1759
|
+
if is_daemon_running():
|
|
1760
|
+
path = "/api/memories/" + urllib.parse.quote(fact_id, safe="")
|
|
1761
|
+
result = daemon_request("PATCH", path, {"content": new_content})
|
|
1762
|
+
if not isinstance(result, dict) or not result.get("success"):
|
|
1763
|
+
if use_json:
|
|
1764
|
+
from superlocalmemory.cli.json_output import json_print
|
|
1765
|
+
json_print("update", error={
|
|
1766
|
+
"code": "DAEMON_MUTATION_FAILED",
|
|
1767
|
+
"message": "Resident daemon rejected the update operation.",
|
|
1768
|
+
})
|
|
1769
|
+
sys.exit(1)
|
|
1770
|
+
raise RuntimeError("Resident daemon rejected the update operation.")
|
|
1771
|
+
if use_json:
|
|
1772
|
+
from superlocalmemory.cli.json_output import json_print
|
|
1773
|
+
json_print("update", data={
|
|
1774
|
+
"fact_id": fact_id,
|
|
1775
|
+
"new_content": new_content[:120],
|
|
1776
|
+
}, next_actions=[
|
|
1777
|
+
{
|
|
1778
|
+
"command": "slm list --json",
|
|
1779
|
+
"description": "List recent memories",
|
|
1780
|
+
},
|
|
1781
|
+
])
|
|
1782
|
+
else:
|
|
1783
|
+
print(f"New: {new_content[:100]}")
|
|
1784
|
+
print(f"Updated: {fact_id}")
|
|
1785
|
+
return
|
|
1786
|
+
|
|
1660
1787
|
try:
|
|
1661
1788
|
config = SLMConfig.load()
|
|
1662
1789
|
engine = MemoryEngine(config)
|
|
@@ -267,6 +267,13 @@ def daemon_request(
|
|
|
267
267
|
if capability is not None:
|
|
268
268
|
headers["X-SLM-Daemon-Capability"] = capability
|
|
269
269
|
headers["X-SLM-Target-Instance"] = descriptor.instance_id
|
|
270
|
+
# Daemon ownership proves that this CLI targets the local instance; it
|
|
271
|
+
# does not replace a dashboard user's profile-scoped authorization in
|
|
272
|
+
# governed workspaces. The user opts in by supplying an explicit
|
|
273
|
+
# session through the process environment (never logged or persisted).
|
|
274
|
+
user_session = os.environ.get("SLM_USER_SESSION", "").strip()
|
|
275
|
+
if user_session:
|
|
276
|
+
headers["X-SLM-User-Session"] = user_session
|
|
270
277
|
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
|
271
278
|
resp = urllib.request.urlopen(req, timeout=timeout_seconds)
|
|
272
279
|
return json.loads(resp.read().decode())
|
|
@@ -19,11 +19,11 @@ store rooted at the active data root (``SLM_DATA_DIR`` or
|
|
|
19
19
|
from __future__ import annotations
|
|
20
20
|
|
|
21
21
|
import json
|
|
22
|
-
import os
|
|
23
22
|
from argparse import Namespace
|
|
24
23
|
from pathlib import Path
|
|
25
24
|
from typing import Any
|
|
26
25
|
|
|
26
|
+
from superlocalmemory.infra.data_root import canonical_data_root
|
|
27
27
|
from superlocalmemory.loops import (
|
|
28
28
|
Bounds,
|
|
29
29
|
LapResult,
|
|
@@ -35,12 +35,7 @@ from superlocalmemory.loops import (
|
|
|
35
35
|
|
|
36
36
|
|
|
37
37
|
def _data_root() -> Path:
|
|
38
|
-
|
|
39
|
-
os.environ.get("SLM_DATA_DIR")
|
|
40
|
-
or os.environ.get("SL_MEMORY_PATH")
|
|
41
|
-
or os.environ.get("SLM_HOME")
|
|
42
|
-
)
|
|
43
|
-
return Path(value).expanduser() if value else Path.home() / ".superlocalmemory"
|
|
38
|
+
return canonical_data_root()
|
|
44
39
|
|
|
45
40
|
|
|
46
41
|
class _FailOpenLedger:
|
|
@@ -114,12 +114,17 @@ def main() -> None:
|
|
|
114
114
|
or any(arg in {"-h", "--help"} for arg in sys.argv[1:])
|
|
115
115
|
or (len(sys.argv) == 2 and sys.argv[1] in {"-v", "--version"})
|
|
116
116
|
)
|
|
117
|
+
_is_connect_dry_run = (
|
|
118
|
+
len(sys.argv) >= 2
|
|
119
|
+
and sys.argv[1] == "connect"
|
|
120
|
+
and "--dry-run" in sys.argv[2:]
|
|
121
|
+
)
|
|
117
122
|
|
|
118
123
|
# WP-07: lazy first-run init — runs after hook/mcp fast-paths so stdout
|
|
119
124
|
# is never polluted on those paths (CRIT-3, MCP JSON-RPC purity).
|
|
120
125
|
# Guarded: any failure must not crash the CLI (AC4).
|
|
121
126
|
_is_mcp_cmd = len(sys.argv) >= 2 and sys.argv[1] == "mcp"
|
|
122
|
-
if not _is_mcp_cmd and not _is_metadata_cmd:
|
|
127
|
+
if not _is_mcp_cmd and not _is_metadata_cmd and not _is_connect_dry_run:
|
|
123
128
|
try:
|
|
124
129
|
from superlocalmemory.cli._lazy_init import _ensure_initialized
|
|
125
130
|
_ensure_initialized()
|
|
@@ -139,7 +144,7 @@ def main() -> None:
|
|
|
139
144
|
|
|
140
145
|
# One-time post-upgrade banner — silent for fresh installs and
|
|
141
146
|
# same-version runs. Guarded against I/O errors internally.
|
|
142
|
-
if not _is_mcp_stdio and not _is_metadata_cmd:
|
|
147
|
+
if not _is_mcp_stdio and not _is_metadata_cmd and not _is_connect_dry_run:
|
|
143
148
|
from superlocalmemory.cli.version_banner import check_and_emit_upgrade_banner
|
|
144
149
|
if check_and_emit_upgrade_banner(_ver):
|
|
145
150
|
# First post-upgrade invocation: apply the data-dir migration if
|
|
@@ -816,8 +821,9 @@ def main() -> None:
|
|
|
816
821
|
sys.exit(0)
|
|
817
822
|
|
|
818
823
|
# V3.3.19: Auto-trigger setup wizard on first use
|
|
819
|
-
|
|
820
|
-
|
|
824
|
+
if not (args.command == "connect" and getattr(args, "dry_run", False)):
|
|
825
|
+
from superlocalmemory.cli.setup_wizard import check_first_use
|
|
826
|
+
check_first_use(args.command)
|
|
821
827
|
|
|
822
828
|
# V3.4.4: Auto-start daemon for all commands that need it.
|
|
823
829
|
# SLM is always-on — close laptop, reboot, crash: daemon auto-recovers.
|
|
@@ -26,6 +26,15 @@ _VERSION_PATTERN = re.compile(r"^[0-9A-Za-z.\-+_]{1,32}$")
|
|
|
26
26
|
|
|
27
27
|
_MAX_MARKER_BYTES = 64 # a semver string is ≤ 32 chars; 64 is plenty
|
|
28
28
|
|
|
29
|
+
_RELEASE_NOTES: dict[str, tuple[str, ...]] = {
|
|
30
|
+
"3.8.1": (
|
|
31
|
+
"Large existing databases no longer delay daemon readiness",
|
|
32
|
+
"Failed enrichment retries stop after ten automatic attempts",
|
|
33
|
+
"Dashboard panes stay mounted between navigation changes",
|
|
34
|
+
"Local model workers stay warm for a 30-minute working session",
|
|
35
|
+
),
|
|
36
|
+
}
|
|
37
|
+
|
|
29
38
|
|
|
30
39
|
def _data_dir() -> Path:
|
|
31
40
|
from superlocalmemory.infra.data_root import canonical_data_root
|
|
@@ -116,11 +125,16 @@ def _banner(prior: str | None, current: str) -> str:
|
|
|
116
125
|
header = (f"SuperLocalMemory upgraded from {prior} to {current}"
|
|
117
126
|
if prior else
|
|
118
127
|
f"SuperLocalMemory upgraded to {current} (from an earlier version)")
|
|
128
|
+
notes = _RELEASE_NOTES.get(
|
|
129
|
+
current,
|
|
130
|
+
(
|
|
131
|
+
"The installed version has changed; existing memory remains in place",
|
|
132
|
+
"See the release notes for version-specific changes",
|
|
133
|
+
),
|
|
134
|
+
)
|
|
119
135
|
return "\n".join([
|
|
120
136
|
header,
|
|
121
|
-
" -
|
|
122
|
-
" - Feedback and learning signals flow from every IDE to the daemon",
|
|
123
|
-
" - Silent data migration complete; no manual steps required",
|
|
137
|
+
*(f" - {note}" for note in notes),
|
|
124
138
|
"Run `slm doctor` to verify your setup.",
|
|
125
139
|
"",
|
|
126
140
|
])
|
|
@@ -52,7 +52,7 @@ class BackendOrchestrator:
|
|
|
52
52
|
"""Central coordinator for multi-backend architecture.
|
|
53
53
|
|
|
54
54
|
Lifecycle:
|
|
55
|
-
on_daemon_start() →
|
|
55
|
+
on_daemon_start() → initialize bounded backend state → ready
|
|
56
56
|
sync_new_fact() → called from store_pipeline after SQLite write
|
|
57
57
|
health_check() → returns status of all backends
|
|
58
58
|
"""
|
|
@@ -71,7 +71,7 @@ class BackendOrchestrator:
|
|
|
71
71
|
# ------------------------------------------------------------------
|
|
72
72
|
|
|
73
73
|
def on_daemon_start(self) -> None:
|
|
74
|
-
"""
|
|
74
|
+
"""Initialize bounded backend state without delaying daemon readiness."""
|
|
75
75
|
logger.info("BackendOrchestrator: daemon starting")
|
|
76
76
|
|
|
77
77
|
# 1. Apply schema (if not already applied)
|
|
@@ -82,18 +82,13 @@ class BackendOrchestrator:
|
|
|
82
82
|
try:
|
|
83
83
|
from superlocalmemory.core.tier_manager import evaluate_tiers
|
|
84
84
|
self._tiers = evaluate_tiers
|
|
85
|
-
logger.info("BackendOrchestrator:
|
|
85
|
+
logger.info("BackendOrchestrator: tier evaluator registered")
|
|
86
86
|
except Exception as exc:
|
|
87
87
|
logger.warning("TierManager init failed (non-fatal): %s", exc)
|
|
88
88
|
|
|
89
|
-
#
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
result = rebalance(self._db)
|
|
93
|
-
logger.info("BackendOrchestrator: initial rebalance — %s",
|
|
94
|
-
result.get("total_evaluated", "?"))
|
|
95
|
-
except Exception as exc:
|
|
96
|
-
logger.warning("Initial rebalance failed (non-fatal): %s", exc)
|
|
89
|
+
# Full-database tier evaluation belongs to MaintenanceScheduler. Running
|
|
90
|
+
# it here blocks FastAPI lifespan readiness on mature upgrade databases
|
|
91
|
+
# and occurs before optional projection backends are fully registered.
|
|
97
92
|
|
|
98
93
|
self._recover_interrupted_scale_promotion()
|
|
99
94
|
|
|
@@ -107,12 +102,12 @@ class BackendOrchestrator:
|
|
|
107
102
|
)
|
|
108
103
|
return
|
|
109
104
|
|
|
110
|
-
#
|
|
105
|
+
# 3. Initialize CozoDB if available
|
|
111
106
|
cozo_available = self._detect_cozo()
|
|
112
107
|
if cozo_available:
|
|
113
108
|
self._init_cozo()
|
|
114
109
|
|
|
115
|
-
#
|
|
110
|
+
# 4. Initialize LanceDB if available
|
|
116
111
|
lancedb_available = self._detect_lancedb()
|
|
117
112
|
if lancedb_available:
|
|
118
113
|
self._init_lancedb()
|
|
@@ -139,13 +134,19 @@ class BackendOrchestrator:
|
|
|
139
134
|
try:
|
|
140
135
|
from superlocalmemory.core.scale_engine import ScaleEngineManager
|
|
141
136
|
|
|
142
|
-
result = ScaleEngineManager(
|
|
137
|
+
result = ScaleEngineManager(
|
|
138
|
+
self._config,
|
|
139
|
+
profile_id="default",
|
|
140
|
+
).recover_interrupted_promotion()
|
|
143
141
|
if result:
|
|
144
142
|
logger.warning("Scale Engine promotion recovery: %s", result)
|
|
145
143
|
except Exception as exc:
|
|
146
144
|
# A scale projection is derived data. Startup must keep serving
|
|
147
145
|
# canonical SQLite even if optional recovery itself is unhealthy.
|
|
148
|
-
logger.error(
|
|
146
|
+
logger.error(
|
|
147
|
+
"Scale Engine recovery requires repair; Local Core remains active: %s",
|
|
148
|
+
exc,
|
|
149
|
+
)
|
|
149
150
|
|
|
150
151
|
# ------------------------------------------------------------------
|
|
151
152
|
# Incremental Sync (F-04: called from store_pipeline)
|
|
@@ -401,7 +402,8 @@ class BackendOrchestrator:
|
|
|
401
402
|
def _apply_schema_v345(self) -> None:
|
|
402
403
|
try:
|
|
403
404
|
from superlocalmemory.storage.schema_v345 import (
|
|
404
|
-
apply_migration,
|
|
405
|
+
apply_migration,
|
|
406
|
+
schema_version_applied,
|
|
405
407
|
)
|
|
406
408
|
# #47 fix: use raw_connection() — DatabaseManager has no `.conn`,
|
|
407
409
|
# so the old code raised AttributeError that was silently swallowed,
|
|
@@ -75,6 +75,8 @@ _TRUSTED_REMOTE_CODE_MODELS = frozenset({
|
|
|
75
75
|
"nomic-ai/nomic-embed-text-v1",
|
|
76
76
|
})
|
|
77
77
|
|
|
78
|
+
_RSS_LIMIT_MB = int(os.environ.get("SLM_EMBED_WORKER_RSS_LIMIT_MB", 2500))
|
|
79
|
+
|
|
78
80
|
|
|
79
81
|
def _trusts_remote_code(model_name: str) -> bool:
|
|
80
82
|
return model_name in _TRUSTED_REMOTE_CODE_MODELS
|
|
@@ -119,10 +121,10 @@ def _worker_main() -> None:
|
|
|
119
121
|
_start_parent_watchdog()
|
|
120
122
|
|
|
121
123
|
import numpy as np
|
|
124
|
+
|
|
122
125
|
from superlocalmemory.core.platform_utils import get_rss_mb
|
|
123
126
|
|
|
124
127
|
model = None
|
|
125
|
-
model_name = None
|
|
126
128
|
dim = 0
|
|
127
129
|
|
|
128
130
|
for line in sys.stdin:
|
|
@@ -154,7 +156,6 @@ def _worker_main() -> None:
|
|
|
154
156
|
_respond({"ok": False, "error": f"Dimension mismatch: {dim} != {expected_dim}"})
|
|
155
157
|
model = None
|
|
156
158
|
continue
|
|
157
|
-
model_name = name
|
|
158
159
|
_respond({"ok": True, "dim": dim, "model": name, "backend": active_backend})
|
|
159
160
|
else:
|
|
160
161
|
_respond({"ok": False, "error": "Model load failed"})
|
|
@@ -170,7 +171,6 @@ def _worker_main() -> None:
|
|
|
170
171
|
model, active_backend = _load_embedding_model(name)
|
|
171
172
|
if model is not None:
|
|
172
173
|
dim = model.get_sentence_embedding_dimension()
|
|
173
|
-
model_name = name
|
|
174
174
|
else:
|
|
175
175
|
_respond({"ok": False, "error": "Model load failed"})
|
|
176
176
|
continue
|
|
@@ -185,9 +185,8 @@ def _worker_main() -> None:
|
|
|
185
185
|
_respond({"ok": False, "error": str(exc)})
|
|
186
186
|
|
|
187
187
|
# V3.3.16: RSS watchdog — V3.4.24: cross-platform via platform_utils.
|
|
188
|
-
_rss_limit = int(os.environ.get("SLM_EMBED_WORKER_RSS_LIMIT_MB", 1800))
|
|
189
188
|
rss_mb = get_rss_mb()
|
|
190
|
-
if rss_mb > 0 and rss_mb >
|
|
189
|
+
if rss_mb > 0 and rss_mb > _RSS_LIMIT_MB:
|
|
191
190
|
sys.exit(0)
|
|
192
191
|
|
|
193
192
|
continue
|