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.
Files changed (118) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/README.md +32 -120
  3. package/package.json +9 -2
  4. package/plugin/.claude-plugin/plugin.json +1 -2
  5. package/plugin/CLAUDE.md +3 -3
  6. package/plugin/agents/slm-governance-advisor.md +1 -1
  7. package/plugin/agents/slm-loop-runner.md +1 -1
  8. package/plugin/agents/slm-memory-advisor.md +1 -1
  9. package/plugin/agents/slm-optimize-advisor.md +1 -1
  10. package/plugin/requirements.txt +1 -1
  11. package/plugin/skills/slm-cache/SKILL.md +1 -1
  12. package/plugin/skills/slm-compress/SKILL.md +1 -1
  13. package/plugin/skills/slm-governance/SKILL.md +1 -1
  14. package/plugin/skills/slm-graph/SKILL.md +1 -1
  15. package/plugin/skills/slm-loop/SKILL.md +1 -1
  16. package/plugin/skills/slm-mesh/SKILL.md +1 -1
  17. package/plugin/skills/slm-profile/SKILL.md +1 -1
  18. package/plugin/skills/slm-recall/SKILL.md +1 -1
  19. package/plugin/skills/slm-remember/SKILL.md +1 -1
  20. package/plugin/skills/slm-scope/SKILL.md +1 -1
  21. package/plugin/skills/slm-session/SKILL.md +1 -1
  22. package/plugin/skills/slm-status/SKILL.md +1 -1
  23. package/plugin-src/rules/AGENTS.md +1 -1
  24. package/plugin-src/skills/slm-cache/SKILL.md +1 -1
  25. package/plugin-src/skills/slm-compress/SKILL.md +1 -1
  26. package/plugin-src/skills/slm-graph/SKILL.md +1 -1
  27. package/plugin-src/skills/slm-recall/SKILL.md +1 -1
  28. package/plugin-src/skills/slm-remember/SKILL.md +1 -1
  29. package/plugin-src/skills/slm-session/SKILL.md +1 -1
  30. package/plugin-src/skills/slm-status/SKILL.md +1 -1
  31. package/pyproject.toml +2 -1
  32. package/src/superlocalmemory/__init__.py +1 -1
  33. package/src/superlocalmemory/cli/commands.py +134 -7
  34. package/src/superlocalmemory/cli/daemon.py +7 -0
  35. package/src/superlocalmemory/cli/loop_cmd.py +2 -7
  36. package/src/superlocalmemory/cli/main.py +10 -4
  37. package/src/superlocalmemory/cli/version_banner.py +17 -3
  38. package/src/superlocalmemory/core/backend_orchestrator.py +18 -16
  39. package/src/superlocalmemory/core/embedding_worker.py +4 -5
  40. package/src/superlocalmemory/core/embeddings.py +119 -45
  41. package/src/superlocalmemory/core/engine.py +24 -21
  42. package/src/superlocalmemory/core/engine_ingestion.py +332 -45
  43. package/src/superlocalmemory/core/ingestion_command.py +134 -25
  44. package/src/superlocalmemory/core/injection.py +12 -7
  45. package/src/superlocalmemory/core/maintenance_scheduler.py +17 -7
  46. package/src/superlocalmemory/core/recall_pipeline.py +5 -3
  47. package/src/superlocalmemory/core/store_pipeline.py +153 -20
  48. package/src/superlocalmemory/hooks/hook_handlers.py +6 -1
  49. package/src/superlocalmemory/hooks/portable_kit.py +34 -2
  50. package/src/superlocalmemory/learning/model_rollback.py +3 -0
  51. package/src/superlocalmemory/learning/ranker_retrain_online.py +2 -0
  52. package/src/superlocalmemory/learning/reward.py +50 -0
  53. package/src/superlocalmemory/learning/source_quality.py +523 -1
  54. package/src/superlocalmemory/loops/ledger.py +25 -5
  55. package/src/superlocalmemory/mcp/server.py +11 -30
  56. package/src/superlocalmemory/mcp/tools_core.py +4 -3
  57. package/src/superlocalmemory/mcp/tools_learning.py +2 -2
  58. package/src/superlocalmemory/retrieval/engine.py +53 -21
  59. package/src/superlocalmemory/retrieval/reranker.py +3 -4
  60. package/src/superlocalmemory/server/config_file.py +90 -0
  61. package/src/superlocalmemory/server/origin.py +50 -0
  62. package/src/superlocalmemory/server/routes/backup.py +293 -70
  63. package/src/superlocalmemory/server/routes/behavioral.py +336 -59
  64. package/src/superlocalmemory/server/routes/brain.py +57 -16
  65. package/src/superlocalmemory/server/routes/config_api.py +84 -82
  66. package/src/superlocalmemory/server/routes/entity.py +100 -23
  67. package/src/superlocalmemory/server/routes/evolution.py +103 -100
  68. package/src/superlocalmemory/server/routes/learning.py +276 -105
  69. package/src/superlocalmemory/server/routes/learning_telemetry.py +153 -0
  70. package/src/superlocalmemory/server/routes/mesh.py +121 -32
  71. package/src/superlocalmemory/server/routes/ratelimit.py +33 -25
  72. package/src/superlocalmemory/server/routes/stats.py +93 -155
  73. package/src/superlocalmemory/server/routes/token.py +3 -13
  74. package/src/superlocalmemory/server/routes/v3_api.py +64 -20
  75. package/src/superlocalmemory/server/unified_daemon.py +467 -40
  76. package/src/superlocalmemory/storage/migration_runner.py +79 -1
  77. package/src/superlocalmemory/storage/migrations/M010_evolution_config.py +5 -0
  78. package/src/superlocalmemory/storage/migrations/M028_fact_entity_associations.py +270 -0
  79. package/src/superlocalmemory/storage/migrations/M029_behavioral_history_indexes.py +137 -0
  80. package/src/superlocalmemory/storage/migrations/M030_entity_explorer_indexes.py +93 -0
  81. package/src/superlocalmemory/storage/migrations/__init__.py +4 -0
  82. package/src/superlocalmemory/storage/schema.py +49 -1
  83. package/src/superlocalmemory/storage/schema_v32.py +2 -0
  84. package/src/superlocalmemory/storage/schema_v347.py +4 -0
  85. package/src/superlocalmemory/ui/index.html +3 -6
  86. package/src/superlocalmemory/ui/js/core.js +52 -9
  87. package/src/superlocalmemory/ui/js/dashboard.js +169 -82
  88. package/src/superlocalmemory/ui/js/od-backup.js +156 -65
  89. package/src/superlocalmemory/ui/js/od-brain.js +88 -51
  90. package/src/superlocalmemory/ui/js/od-entities.js +22 -22
  91. package/src/superlocalmemory/ui/js/od-graph.js +11 -4
  92. package/src/superlocalmemory/ui/js/od-memories.js +47 -5
  93. package/src/superlocalmemory/ui/js/od-mesh.js +23 -9
  94. package/src/superlocalmemory/ui/js/od-settings.js +113 -59
  95. package/src/superlocalmemory/ui/js/od-shell.js +249 -33
  96. package/src/superlocalmemory/ui/js/od-skills.js +44 -17
  97. package/src/superlocalmemory/ui/js/settings.js +15 -1
  98. package/plugin-src/.mcp.json +0 -12
  99. package/plugin-src/agents/slm-governance-advisor.md +0 -80
  100. package/plugin-src/agents/slm-loop-runner.md +0 -71
  101. package/plugin-src/agents/slm-memory-advisor.md +0 -49
  102. package/plugin-src/agents/slm-optimize-advisor.md +0 -44
  103. package/plugin-src/commands/slm-loop.md +0 -31
  104. package/plugin-src/hooks/.gitkeep +0 -0
  105. package/plugin-src/hooks/hooks.json +0 -102
  106. package/plugin-src/manifest.json +0 -30
  107. package/plugin-src/requirements.txt +0 -1
  108. package/plugin-src/rules/CLAUDE.md.fragment +0 -44
  109. package/plugin-src/scripts/ensure-venv.bat +0 -122
  110. package/plugin-src/scripts/ensure-venv.sh +0 -105
  111. package/plugin-src/scripts/slm-launch +0 -62
  112. package/plugin-src/scripts/slm-launch.bat +0 -23
  113. package/plugin-src/settings.json +0 -25
  114. package/plugin-src/skills/slm-governance/SKILL.md +0 -248
  115. package/plugin-src/skills/slm-loop/SKILL.md +0 -99
  116. package/plugin-src/skills/slm-mesh/SKILL.md +0 -282
  117. package/plugin-src/skills/slm-profile/SKILL.md +0 -148
  118. 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 args.command not in ("hooks", "codex", "init", "mcp"):
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
- _agents_src = _pkg_root / "plugin-src" / "rules" / "AGENTS.md"
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
- if _agents_src.exists():
968
- return _agents_src.read_text(encoding="utf-8")
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 found at %s — skipping AGENTS.md write", _agents_src
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
- value = (
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
- from superlocalmemory.cli.setup_wizard import check_first_use
820
- check_first_use(args.command)
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
- " - Multi-IDE MCP processes now share a worker — large RAM drop",
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() → migrate backends → ready
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
- """Called once on daemon startup. Order matters (F-11: rebalance before migration)."""
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: TierManager initialized")
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
- # 3. Run initial tier rebalance FIRST (F-11: before migration)
90
- try:
91
- from superlocalmemory.core.tier_manager import evaluate_tiers as rebalance
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
- # 4. Initialize CozoDB if available
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
- # 5. Initialize LanceDB if available
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(self._config, profile_id="default").recover_interrupted_promotion()
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("Scale Engine recovery requires repair; Local Core remains active: %s", exc)
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, schema_version_applied,
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 > _rss_limit:
189
+ if rss_mb > 0 and rss_mb > _RSS_LIMIT_MB:
191
190
  sys.exit(0)
192
191
 
193
192
  continue