superlocalmemory 3.8.1 → 3.8.2
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 +52 -0
- package/README.md +2 -2
- 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 +2 -2
- 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 +3 -5
- 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 +2 -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 +3 -5
- 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/scripts/postinstall.js +7 -1
- package/src/superlocalmemory/__init__.py +1 -1
- package/src/superlocalmemory/cli/commands.py +360 -2
- package/src/superlocalmemory/cli/main.py +62 -3
- package/src/superlocalmemory/cli/setup_wizard.py +142 -16
- package/src/superlocalmemory/core/component_healer.py +144 -0
- package/src/superlocalmemory/core/component_registry.py +487 -0
- package/src/superlocalmemory/core/config.py +21 -0
- package/src/superlocalmemory/core/embeddings.py +14 -1
- package/src/superlocalmemory/core/engine.py +9 -5
- package/src/superlocalmemory/core/ingestion_command.py +36 -16
- package/src/superlocalmemory/core/maintenance.py +43 -0
- package/src/superlocalmemory/core/maintenance_scheduler.py +28 -0
- package/src/superlocalmemory/core/recall_pipeline.py +39 -3
- package/src/superlocalmemory/core/store_pipeline.py +42 -0
- package/src/superlocalmemory/mcp/_daemon_proxy.py +6 -2
- package/src/superlocalmemory/mcp/_pool_adapter.py +4 -1
- package/src/superlocalmemory/mcp/tools_active.py +1 -1
- package/src/superlocalmemory/mcp/tools_core.py +17 -2
- package/src/superlocalmemory/retrieval/bridge_discovery.py +14 -0
- package/src/superlocalmemory/retrieval/spreading_activation.py +68 -38
- package/src/superlocalmemory/server/routes/behavioral.py +6 -2
- package/src/superlocalmemory/server/routes/learning.py +13 -3
- package/src/superlocalmemory/server/routes/memories.py +8 -3
- package/src/superlocalmemory/server/routes/v3_api.py +120 -0
- package/src/superlocalmemory/server/unified_daemon.py +266 -2
- package/src/superlocalmemory/storage/embedding_migrator.py +235 -0
- package/src/superlocalmemory/ui/index.html +3 -2
- package/src/superlocalmemory/ui/js/od-components.js +147 -0
- package/src/superlocalmemory/ui/js/od-entities.js +43 -0
- package/src/superlocalmemory/ui/js/od-graph.js +35 -0
- package/src/superlocalmemory/ui/js/od-health.js +18 -0
- package/src/superlocalmemory/ui/js/od-memories.js +37 -0
- package/src/superlocalmemory/ui/js/od-operations.js +36 -0
- package/src/superlocalmemory/ui/js/od-settings.js +72 -3
|
@@ -1060,6 +1060,126 @@ async def set_auto_recall_config(request: Request):
|
|
|
1060
1060
|
return _internal_error()
|
|
1061
1061
|
|
|
1062
1062
|
|
|
1063
|
+
# ── Runtime behaviour config (v3.8.2 UX-1) ──────────────────
|
|
1064
|
+
# User-facing settings that take effect LIVE (no restart) via the
|
|
1065
|
+
# reconfigure_daemon_engine hot-swap path AND persist across restarts via
|
|
1066
|
+
# SLMConfig.save(). Scoped deliberately to fields that save() round-trips:
|
|
1067
|
+
# retrieval (asdict) + injection (explicit). Excludes setup-time/internal
|
|
1068
|
+
# knobs and any field save() doesn't persist (which would silently revert).
|
|
1069
|
+
_RUNTIME_CONFIG_FIELDS = (
|
|
1070
|
+
# (section, field, kind, min, max)
|
|
1071
|
+
("retrieval", "top_k", "int", 1, 200),
|
|
1072
|
+
("retrieval", "use_cross_encoder", "bool", None, None),
|
|
1073
|
+
("injection", "enabled", "bool", None, None),
|
|
1074
|
+
("injection", "core_block_enabled", "bool", None, None),
|
|
1075
|
+
("injection", "core_block_max_facts", "int", 0, 50),
|
|
1076
|
+
)
|
|
1077
|
+
|
|
1078
|
+
|
|
1079
|
+
def _runtime_config_snapshot(config) -> dict:
|
|
1080
|
+
"""Current values of the exposed runtime fields, grouped by section."""
|
|
1081
|
+
out: dict = {}
|
|
1082
|
+
for section, field, *_ in _RUNTIME_CONFIG_FIELDS:
|
|
1083
|
+
sec = getattr(config, section, None)
|
|
1084
|
+
out.setdefault(section, {})[field] = (
|
|
1085
|
+
getattr(sec, field, None) if sec is not None else None
|
|
1086
|
+
)
|
|
1087
|
+
return out
|
|
1088
|
+
|
|
1089
|
+
|
|
1090
|
+
@router.get("/runtime/config")
|
|
1091
|
+
async def get_runtime_config(request: Request):
|
|
1092
|
+
"""User-facing runtime behaviour settings.
|
|
1093
|
+
|
|
1094
|
+
Recall depth (top_k), reranker on/off (use_cross_encoder), and memory
|
|
1095
|
+
injection (master + core-block). All apply live via the daemon hot-swap —
|
|
1096
|
+
no restart — and persist across restarts.
|
|
1097
|
+
"""
|
|
1098
|
+
try:
|
|
1099
|
+
from superlocalmemory.core.config import SLMConfig
|
|
1100
|
+
config = getattr(request.app.state, "config", None) or SLMConfig.load()
|
|
1101
|
+
return {"success": True, "config": _runtime_config_snapshot(config)}
|
|
1102
|
+
except Exception:
|
|
1103
|
+
return _internal_error()
|
|
1104
|
+
|
|
1105
|
+
|
|
1106
|
+
@router.put("/runtime/config")
|
|
1107
|
+
async def set_runtime_config(request: Request):
|
|
1108
|
+
"""Validate, persist, and hot-apply runtime behaviour settings.
|
|
1109
|
+
|
|
1110
|
+
Body: ``{"retrieval": {"top_k": 30}, "injection": {"enabled": false}}``.
|
|
1111
|
+
Only known fields are accepted; a bad type/range is rejected 400 and
|
|
1112
|
+
nothing is applied (fail-fast — never let a bad value wedge the engine).
|
|
1113
|
+
"""
|
|
1114
|
+
_require_manage(request)
|
|
1115
|
+
try:
|
|
1116
|
+
import dataclasses as _dc
|
|
1117
|
+
from superlocalmemory.core.config import SLMConfig
|
|
1118
|
+
|
|
1119
|
+
body = await request.json()
|
|
1120
|
+
if not isinstance(body, dict):
|
|
1121
|
+
return JSONResponse({"error": "body must be an object"}, status_code=400)
|
|
1122
|
+
|
|
1123
|
+
# Validate everything BEFORE mutating anything.
|
|
1124
|
+
updates: dict[str, dict] = {}
|
|
1125
|
+
for section, field, kind, lo, hi in _RUNTIME_CONFIG_FIELDS:
|
|
1126
|
+
sec_in = body.get(section)
|
|
1127
|
+
if not isinstance(sec_in, dict) or field not in sec_in:
|
|
1128
|
+
continue
|
|
1129
|
+
val = sec_in[field]
|
|
1130
|
+
if kind == "bool":
|
|
1131
|
+
if not isinstance(val, bool):
|
|
1132
|
+
return JSONResponse(
|
|
1133
|
+
{"error": f"{section}.{field} must be true or false"},
|
|
1134
|
+
status_code=400,
|
|
1135
|
+
)
|
|
1136
|
+
elif kind == "int":
|
|
1137
|
+
# bool is an int subclass — reject it explicitly.
|
|
1138
|
+
if isinstance(val, bool) or not isinstance(val, int):
|
|
1139
|
+
return JSONResponse(
|
|
1140
|
+
{"error": f"{section}.{field} must be an integer"},
|
|
1141
|
+
status_code=400,
|
|
1142
|
+
)
|
|
1143
|
+
if (lo is not None and val < lo) or (hi is not None and val > hi):
|
|
1144
|
+
return JSONResponse(
|
|
1145
|
+
{"error": f"{section}.{field} must be between {lo} and {hi}"},
|
|
1146
|
+
status_code=400,
|
|
1147
|
+
)
|
|
1148
|
+
updates.setdefault(section, {})[field] = val
|
|
1149
|
+
|
|
1150
|
+
if not updates:
|
|
1151
|
+
return JSONResponse(
|
|
1152
|
+
{"error": "no known runtime settings in request body"},
|
|
1153
|
+
status_code=400,
|
|
1154
|
+
)
|
|
1155
|
+
|
|
1156
|
+
# These are plain behaviour flags read per-recall — NOT model/mode
|
|
1157
|
+
# swaps. So we apply them the light way: swap the sub-config objects on
|
|
1158
|
+
# the LIVE config the daemon+engine already hold, then persist to disk.
|
|
1159
|
+
# This avoids the heavyweight engine drain+rebuild (which reloads
|
|
1160
|
+
# models and can time out draining in-flight recalls) — the running
|
|
1161
|
+
# engine simply reads the new values on its next recall/injection.
|
|
1162
|
+
live = getattr(request.app.state, "config", None) or SLMConfig.load()
|
|
1163
|
+
targets = [live]
|
|
1164
|
+
engine = getattr(request.app.state, "engine", None)
|
|
1165
|
+
eng_cfg = getattr(engine, "_config", None) if engine is not None else None
|
|
1166
|
+
if eng_cfg is not None and eng_cfg is not live:
|
|
1167
|
+
targets.append(eng_cfg)
|
|
1168
|
+
for cfg in targets:
|
|
1169
|
+
for section, changes in updates.items():
|
|
1170
|
+
sec = getattr(cfg, section, None)
|
|
1171
|
+
if sec is None:
|
|
1172
|
+
continue
|
|
1173
|
+
# replace() yields a NEW sub-config (works frozen or mutable);
|
|
1174
|
+
# assigning the attribute is an atomic reference swap.
|
|
1175
|
+
setattr(cfg, section, _dc.replace(sec, **changes))
|
|
1176
|
+
request.app.state.config = live
|
|
1177
|
+
live.save(mode_change=False) # durable across restart
|
|
1178
|
+
return {"success": True, "config": _runtime_config_snapshot(live)}
|
|
1179
|
+
except Exception:
|
|
1180
|
+
return _internal_error()
|
|
1181
|
+
|
|
1182
|
+
|
|
1063
1183
|
# ── IDE Status ───────────────────────────────────────────────
|
|
1064
1184
|
|
|
1065
1185
|
@router.get("/ide/status")
|
|
@@ -1068,6 +1068,21 @@ async def _source_quality_repair_loop(
|
|
|
1068
1068
|
state="retrying",
|
|
1069
1069
|
last_error="storage_temporarily_unavailable",
|
|
1070
1070
|
)
|
|
1071
|
+
except Exception as exc: # F6 fix: broad catch keeps loop alive
|
|
1072
|
+
# Any unexpected exception in a single tick (e.g. malformed JSON
|
|
1073
|
+
# blob, AttributeError from a half-migrated schema) must not kill
|
|
1074
|
+
# the entire repair loop. Log and continue to next iteration.
|
|
1075
|
+
logger.warning(
|
|
1076
|
+
"source-quality repair tick failed unexpectedly: %s — "
|
|
1077
|
+
"loop continues",
|
|
1078
|
+
exc,
|
|
1079
|
+
exc_info=True,
|
|
1080
|
+
)
|
|
1081
|
+
_set_source_quality_repair_status(
|
|
1082
|
+
application,
|
|
1083
|
+
state="retrying",
|
|
1084
|
+
last_error=type(exc).__name__,
|
|
1085
|
+
)
|
|
1071
1086
|
else:
|
|
1072
1087
|
successful_ticks += 1
|
|
1073
1088
|
if pending == []:
|
|
@@ -1448,9 +1463,186 @@ async def lifespan(application: FastAPI):
|
|
|
1448
1463
|
except Exception as exc:
|
|
1449
1464
|
logger.warning("Vector store backfill failed (non-fatal): %s", exc)
|
|
1450
1465
|
|
|
1466
|
+
def _self_heal():
|
|
1467
|
+
"""v3.8.2 zero-pain self-heal.
|
|
1468
|
+
|
|
1469
|
+
On daemon start (especially right after a pip/npm upgrade of a
|
|
1470
|
+
months-old database), silently restore full retrieval capability
|
|
1471
|
+
with ZERO user action:
|
|
1472
|
+
1. embed facts that were never embedded (NULL embedding column),
|
|
1473
|
+
2. backfill key-expansion alt-keys (BM25 recall aid) via one
|
|
1474
|
+
bounded maintenance pass per profile,
|
|
1475
|
+
3. index everything (including the just-embedded facts) into the
|
|
1476
|
+
sqlite-vec store.
|
|
1477
|
+
Fully non-blocking (daemon thread) — recall keeps serving throughout.
|
|
1478
|
+
Every step is bounded + idempotent, so on an already-complete DB the
|
|
1479
|
+
whole pass is a fast no-op. Progress is exposed at /status.self_heal
|
|
1480
|
+
so the dashboard can show a plain-language "Optimizing memory…" line.
|
|
1481
|
+
"""
|
|
1482
|
+
import time as _t
|
|
1483
|
+
global _SELF_HEAL_STATUS
|
|
1484
|
+
_SELF_HEAL_STATUS = {
|
|
1485
|
+
"state": "checking_components", "embeddings_backfilled": 0,
|
|
1486
|
+
"expansion_backfilled": 0, "null_remaining": None,
|
|
1487
|
+
"components": None,
|
|
1488
|
+
"started_at": _t.time(), "finished_at": None,
|
|
1489
|
+
}
|
|
1490
|
+
# Step 0 (v3.8.2 "whole self-healer"): repair components that
|
|
1491
|
+
# silently failed to install from the internet — BEFORE waiting on
|
|
1492
|
+
# the embedder, since a missing embedding model would make that wait
|
|
1493
|
+
# pointless. Downloads missing HF models (embedder/reranker, only
|
|
1494
|
+
# when torch is present) and pip-installs sqlite-vec when the
|
|
1495
|
+
# interpreter is user-writable. Bounded, fail-open, never sudo,
|
|
1496
|
+
# never auto-pulls Ollama. Manual-only items are recorded for the
|
|
1497
|
+
# dashboard "what's missing" report (GET /api/v3/components), not
|
|
1498
|
+
# acted on here.
|
|
1499
|
+
try:
|
|
1500
|
+
from superlocalmemory.core import component_healer
|
|
1501
|
+
heal_res = component_healer.heal_missing(
|
|
1502
|
+
config,
|
|
1503
|
+
on_progress=lambda k, m: logger.info(
|
|
1504
|
+
"Self-heal component[%s]: %s", k, m,
|
|
1505
|
+
),
|
|
1506
|
+
)
|
|
1507
|
+
_SELF_HEAL_STATUS["components"] = heal_res
|
|
1508
|
+
if heal_res["attempted"]:
|
|
1509
|
+
logger.info("Self-heal components: %s", heal_res)
|
|
1510
|
+
except Exception as exc:
|
|
1511
|
+
logger.warning(
|
|
1512
|
+
"Self-heal component check failed (non-fatal): %s", exc,
|
|
1513
|
+
)
|
|
1514
|
+
_SELF_HEAL_STATUS["state"] = "waiting_embedder"
|
|
1515
|
+
for _ in range(120): # up to ~60s for the embedder to warm
|
|
1516
|
+
if _embedding_warm:
|
|
1517
|
+
break
|
|
1518
|
+
_t.sleep(0.5)
|
|
1519
|
+
try:
|
|
1520
|
+
embedder = getattr(retrieval_eng, "_embedder", None) if retrieval_eng else None
|
|
1521
|
+
db = engine._db
|
|
1522
|
+
if embedder is None or db is None:
|
|
1523
|
+
_SELF_HEAL_STATUS["state"] = "skipped_no_embedder"
|
|
1524
|
+
return
|
|
1525
|
+
# 1) Embed never-embedded facts (all profiles, the upgrade
|
|
1526
|
+
# headline). Looped: backfill is idempotent + bounded, and the
|
|
1527
|
+
# shared embedding worker can transiently return None under
|
|
1528
|
+
# startup contention (concurrent recall-warmup). Retrying until
|
|
1529
|
+
# the NULL count stops shrinking makes the heal converge
|
|
1530
|
+
# robustly rather than abandoning on one transient miss.
|
|
1531
|
+
# Facts that never embed (e.g. a document far over the model's
|
|
1532
|
+
# token limit) are left as-is after a bounded number of no-progress
|
|
1533
|
+
# attempts. No-op when there are no NULLs.
|
|
1534
|
+
_SELF_HEAL_STATUS["state"] = "backfilling_embeddings"
|
|
1535
|
+
try:
|
|
1536
|
+
from superlocalmemory.storage.embedding_migrator import (
|
|
1537
|
+
backfill_missing_embeddings,
|
|
1538
|
+
)
|
|
1539
|
+
# RECALL-PRIORITY THROTTLE: the embedding worker is a single
|
|
1540
|
+
# serialized subprocess shared with foreground recall. A
|
|
1541
|
+
# continuous backfill starves interactive query-embedding and
|
|
1542
|
+
# recalls time out. So: (a) tiny batches (short worker holds),
|
|
1543
|
+
# and (b) before each batch, defer while ANY user recall is in
|
|
1544
|
+
# flight — reusing the same recall_gate the pending materializer
|
|
1545
|
+
# uses. This keeps recall responsive throughout the heal (the
|
|
1546
|
+
# zero-pain requirement); the heal just takes a little longer.
|
|
1547
|
+
from superlocalmemory.core import recall_gate
|
|
1548
|
+
total_embedded = 0
|
|
1549
|
+
no_progress = 0
|
|
1550
|
+
for _attempt in range(500):
|
|
1551
|
+
# Absolute priority to user recalls: pause the heal while
|
|
1552
|
+
# a recall is active (bounded wait so we never wedge).
|
|
1553
|
+
_waited = 0.0
|
|
1554
|
+
while recall_gate.in_flight() > 0 and _waited < 30.0:
|
|
1555
|
+
_t.sleep(0.5)
|
|
1556
|
+
_waited += 0.5
|
|
1557
|
+
r = backfill_missing_embeddings(
|
|
1558
|
+
config, db, embedder, limit=5, all_profiles=True,
|
|
1559
|
+
)
|
|
1560
|
+
got = r.get("embedded", 0)
|
|
1561
|
+
total_embedded += got
|
|
1562
|
+
_SELF_HEAL_STATUS["embeddings_backfilled"] = total_embedded
|
|
1563
|
+
_SELF_HEAL_STATUS["null_remaining"] = r.get("remaining_null", 0)
|
|
1564
|
+
if r.get("remaining_null", 0) == 0:
|
|
1565
|
+
break
|
|
1566
|
+
if got == 0:
|
|
1567
|
+
no_progress += 1
|
|
1568
|
+
if no_progress >= 5: # transient recovery exhausted
|
|
1569
|
+
break
|
|
1570
|
+
_t.sleep(3) # let the worker settle, then retry
|
|
1571
|
+
else:
|
|
1572
|
+
no_progress = 0
|
|
1573
|
+
_t.sleep(0.5) # brief pause between bursts
|
|
1574
|
+
if total_embedded:
|
|
1575
|
+
logger.info(
|
|
1576
|
+
"Self-heal: embedded %d previously-unembedded facts "
|
|
1577
|
+
"(%d remaining)", total_embedded,
|
|
1578
|
+
_SELF_HEAL_STATUS["null_remaining"],
|
|
1579
|
+
)
|
|
1580
|
+
except Exception as exc:
|
|
1581
|
+
logger.warning("Self-heal embedding backfill failed (non-fatal): %s", exc)
|
|
1582
|
+
# 2) Math/key-expansion maintenance is intentionally NOT run here:
|
|
1583
|
+
# run_maintenance triggers a full Langevin backfill over every
|
|
1584
|
+
# fact, which on a large legacy DB takes minutes and its CPU
|
|
1585
|
+
# burst inflates foreground recall latency during the heal
|
|
1586
|
+
# window. The startup heal stays lean (embeddings + vector
|
|
1587
|
+
# index — what makes facts findable again). Langevin/Sheaf/
|
|
1588
|
+
# key-expansion continue to converge on the periodic
|
|
1589
|
+
# MaintenanceScheduler exactly as before — unchanged behavior.
|
|
1590
|
+
# 3) Index everything (incl. newly-embedded) into the vector store.
|
|
1591
|
+
_SELF_HEAL_STATUS["state"] = "indexing_vectors"
|
|
1592
|
+
try:
|
|
1593
|
+
_backfill_vector_store()
|
|
1594
|
+
except Exception as exc:
|
|
1595
|
+
logger.warning("Self-heal vector index failed (non-fatal): %s", exc)
|
|
1596
|
+
_SELF_HEAL_STATUS["state"] = "complete"
|
|
1597
|
+
_SELF_HEAL_STATUS["finished_at"] = _t.time()
|
|
1598
|
+
logger.info("Self-heal complete: %s", _SELF_HEAL_STATUS)
|
|
1599
|
+
except Exception as exc:
|
|
1600
|
+
_SELF_HEAL_STATUS["state"] = "error"
|
|
1601
|
+
logger.warning("Self-heal failed (non-fatal): %s", exc)
|
|
1602
|
+
|
|
1603
|
+
def _component_recheck_loop():
|
|
1604
|
+
"""Periodic component re-check (v3.8.2 "whole self-healer").
|
|
1605
|
+
|
|
1606
|
+
The startup heal (_self_heal Step 0) runs once. This keeps the
|
|
1607
|
+
self-healer promise for a long-running daemon: it re-probes
|
|
1608
|
+
components on a slow cadence and auto-repairs any that regress
|
|
1609
|
+
(a cache eviction, a half-finished install). No-op on a healthy
|
|
1610
|
+
machine — nothing is auto-fixable-missing, so it is just a cheap
|
|
1611
|
+
probe. Disable with SLM_COMPONENT_RECHECK_SEC=0.
|
|
1612
|
+
"""
|
|
1613
|
+
import os as _os
|
|
1614
|
+
import time as _t
|
|
1615
|
+
try:
|
|
1616
|
+
cadence = int(_os.environ.get("SLM_COMPONENT_RECHECK_SEC", "1800"))
|
|
1617
|
+
except ValueError:
|
|
1618
|
+
cadence = 1800
|
|
1619
|
+
if cadence <= 0:
|
|
1620
|
+
return
|
|
1621
|
+
# Let the startup heal + warmup settle before the first re-check.
|
|
1622
|
+
_t.sleep(max(cadence, 300))
|
|
1623
|
+
while True:
|
|
1624
|
+
try:
|
|
1625
|
+
from superlocalmemory.core import component_healer
|
|
1626
|
+
res = component_healer.heal_missing(
|
|
1627
|
+
config,
|
|
1628
|
+
on_progress=lambda k, m: logger.info(
|
|
1629
|
+
"Component re-check[%s]: %s", k, m,
|
|
1630
|
+
),
|
|
1631
|
+
)
|
|
1632
|
+
if res["attempted"]:
|
|
1633
|
+
logger.info("Component re-check repaired: %s", res)
|
|
1634
|
+
except Exception as exc:
|
|
1635
|
+
logger.debug("Component re-check failed (non-fatal): %s", exc)
|
|
1636
|
+
_t.sleep(cadence)
|
|
1637
|
+
|
|
1451
1638
|
threading.Thread(target=_warmup_embedder, daemon=True, name="embed-warmup").start()
|
|
1452
1639
|
threading.Thread(target=_warmup_recall, daemon=True, name="recall-warmup").start()
|
|
1453
|
-
|
|
1640
|
+
# v3.8.2: self-heal supersedes the bare vector-store backfill (it calls
|
|
1641
|
+
# _backfill_vector_store itself, after embedding + expansion heal).
|
|
1642
|
+
threading.Thread(target=_self_heal, daemon=True, name="self-heal").start()
|
|
1643
|
+
threading.Thread(
|
|
1644
|
+
target=_component_recheck_loop, daemon=True, name="component-recheck",
|
|
1645
|
+
).start()
|
|
1454
1646
|
|
|
1455
1647
|
# v3.6.8: Runtime recall-health monitor. The three warmups above run
|
|
1456
1648
|
# ONCE at boot; on a long-running daemon the graph page cache gets
|
|
@@ -2761,7 +2953,15 @@ def _register_daemon_routes(application: FastAPI) -> None:
|
|
|
2761
2953
|
request: Request,
|
|
2762
2954
|
q: str = "", query: str = "", limit: int = CANONICAL_RECALL_LIMIT,
|
|
2763
2955
|
session_id: str = "",
|
|
2764
|
-
|
|
2956
|
+
# v3.8.2 client-driven agentic: ``fast`` is left UNSET (None) by default
|
|
2957
|
+
# so the daemon resolves the configured policy (retrieval.client_driven_agentic,
|
|
2958
|
+
# ships True). The agent hot path is consumed by a frontier LLM that
|
|
2959
|
+
# reformulates queries far better than the local Ollama model, so it
|
|
2960
|
+
# skips the internal agentic round and returns fast local retrieval (all
|
|
2961
|
+
# six channels + reranker) plus confidence signals; the client re-queries
|
|
2962
|
+
# on low confidence. An explicit ?fast=true / ?fast=false always wins.
|
|
2963
|
+
# See recall_pipeline.resolve_hot_path_fast.
|
|
2964
|
+
fast: bool | None = None,
|
|
2765
2965
|
full: bool = False,
|
|
2766
2966
|
include_source: bool = False,
|
|
2767
2967
|
include_global: bool | None = None,
|
|
@@ -2773,6 +2973,10 @@ def _register_daemon_routes(application: FastAPI) -> None:
|
|
|
2773
2973
|
engine = _get_engine_or_503()
|
|
2774
2974
|
if not search_query:
|
|
2775
2975
|
return {"results": [], "count": 0, "query_type": "none", "retrieval_time_ms": 0}
|
|
2976
|
+
# v3.8.2: resolve the client-driven-agentic default now so the concrete
|
|
2977
|
+
# bool drives BOTH the full-recall semaphore below and engine.recall().
|
|
2978
|
+
from superlocalmemory.core.recall_pipeline import resolve_hot_path_fast
|
|
2979
|
+
fast = resolve_hot_path_fast(fast, engine._config)
|
|
2776
2980
|
# S9-DASH-02: session_id for the outcome-queue producer.
|
|
2777
2981
|
# Priority: ?session_id= > X-SLM-Session-Id header > synthetic
|
|
2778
2982
|
# "http:<ts>". Without a session_id the recall still works
|
|
@@ -3178,8 +3382,68 @@ def _register_daemon_routes(application: FastAPI) -> None:
|
|
|
3178
3382
|
"legacy_port": _LEGACY_PORT,
|
|
3179
3383
|
"profile": profile_snapshot.profile_id,
|
|
3180
3384
|
"profile_generation": profile_snapshot.generation,
|
|
3385
|
+
# F2 fix: expose M028 backfill progress so operators can monitor
|
|
3386
|
+
# the post-upgrade fact/entity association repair state.
|
|
3387
|
+
"m028_backfill": getattr(
|
|
3388
|
+
application.state,
|
|
3389
|
+
"fact_entity_association_repair_status",
|
|
3390
|
+
None,
|
|
3391
|
+
),
|
|
3392
|
+
# v3.8.2: zero-pain self-heal progress (embeddings/expansion/vector
|
|
3393
|
+
# index backfill after an upgrade). Dashboard renders a plain
|
|
3394
|
+
# "Optimizing memory…" line from this. Defaults to idle before start.
|
|
3395
|
+
"self_heal": globals().get("_SELF_HEAL_STATUS", {"state": "idle"}),
|
|
3181
3396
|
}
|
|
3182
3397
|
|
|
3398
|
+
@application.get("/api/v3/components")
|
|
3399
|
+
async def components():
|
|
3400
|
+
"""Component / dependency health (v3.8.2).
|
|
3401
|
+
|
|
3402
|
+
Read-only snapshot from the central registry (core.component_registry)
|
|
3403
|
+
— the same source the self-heal thread acts on. Powers the dashboard
|
|
3404
|
+
'what's missing' report and `slm doctor`. Includes live 'retrying'
|
|
3405
|
+
overlays while a background repair is in flight. Never mutates state.
|
|
3406
|
+
"""
|
|
3407
|
+
_update_activity()
|
|
3408
|
+
try:
|
|
3409
|
+
from superlocalmemory.core import component_registry
|
|
3410
|
+
cfg = getattr(application.state, "config", None)
|
|
3411
|
+
return component_registry.snapshot(cfg)
|
|
3412
|
+
except Exception as exc:
|
|
3413
|
+
raise HTTPException(500, detail=str(exc))
|
|
3414
|
+
|
|
3415
|
+
@application.post("/api/v3/components/heal")
|
|
3416
|
+
async def heal_components(request: Request):
|
|
3417
|
+
"""Trigger a component self-heal pass (dashboard 'Retry now').
|
|
3418
|
+
|
|
3419
|
+
Repairs auto-fixable missing components (re-download missing models,
|
|
3420
|
+
install sqlite-vec). Runs in a background thread and returns
|
|
3421
|
+
immediately so the HTTP request never blocks on a multi-minute
|
|
3422
|
+
download; the dashboard polls GET /api/v3/components to watch the
|
|
3423
|
+
'retrying' → 'ok' transition. Safe to call repeatedly (no-op when
|
|
3424
|
+
healthy). Requires the dashboard write principal, like other
|
|
3425
|
+
dashboard mutations.
|
|
3426
|
+
"""
|
|
3427
|
+
_require_write_actor(request)
|
|
3428
|
+
_update_activity()
|
|
3429
|
+
cfg = getattr(application.state, "config", None)
|
|
3430
|
+
|
|
3431
|
+
def _run():
|
|
3432
|
+
try:
|
|
3433
|
+
from superlocalmemory.core import component_healer
|
|
3434
|
+
res = component_healer.heal_missing(
|
|
3435
|
+
cfg,
|
|
3436
|
+
on_progress=lambda k, m: logger.info(
|
|
3437
|
+
"Manual heal[%s]: %s", k, m,
|
|
3438
|
+
),
|
|
3439
|
+
)
|
|
3440
|
+
logger.info("Manual component heal: %s", res)
|
|
3441
|
+
except Exception as exc:
|
|
3442
|
+
logger.warning("Manual component heal failed (non-fatal): %s", exc)
|
|
3443
|
+
|
|
3444
|
+
threading.Thread(target=_run, daemon=True, name="manual-heal").start()
|
|
3445
|
+
return {"status": "started"}
|
|
3446
|
+
|
|
3183
3447
|
@application.get("/list")
|
|
3184
3448
|
async def list_facts(limit: int = 50):
|
|
3185
3449
|
_update_activity()
|