superlocalmemory 4.0.5 → 4.0.6
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 +57 -0
- package/README.md +10 -6
- package/package.json +3 -1
- package/pyproject.toml +1 -1
- package/src/superlocalmemory/__init__.py +1 -1
- package/src/superlocalmemory/access/rbac.py +106 -0
- package/src/superlocalmemory/brain/truth.py +80 -10
- package/src/superlocalmemory/cli/__main__.py +17 -0
- package/src/superlocalmemory/cli/commands.py +14 -3
- package/src/superlocalmemory/cli/gdpr_cmd.py +779 -0
- package/src/superlocalmemory/cli/gdpr_io.py +109 -0
- package/src/superlocalmemory/cli/main.py +76 -0
- package/src/superlocalmemory/code_graph/extractors/__init__.py +17 -0
- package/src/superlocalmemory/code_graph/graph_store.py +180 -3
- package/src/superlocalmemory/code_graph/parser.py +280 -100
- package/src/superlocalmemory/compliance/gdpr.py +358 -0
- package/src/superlocalmemory/core/config.py +44 -1
- package/src/superlocalmemory/core/engine_wiring.py +5 -1
- package/src/superlocalmemory/core/maintenance.py +43 -1
- package/src/superlocalmemory/core/recall_worker.py +33 -12
- package/src/superlocalmemory/infra/backup.py +138 -0
- package/src/superlocalmemory/infra/backup_obligations.py +423 -0
- package/src/superlocalmemory/learning/engagement.py +165 -0
- package/src/superlocalmemory/mcp/tools_code_graph.py +31 -4
- package/src/superlocalmemory/mcp/tools_v3.py +20 -6
- package/src/superlocalmemory/retrieval/engine.py +21 -0
- package/src/superlocalmemory/retrieval/remote_reranker.py +108 -11
- package/src/superlocalmemory/server/routes/brain.py +283 -15
- package/src/superlocalmemory/server/routes/learning.py +13 -25
- package/src/superlocalmemory/server/routes/v3_api.py +171 -60
- package/src/superlocalmemory/storage/database.py +36 -0
- package/src/superlocalmemory/storage/models.py +12 -4
- package/src/superlocalmemory/summaries/__init__.py +37 -0
- package/src/superlocalmemory/summaries/base.py +108 -0
- package/src/superlocalmemory/summaries/daily_reflection.py +293 -0
- package/src/superlocalmemory/summaries/project_work_log.py +424 -0
- package/src/superlocalmemory/summaries/session_summary.py +307 -0
- package/src/superlocalmemory/ui/css/design-system.css +76 -1
- package/src/superlocalmemory/ui/index.html +28 -11
- package/src/superlocalmemory/ui/js/od-agents.js +49 -5
- package/src/superlocalmemory/ui/js/od-brain.js +257 -77
- package/src/superlocalmemory/ui/js/od-graph.js +147 -6
|
@@ -44,25 +44,31 @@ Design notes (LLD-04 §7 hard rules):
|
|
|
44
44
|
|
|
45
45
|
from __future__ import annotations
|
|
46
46
|
|
|
47
|
+
import json as _json
|
|
47
48
|
import logging
|
|
49
|
+
import shutil
|
|
48
50
|
import sqlite3
|
|
51
|
+
import subprocess
|
|
52
|
+
import threading
|
|
53
|
+
import time
|
|
49
54
|
from datetime import datetime, timedelta, timezone
|
|
50
55
|
from pathlib import Path
|
|
51
56
|
from typing import Any
|
|
52
57
|
|
|
53
58
|
from fastapi import APIRouter, Depends, HTTPException, Request
|
|
59
|
+
|
|
54
60
|
from superlocalmemory import __version__
|
|
55
61
|
from superlocalmemory.brain import BrainTruthService
|
|
56
|
-
|
|
57
62
|
from superlocalmemory.core.security_primitives import (
|
|
58
63
|
redact_secrets,
|
|
59
64
|
verify_install_token,
|
|
60
65
|
)
|
|
66
|
+
from superlocalmemory.infra.data_root import canonical_data_root
|
|
61
67
|
from superlocalmemory.learning.database import LearningDatabase
|
|
62
68
|
from superlocalmemory.learning.features import FEATURE_DIM
|
|
63
|
-
from superlocalmemory.infra.data_root import canonical_data_root
|
|
64
|
-
from superlocalmemory.storage.read_connection import ReadConnectionFactory
|
|
65
69
|
from superlocalmemory.storage.agent_experience import get_profile_receipt_summary
|
|
70
|
+
from superlocalmemory.storage.read_connection import ReadConnectionFactory
|
|
71
|
+
|
|
66
72
|
from .helpers import get_active_profile
|
|
67
73
|
|
|
68
74
|
logger = logging.getLogger("superlocalmemory.routes.brain")
|
|
@@ -84,6 +90,17 @@ _VERSION: str = __version__
|
|
|
84
90
|
# NOTE: do NOT add the literal forbidden-key strings here — the U4 grep
|
|
85
91
|
# guard runs over this file.
|
|
86
92
|
|
|
93
|
+
# ---------------------------------------------------------------------------
|
|
94
|
+
# Bounded-loops detection — TTL-cached subprocess probe.
|
|
95
|
+
# The binary lives in a pipx venv; importlib.util.find_spec('bounded_loops')
|
|
96
|
+
# will NOT find it from our venv. Detect via PATH / filesystem instead.
|
|
97
|
+
# ---------------------------------------------------------------------------
|
|
98
|
+
|
|
99
|
+
_BL_PROBE: dict[str, object] = {}
|
|
100
|
+
_BL_PROBE_LOCK = threading.Lock()
|
|
101
|
+
_BL_PROBE_TTL = 60.0 # seconds between re-probes
|
|
102
|
+
|
|
103
|
+
|
|
87
104
|
# Memory directory (home-dir based). Always resolved at call time so that
|
|
88
105
|
# tests can override via monkeypatch on ``_learning_db_path``.
|
|
89
106
|
_MEMORY_DIR_DEFAULT = None # test-only compatibility override
|
|
@@ -596,7 +613,8 @@ def _adapter_last_sync_ago(adapter_name: str) -> int | None:
|
|
|
596
613
|
honest empty rather than a fabricated number.
|
|
597
614
|
"""
|
|
598
615
|
try:
|
|
599
|
-
from datetime import datetime as _dt
|
|
616
|
+
from datetime import datetime as _dt
|
|
617
|
+
from datetime import timezone as _tz
|
|
600
618
|
memory_db = _memory_dir() / "memory.db"
|
|
601
619
|
if not memory_db.exists():
|
|
602
620
|
return None
|
|
@@ -620,6 +638,166 @@ def _adapter_last_sync_ago(adapter_name: str) -> int | None:
|
|
|
620
638
|
return None
|
|
621
639
|
|
|
622
640
|
|
|
641
|
+
def _detect_codex_config() -> dict:
|
|
642
|
+
"""Detect Codex integration from ``~/.codex/config.toml``.
|
|
643
|
+
|
|
644
|
+
Evidence tier: CONFIGURED — the config file contains
|
|
645
|
+
``[mcp_servers.superlocalmemory]``, which proves Codex is wired to use
|
|
646
|
+
SLM as an MCP server. This is the same tier as Claude Code's install-
|
|
647
|
+
token check: file presence, not live traffic.
|
|
648
|
+
|
|
649
|
+
What we deliberately do NOT claim: that Codex is currently running, or
|
|
650
|
+
that the MCP connection has been used recently. For live-traffic
|
|
651
|
+
evidence the correct signal is ``tool_events`` rows attributed to
|
|
652
|
+
``SLM_AGENT_ID='codex'`` — a check not performed here because
|
|
653
|
+
``tool_events`` has no ``agent_id`` column.
|
|
654
|
+
"""
|
|
655
|
+
try:
|
|
656
|
+
codex_config = Path.home() / ".codex" / "config.toml"
|
|
657
|
+
if not codex_config.exists():
|
|
658
|
+
return {"active": False, "reason": "codex_config_absent"}
|
|
659
|
+
try:
|
|
660
|
+
content = codex_config.read_text(encoding="utf-8", errors="replace")
|
|
661
|
+
except OSError:
|
|
662
|
+
return {"active": False, "reason": "codex_config_unreadable"}
|
|
663
|
+
if "[mcp_servers.superlocalmemory]" not in content:
|
|
664
|
+
return {"active": False, "reason": "no_slm_mcp_block"}
|
|
665
|
+
return {
|
|
666
|
+
"active": True,
|
|
667
|
+
"evidence_tier": "configured",
|
|
668
|
+
"evidence": "~/.codex/config.toml#[mcp_servers.superlocalmemory]",
|
|
669
|
+
}
|
|
670
|
+
except Exception: # pragma: no cover — defensive
|
|
671
|
+
return {"active": False, "reason": "detection_error"}
|
|
672
|
+
|
|
673
|
+
|
|
674
|
+
def _bounded_loops_probe_fresh() -> dict:
|
|
675
|
+
"""Probe bounded-loops installation — may spawn one short subprocess.
|
|
676
|
+
|
|
677
|
+
Detection approach: ``shutil.which("bounded-loops-mcp")`` (PATH scan,
|
|
678
|
+
no subprocess), then ``bl --version`` with a 1.5 s timeout (see the note at the call
|
|
679
|
+
site: 300 ms sat too close to the measured ~226 ms and flickered).
|
|
680
|
+
|
|
681
|
+
Evidence tier: INSTALLED — the ``bounded-loops-mcp`` binary is on the
|
|
682
|
+
resolved PATH, meaning the pipx package is present.
|
|
683
|
+
|
|
684
|
+
What we do NOT claim: that the bridge contract ``bounded-loops.dev/
|
|
685
|
+
slm-bridge/v1`` is live or that any run has been observed. A full
|
|
686
|
+
capability handshake requires an async MCP round-trip; that is outside
|
|
687
|
+
the scope of this synchronous probe.
|
|
688
|
+
|
|
689
|
+
Trap: do NOT use ``importlib.util.find_spec('bounded_loops')`` — the
|
|
690
|
+
package lives in its own pipx venv and is not importable from this one.
|
|
691
|
+
"""
|
|
692
|
+
bl_mcp_path = shutil.which("bounded-loops-mcp")
|
|
693
|
+
if bl_mcp_path is None:
|
|
694
|
+
# Cross-check: ~/.bounded-loops data dir (pipx installs leave this).
|
|
695
|
+
bl_data = Path.home() / ".bounded-loops"
|
|
696
|
+
if bl_data.is_dir():
|
|
697
|
+
return {
|
|
698
|
+
"installed": True,
|
|
699
|
+
"version": None,
|
|
700
|
+
"bridge_contract": "bounded-loops.dev/slm-bridge/v1",
|
|
701
|
+
"evidence_tier": "data_dir",
|
|
702
|
+
"evidence": "~/.bounded-loops exists",
|
|
703
|
+
"note": "mcp binary not on PATH; data dir detected",
|
|
704
|
+
"is_real": True,
|
|
705
|
+
"source": "filesystem",
|
|
706
|
+
}
|
|
707
|
+
return {
|
|
708
|
+
"installed": False,
|
|
709
|
+
"reason": "bounded_loops_mcp_not_in_path",
|
|
710
|
+
"is_real": True,
|
|
711
|
+
"source": "shutil.which",
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
version: str | None = None
|
|
715
|
+
bl_path = shutil.which("bl")
|
|
716
|
+
if bl_path:
|
|
717
|
+
try:
|
|
718
|
+
# 1.5s, not 0.3s. Measured on this machine: a cold
|
|
719
|
+
# _compute_bounded_loops() takes ~226 ms end to end, most of it this
|
|
720
|
+
# subprocess — a 300 ms budget leaves ~74 ms of headroom, so under
|
|
721
|
+
# any load the probe times out and the card silently reports
|
|
722
|
+
# "Version: unknown" while `bl --version` prints "bl 0.6.4" from a
|
|
723
|
+
# shell. A version string that flickers with machine load is worse
|
|
724
|
+
# than no version string, because it looks like a real finding.
|
|
725
|
+
# The cost of widening is bounded: this runs in a worker thread via
|
|
726
|
+
# asyncio.to_thread and is TTL-cached for 60s, so the worst case is
|
|
727
|
+
# one 1.5s thread once a minute, never on the recall/remember path.
|
|
728
|
+
proc = subprocess.run(
|
|
729
|
+
[bl_path, "--version"],
|
|
730
|
+
capture_output=True,
|
|
731
|
+
text=True,
|
|
732
|
+
timeout=1.5,
|
|
733
|
+
)
|
|
734
|
+
raw = (proc.stdout.strip() or proc.stderr.strip())
|
|
735
|
+
if raw.startswith("bl "):
|
|
736
|
+
version = raw[3:].strip()
|
|
737
|
+
except Exception: # pragma: no cover — timeout or missing binary
|
|
738
|
+
pass
|
|
739
|
+
|
|
740
|
+
return {
|
|
741
|
+
"installed": True,
|
|
742
|
+
"version": version,
|
|
743
|
+
"bridge_contract": "bounded-loops.dev/slm-bridge/v1",
|
|
744
|
+
"evidence_tier": "path",
|
|
745
|
+
"evidence": "shutil.which(bounded-loops-mcp)",
|
|
746
|
+
"note": "bridge capability requires a runtime MCP handshake; not probed here",
|
|
747
|
+
"is_real": True,
|
|
748
|
+
"source": "shutil.which + bl --version",
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
|
|
752
|
+
def _compute_bounded_loops() -> dict:
|
|
753
|
+
"""Section ``bounded_loops`` — installation/presence status.
|
|
754
|
+
|
|
755
|
+
Wraps ``_bounded_loops_probe_fresh`` in a 60-second TTL cache so the
|
|
756
|
+
subprocess call is never inline on every Brain request. First call may
|
|
757
|
+
take up to 1.5 s (subprocess timeout); subsequent calls within the TTL
|
|
758
|
+
window return immediately from the cache. The probe is single-flight, so
|
|
759
|
+
concurrent callers share one subprocess rather than spawning one each.
|
|
760
|
+
"""
|
|
761
|
+
with _BL_PROBE_LOCK:
|
|
762
|
+
cached = _BL_PROBE.get("result")
|
|
763
|
+
cached_at = float(_BL_PROBE.get("cached_at", 0.0))
|
|
764
|
+
if cached is not None and (time.monotonic() - cached_at) < _BL_PROBE_TTL:
|
|
765
|
+
return cached # type: ignore[return-value]
|
|
766
|
+
|
|
767
|
+
# SINGLE-FLIGHT: run the probe while STILL HOLDING the lock.
|
|
768
|
+
# The previous shape released the lock between the check and the probe,
|
|
769
|
+
# so every concurrent caller that arrived after a TTL expiry spawned its
|
|
770
|
+
# own `bl --version` subprocess — N dashboard requests meant N processes,
|
|
771
|
+
# each up to 1.5s. Holding the lock makes the 2nd..Nth caller wait for
|
|
772
|
+
# the first result instead. Blocking here is safe: this whole function
|
|
773
|
+
# runs in a worker thread via asyncio.to_thread, so the event loop is
|
|
774
|
+
# never held, and it is off the recall/remember path entirely.
|
|
775
|
+
try:
|
|
776
|
+
fresh = _bounded_loops_probe_fresh()
|
|
777
|
+
fresh["section_enabled"] = bool(fresh.get("installed", False))
|
|
778
|
+
except Exception as exc: # probe itself blew up
|
|
779
|
+
# Cache the FAILURE too, and label it as a failure rather than as a
|
|
780
|
+
# clean "not installed". Without caching it, every request would
|
|
781
|
+
# retry a broken probe forever; without labelling it, a broken probe
|
|
782
|
+
# is indistinguishable from Bounded Loops genuinely being absent —
|
|
783
|
+
# the silent-failure class this release exists to remove.
|
|
784
|
+
logger.warning("bounded-loops probe failed: %s", exc)
|
|
785
|
+
fresh = {
|
|
786
|
+
"installed": None,
|
|
787
|
+
"is_real": False,
|
|
788
|
+
"probe_failed": True,
|
|
789
|
+
"reason": f"probe_error:{type(exc).__name__}",
|
|
790
|
+
"section_enabled": False,
|
|
791
|
+
"source": "probe raised",
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
# Timestamp AFTER the probe so the TTL measures time since completion,
|
|
795
|
+
# not time since the request arrived.
|
|
796
|
+
_BL_PROBE["result"] = fresh
|
|
797
|
+
_BL_PROBE["cached_at"] = time.monotonic()
|
|
798
|
+
return fresh
|
|
799
|
+
|
|
800
|
+
|
|
623
801
|
def _compute_cross_platform() -> dict:
|
|
624
802
|
"""Section ``cross_platform`` — live status per injection target.
|
|
625
803
|
|
|
@@ -700,26 +878,93 @@ def _compute_cross_platform() -> dict:
|
|
|
700
878
|
out["mcp"] = {"active": True, "tool": "mcp__slm"}
|
|
701
879
|
# CLI is trivially active on any install.
|
|
702
880
|
out["cli"] = {"active": True}
|
|
881
|
+
# Codex: detect from ~/.codex/config.toml.
|
|
882
|
+
# evidence_tier="configured" — config proves SLM is wired; it does not
|
|
883
|
+
# prove recent MCP traffic (tool_events has no agent_id column).
|
|
884
|
+
out["codex"] = _detect_codex_config()
|
|
703
885
|
return out
|
|
704
886
|
|
|
705
887
|
|
|
888
|
+
def _registry_staleness() -> tuple[str, int | None]:
|
|
889
|
+
"""Return (registry_status, newest_entry_seconds_ago).
|
|
890
|
+
|
|
891
|
+
Reads the raw session registry file to determine whether the registry
|
|
892
|
+
has been written to recently, regardless of profile matching. This
|
|
893
|
+
distinguishes three silent-but-different states:
|
|
894
|
+
|
|
895
|
+
``live`` — newest entry is within 2× the presence window (10 min)
|
|
896
|
+
``stale`` — file exists, entries exist, but all are older than 10 min
|
|
897
|
+
→ TELEMETRY GAP: hooks exist but are not firing
|
|
898
|
+
``empty`` — file exists but has no entries
|
|
899
|
+
``absent`` — file does not exist (first-run or cleaned up)
|
|
900
|
+
``unknown`` — file could not be read
|
|
901
|
+
|
|
902
|
+
IMPORTANT: the window is NOT widened here. We report the gap honestly
|
|
903
|
+
rather than hiding it by using a larger cutoff on the query.
|
|
904
|
+
"""
|
|
905
|
+
try:
|
|
906
|
+
from superlocalmemory.infra.data_root import state_path
|
|
907
|
+
registry_path = state_path(".active_sessions.json")
|
|
908
|
+
if not registry_path.exists():
|
|
909
|
+
return "absent", None
|
|
910
|
+
try:
|
|
911
|
+
raw = registry_path.read_text(encoding="utf-8")
|
|
912
|
+
data = _json.loads(raw)
|
|
913
|
+
except Exception:
|
|
914
|
+
return "unknown", None
|
|
915
|
+
if not isinstance(data, dict) or not data:
|
|
916
|
+
return "empty", None
|
|
917
|
+
now_ns = time.time_ns()
|
|
918
|
+
ts_vals = [
|
|
919
|
+
int(row.get("ts_ns", 0))
|
|
920
|
+
for row in data.values()
|
|
921
|
+
if isinstance(row, dict) and int(row.get("ts_ns", 0)) > 0
|
|
922
|
+
]
|
|
923
|
+
if not ts_vals:
|
|
924
|
+
return "empty", None
|
|
925
|
+
newest_ns = max(ts_vals)
|
|
926
|
+
seconds_ago = max(0, int((now_ns - newest_ns) / 1_000_000_000))
|
|
927
|
+
# 2× the 300 s presence window = 600 s sane boundary.
|
|
928
|
+
if seconds_ago <= 600:
|
|
929
|
+
return "live", seconds_ago
|
|
930
|
+
return "stale", seconds_ago
|
|
931
|
+
except Exception: # pragma: no cover — defensive
|
|
932
|
+
return "unknown", None
|
|
933
|
+
|
|
934
|
+
|
|
706
935
|
def _compute_active_clients(profile_id: str) -> dict:
|
|
707
|
-
"""Return recent host presence
|
|
936
|
+
"""Return recent host presence with honest telemetry-gap detection.
|
|
708
937
|
|
|
709
938
|
``cross_platform`` describes configured integration targets. This
|
|
710
|
-
separate read-model answers
|
|
711
|
-
|
|
939
|
+
separate read-model answers a different question: which host clients
|
|
940
|
+
have recently interacted with this brain?
|
|
941
|
+
|
|
942
|
+
Three distinct states are surfaced instead of a silent empty list:
|
|
943
|
+
``NO_ACTIVITY`` — registry is live; no clients in the 5-min window
|
|
944
|
+
``TELEMETRY_GAP`` — registry's newest entry is > 10 min old; hooks
|
|
945
|
+
may be installed but are not writing presence
|
|
946
|
+
``REGISTRY_ERROR`` — exception accessing the registry; reported as
|
|
947
|
+
``is_real: False`` rather than an empty success
|
|
712
948
|
"""
|
|
949
|
+
reg_status, newest_ago = _registry_staleness()
|
|
950
|
+
|
|
951
|
+
clients: list[dict] = []
|
|
952
|
+
registry_ok = True
|
|
713
953
|
try:
|
|
714
954
|
from superlocalmemory.hooks.session_registry import active_client_summary
|
|
715
955
|
clients = active_client_summary(profile_id, within_seconds=300)
|
|
716
|
-
except Exception:
|
|
717
|
-
|
|
956
|
+
except Exception as exc: # distinguish failure from emptiness (Wave 4)
|
|
957
|
+
registry_ok = False
|
|
958
|
+
reg_status = "error"
|
|
959
|
+
logger.debug("active_clients: registry error: %s", exc)
|
|
960
|
+
|
|
718
961
|
return {
|
|
719
|
-
"is_real":
|
|
962
|
+
"is_real": registry_ok,
|
|
720
963
|
"scope": "profile",
|
|
721
964
|
"window_seconds": 300,
|
|
722
965
|
"clients": clients,
|
|
966
|
+
"registry_status": reg_status,
|
|
967
|
+
"newest_entry_seconds_ago": newest_ago,
|
|
723
968
|
"source": "session_registry (ephemeral, profile-scoped)",
|
|
724
969
|
}
|
|
725
970
|
|
|
@@ -1140,7 +1385,7 @@ async def get_brain(request: Request, profile_id: str | None = None) -> dict:
|
|
|
1140
1385
|
preferences, learning, usage, bandit_snap, cache,
|
|
1141
1386
|
cross_platform, outcomes_preview, evolution, active_clients,
|
|
1142
1387
|
feedback_loop, source_quality, graph_summary, agent_experience,
|
|
1143
|
-
brain_truth,
|
|
1388
|
+
brain_truth, bounded_loops,
|
|
1144
1389
|
) = await asyncio.gather(
|
|
1145
1390
|
asyncio.to_thread(_compute_preferences, profile_id),
|
|
1146
1391
|
asyncio.to_thread(_compute_learning_status, profile_id, lrn_db),
|
|
@@ -1159,6 +1404,7 @@ async def get_brain(request: Request, profile_id: str | None = None) -> dict:
|
|
|
1159
1404
|
asyncio.to_thread(_compute_graph_summary, profile_id),
|
|
1160
1405
|
asyncio.to_thread(_compute_agent_experience, profile_id),
|
|
1161
1406
|
asyncio.to_thread(truth_service.snapshot, profile_id),
|
|
1407
|
+
asyncio.to_thread(_compute_bounded_loops),
|
|
1162
1408
|
return_exceptions=True,
|
|
1163
1409
|
)
|
|
1164
1410
|
|
|
@@ -1169,9 +1415,15 @@ async def get_brain(request: Request, profile_id: str | None = None) -> dict:
|
|
|
1169
1415
|
return fallback
|
|
1170
1416
|
return value
|
|
1171
1417
|
|
|
1172
|
-
active_clients = _ok(active_clients, {
|
|
1173
|
-
|
|
1174
|
-
|
|
1418
|
+
active_clients = _ok(active_clients, {
|
|
1419
|
+
"is_real": False,
|
|
1420
|
+
"scope": "profile",
|
|
1421
|
+
"clients": [],
|
|
1422
|
+
"window_seconds": 300,
|
|
1423
|
+
"registry_status": "error",
|
|
1424
|
+
"newest_entry_seconds_ago": None,
|
|
1425
|
+
"source": "session_registry unavailable",
|
|
1426
|
+
})
|
|
1175
1427
|
feedback_loop = _ok(feedback_loop, {"is_real": True, "signals_by_type": {},
|
|
1176
1428
|
"explicit_signals": 0, "implicit_signals": 0,
|
|
1177
1429
|
"settled_outcomes": 0, "mean_settled_reward": None,
|
|
@@ -1248,6 +1500,21 @@ async def get_brain(request: Request, profile_id: str | None = None) -> dict:
|
|
|
1248
1500
|
"shadow_preview": _compute_shadow_preview(profile_id),
|
|
1249
1501
|
"evolution_cost_preview": _compute_evolution_cost_preview(profile_id),
|
|
1250
1502
|
"outcome_queue": _compute_outcome_queue_stats(profile_id),
|
|
1503
|
+
# ``bounded_loops`` — lazy-probed, TTL-cached installation status.
|
|
1504
|
+
# ``section_enabled`` drives the UI panel on/off decision.
|
|
1505
|
+
# Evidence tier: path-based (shutil.which); bridge capability not probed.
|
|
1506
|
+
# is_real=False and installed=None, NOT installed=False. If the probe
|
|
1507
|
+
# task raised, we do not know whether Bounded Loops is installed — and
|
|
1508
|
+
# saying "installed: False, is_real: True" asserts absence we never
|
|
1509
|
+
# established. That reads to the UI, and to any API consumer, exactly
|
|
1510
|
+
# like a machine with Bounded Loops genuinely not present.
|
|
1511
|
+
"bounded_loops": _ok(bounded_loops, {
|
|
1512
|
+
"is_real": False,
|
|
1513
|
+
"installed": None,
|
|
1514
|
+
"probe_failed": True,
|
|
1515
|
+
"section_enabled": False,
|
|
1516
|
+
"source": "bounded_loops_probe unavailable",
|
|
1517
|
+
}),
|
|
1251
1518
|
"meta": _meta_now(),
|
|
1252
1519
|
}
|
|
1253
1520
|
|
|
@@ -1261,7 +1528,8 @@ def _compute_outcome_queue_stats(profile_id: str) -> dict:
|
|
|
1261
1528
|
"""
|
|
1262
1529
|
try:
|
|
1263
1530
|
from superlocalmemory.learning.outcome_queue import (
|
|
1264
|
-
get_counters,
|
|
1531
|
+
get_counters,
|
|
1532
|
+
queue_size,
|
|
1265
1533
|
)
|
|
1266
1534
|
counters = get_counters()
|
|
1267
1535
|
qsz = queue_size()
|
|
@@ -30,6 +30,7 @@ from .learning_telemetry import (
|
|
|
30
30
|
sqlite_status as _sqlite_status,
|
|
31
31
|
)
|
|
32
32
|
from superlocalmemory.storage.memory_write import memory_read
|
|
33
|
+
from superlocalmemory.learning.engagement import derive_engagement_from_dbs
|
|
33
34
|
|
|
34
35
|
logger = logging.getLogger("superlocalmemory.routes.learning")
|
|
35
36
|
router = APIRouter()
|
|
@@ -338,31 +339,18 @@ def learning_status(request: Request):
|
|
|
338
339
|
"signals": signal_count,
|
|
339
340
|
}
|
|
340
341
|
|
|
341
|
-
# Engagement — v3.4.
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
"health_status": health.upper(),
|
|
354
|
-
"days_active": active_days,
|
|
355
|
-
"memories_per_day": memories_per_day,
|
|
356
|
-
"total_events": total_events,
|
|
357
|
-
"recall_count": stats.get("recall_count", 0),
|
|
358
|
-
"store_count": stats.get("store_count", 0),
|
|
359
|
-
"session_count": stats.get("session_count", 0),
|
|
360
|
-
"engagement_score": stats.get("engagement_score", 0),
|
|
361
|
-
}
|
|
362
|
-
except Exception as exc:
|
|
363
|
-
logger.debug("engagement stats: %s", exc)
|
|
364
|
-
result["engagement"] = None
|
|
365
|
-
else:
|
|
342
|
+
# Engagement — v3.4.9: derive-on-read from existing tables (zero writes,
|
|
343
|
+
# satisfies I1 recall/remember latency, I3 no unbounded growth, I6 provenance).
|
|
344
|
+
# memory.db:atomic_facts drives store_count/days_active/health.
|
|
345
|
+
# learning.db:learning_signals drives recall_count (proxy: distinct queries).
|
|
346
|
+
try:
|
|
347
|
+
result["engagement"] = derive_engagement_from_dbs(
|
|
348
|
+
memory_db_path=MEMORY_DIR / "memory.db",
|
|
349
|
+
learning_db_path=LEARNING_DB,
|
|
350
|
+
profile_id=active_profile,
|
|
351
|
+
)
|
|
352
|
+
except Exception as exc:
|
|
353
|
+
logger.debug("derive_engagement_from_dbs: %s", exc)
|
|
366
354
|
result["engagement"] = None
|
|
367
355
|
|
|
368
356
|
# Tech preferences + workflow patterns from V3.1 behavioral store
|