superlocalmemory 3.4.25 → 3.4.31
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 +92 -0
- package/README.md +8 -1
- package/package.json +1 -1
- package/pyproject.toml +3 -1
- package/src/superlocalmemory/__init__.py +1 -1
- package/src/superlocalmemory/cli/daemon.py +90 -16
- package/src/superlocalmemory/cli/doctor_cmd.py +152 -0
- package/src/superlocalmemory/cli/main.py +28 -0
- package/src/superlocalmemory/cli/pending_store.py +55 -3
- package/src/superlocalmemory/cli/post_install.py +15 -0
- package/src/superlocalmemory/cli/setup_wizard.py +20 -0
- package/src/superlocalmemory/cli/version_banner.py +183 -0
- package/src/superlocalmemory/cli/wizard_v3426_options.py +129 -0
- package/src/superlocalmemory/core/clock_monitor.py +45 -0
- package/src/superlocalmemory/core/db_pool.py +80 -0
- package/src/superlocalmemory/core/engine.py +75 -30
- package/src/superlocalmemory/core/engine_capabilities.py +24 -0
- package/src/superlocalmemory/core/engine_lock.py +75 -0
- package/src/superlocalmemory/core/error_catalog.py +113 -0
- package/src/superlocalmemory/core/error_envelope.py +60 -0
- package/src/superlocalmemory/core/file_lock.py +92 -0
- package/src/superlocalmemory/core/loop_watchdog.py +56 -0
- package/src/superlocalmemory/core/maintenance_scheduler.py +8 -0
- package/src/superlocalmemory/core/priority_queue.py +61 -0
- package/src/superlocalmemory/core/queue_dispatcher.py +73 -0
- package/src/superlocalmemory/core/rate_limit.py +151 -0
- package/src/superlocalmemory/core/recall_queue.py +370 -0
- package/src/superlocalmemory/core/recall_worker.py +10 -0
- package/src/superlocalmemory/core/safe_fs.py +108 -0
- package/src/superlocalmemory/hooks/auto_capture.py +34 -12
- package/src/superlocalmemory/hooks/auto_recall.py +36 -9
- package/src/superlocalmemory/learning/signals.py +7 -1
- package/src/superlocalmemory/mcp/_daemon_proxy.py +107 -0
- package/src/superlocalmemory/mcp/_pool_adapter.py +121 -0
- package/src/superlocalmemory/mcp/resources.py +8 -5
- package/src/superlocalmemory/mcp/server.py +38 -9
- package/src/superlocalmemory/mcp/tools_active.py +21 -9
- package/src/superlocalmemory/mcp/tools_core.py +13 -9
- package/src/superlocalmemory/mcp/tools_evolution.py +4 -2
- package/src/superlocalmemory/mcp/tools_learning.py +5 -3
- package/src/superlocalmemory/mcp/tools_mesh.py +5 -3
- package/src/superlocalmemory/mcp/tools_v3.py +18 -22
- package/src/superlocalmemory/mcp/tools_v33.py +65 -2
- package/src/superlocalmemory/migrations/__init__.py +5 -0
- package/src/superlocalmemory/migrations/v3_4_25_to_v3_4_26.py +144 -0
- package/src/superlocalmemory/server/routes/data_io.py +21 -2
- package/src/superlocalmemory/server/routes/memories.py +91 -0
- package/src/superlocalmemory/server/routes/stats.py +16 -2
- package/src/superlocalmemory/server/unified_daemon.py +128 -12
- package/src/superlocalmemory/ui/index.html +35 -25
- package/src/superlocalmemory/ui/js/core.js +20 -4
- package/src/superlocalmemory/ui/js/fact-detail.js +62 -73
- package/src/superlocalmemory/ui/js/memories.js +34 -2
- package/src/superlocalmemory/ui/js/modal.js +41 -2
- package/src/superlocalmemory/ui/js/search.js +27 -0
|
@@ -40,6 +40,12 @@ async def get_stats():
|
|
|
40
40
|
"SELECT COUNT(*) as total FROM atomic_facts WHERE profile_id = ?",
|
|
41
41
|
(active_profile,),
|
|
42
42
|
)
|
|
43
|
+
total_facts = cursor.fetchone()['total']
|
|
44
|
+
|
|
45
|
+
cursor.execute(
|
|
46
|
+
"SELECT COUNT(*) as total FROM memories WHERE profile_id = ?",
|
|
47
|
+
(active_profile,),
|
|
48
|
+
)
|
|
43
49
|
total_memories = cursor.fetchone()['total']
|
|
44
50
|
|
|
45
51
|
total_sessions = 0
|
|
@@ -52,7 +58,7 @@ async def get_stats():
|
|
|
52
58
|
except Exception:
|
|
53
59
|
pass
|
|
54
60
|
|
|
55
|
-
total_graph_nodes =
|
|
61
|
+
total_graph_nodes = total_facts
|
|
56
62
|
total_graph_edges = 0
|
|
57
63
|
try:
|
|
58
64
|
cursor.execute(
|
|
@@ -121,12 +127,13 @@ async def get_stats():
|
|
|
121
127
|
importance_dist = []
|
|
122
128
|
|
|
123
129
|
else:
|
|
124
|
-
# V2 fallback
|
|
130
|
+
# V2 fallback — no atomic_facts; facts == memories
|
|
125
131
|
cursor.execute(
|
|
126
132
|
"SELECT COUNT(*) as total FROM memories WHERE profile = ?",
|
|
127
133
|
(active_profile,),
|
|
128
134
|
)
|
|
129
135
|
total_memories = cursor.fetchone()['total']
|
|
136
|
+
total_facts = total_memories
|
|
130
137
|
|
|
131
138
|
try:
|
|
132
139
|
cursor.execute("SELECT COUNT(*) as total FROM sessions")
|
|
@@ -201,9 +208,16 @@ async def get_stats():
|
|
|
201
208
|
|
|
202
209
|
conn.close()
|
|
203
210
|
|
|
211
|
+
facts_per_memory = (
|
|
212
|
+
round(total_facts / total_memories, 1)
|
|
213
|
+
if total_memories > 0 else 0.0
|
|
214
|
+
)
|
|
215
|
+
|
|
204
216
|
return {
|
|
205
217
|
"overview": {
|
|
206
218
|
"total_memories": total_memories,
|
|
219
|
+
"total_facts": total_facts,
|
|
220
|
+
"facts_per_memory": facts_per_memory,
|
|
207
221
|
"total_sessions": total_sessions,
|
|
208
222
|
"total_clusters": total_clusters,
|
|
209
223
|
"graph_nodes": total_graph_nodes,
|
|
@@ -59,6 +59,7 @@ _PORT_FILE = Path.home() / ".superlocalmemory" / "daemon.port"
|
|
|
59
59
|
class RememberRequest(BaseModel):
|
|
60
60
|
content: str
|
|
61
61
|
tags: str = ""
|
|
62
|
+
metadata: dict | None = None # v3.4.26: pass-through from MCP pool_store
|
|
62
63
|
|
|
63
64
|
|
|
64
65
|
class ObserveRequest(BaseModel):
|
|
@@ -952,23 +953,56 @@ def _register_daemon_routes(application: FastAPI) -> None:
|
|
|
952
953
|
response = engine.recall(
|
|
953
954
|
search_query, limit=limit, session_id=effective_sid,
|
|
954
955
|
)
|
|
955
|
-
|
|
956
|
-
|
|
956
|
+
# v3.4.26: return the same field shape as recall_worker so
|
|
957
|
+
# MCP processes proxying through the daemon get recall_trace-
|
|
958
|
+
# compatible data without a second round trip.
|
|
959
|
+
memory_ids = list({
|
|
960
|
+
r.fact.memory_id for r in response.results[:limit]
|
|
961
|
+
if r.fact.memory_id
|
|
962
|
+
})
|
|
963
|
+
memory_map = (
|
|
964
|
+
engine._db.get_memory_content_batch(memory_ids)
|
|
965
|
+
if memory_ids else {}
|
|
966
|
+
)
|
|
967
|
+
results = []
|
|
968
|
+
for r in response.results[:limit]:
|
|
969
|
+
fact_type = getattr(r.fact, "fact_type", None)
|
|
970
|
+
lifecycle = getattr(r.fact, "lifecycle", None)
|
|
971
|
+
results.append({
|
|
972
|
+
"fact_id": r.fact.fact_id,
|
|
973
|
+
"memory_id": r.fact.memory_id,
|
|
957
974
|
"content": r.fact.content,
|
|
975
|
+
"source_content": memory_map.get(r.fact.memory_id, ""),
|
|
958
976
|
"score": round(r.score, 4),
|
|
959
|
-
"
|
|
960
|
-
"
|
|
977
|
+
"confidence": round(r.confidence, 4),
|
|
978
|
+
"trust_score": round(r.trust_score, 4),
|
|
961
979
|
"channel_scores": {
|
|
962
|
-
k: round(v, 4)
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
980
|
+
k: round(v, 4)
|
|
981
|
+
for k, v in (r.channel_scores or {}).items()
|
|
982
|
+
},
|
|
983
|
+
"fact_type": fact_type.value
|
|
984
|
+
if fact_type and hasattr(fact_type, "value")
|
|
985
|
+
else getattr(r.fact, "fact_type", ""),
|
|
986
|
+
"lifecycle": lifecycle.value
|
|
987
|
+
if lifecycle and hasattr(lifecycle, "value") else "",
|
|
988
|
+
"access_count": getattr(r.fact, "access_count", 0),
|
|
989
|
+
"evidence_chain": list(
|
|
990
|
+
getattr(r, "evidence_chain", []) or []
|
|
991
|
+
),
|
|
992
|
+
})
|
|
967
993
|
return {
|
|
968
|
-
"
|
|
969
|
-
"
|
|
994
|
+
"ok": True,
|
|
995
|
+
"query": search_query,
|
|
970
996
|
"query_type": response.query_type,
|
|
997
|
+
"result_count": len(results),
|
|
971
998
|
"retrieval_time_ms": round(response.retrieval_time_ms, 1),
|
|
999
|
+
"channel_weights": {
|
|
1000
|
+
k: round(v, 3)
|
|
1001
|
+
for k, v in (response.channel_weights or {}).items()
|
|
1002
|
+
},
|
|
1003
|
+
"total_candidates": getattr(response, "total_candidates", 0),
|
|
1004
|
+
"results": results,
|
|
1005
|
+
"count": len(results),
|
|
972
1006
|
}
|
|
973
1007
|
except Exception as exc:
|
|
974
1008
|
raise HTTPException(500, detail=str(exc))
|
|
@@ -979,8 +1013,11 @@ def _register_daemon_routes(application: FastAPI) -> None:
|
|
|
979
1013
|
engine = _get_engine_or_503()
|
|
980
1014
|
try:
|
|
981
1015
|
metadata = {"tags": req.tags} if req.tags else {}
|
|
1016
|
+
extra = getattr(req, "metadata", None)
|
|
1017
|
+
if isinstance(extra, dict):
|
|
1018
|
+
metadata.update(extra)
|
|
982
1019
|
fact_ids = engine.store(req.content, metadata=metadata)
|
|
983
|
-
return {"fact_ids": fact_ids, "count": len(fact_ids)}
|
|
1020
|
+
return {"ok": True, "fact_ids": fact_ids, "count": len(fact_ids)}
|
|
984
1021
|
except Exception as exc:
|
|
985
1022
|
raise HTTPException(500, detail=str(exc))
|
|
986
1023
|
|
|
@@ -990,6 +1027,71 @@ def _register_daemon_routes(application: FastAPI) -> None:
|
|
|
990
1027
|
result = _observe_buffer.enqueue(req.content)
|
|
991
1028
|
return result
|
|
992
1029
|
|
|
1030
|
+
# v3.4.26: CCQ consolidation via daemon so MCP clients don't need to
|
|
1031
|
+
# import CognitiveConsolidator (which pulls sentence-transformers).
|
|
1032
|
+
@application.post("/consolidate/cognitive")
|
|
1033
|
+
async def consolidate_cognitive_endpoint(body: dict):
|
|
1034
|
+
_update_activity()
|
|
1035
|
+
engine = _get_engine_or_503()
|
|
1036
|
+
try:
|
|
1037
|
+
pid = body.get("profile_id") or engine.profile_id
|
|
1038
|
+
from superlocalmemory.encoding.cognitive_consolidator import (
|
|
1039
|
+
CognitiveConsolidator,
|
|
1040
|
+
)
|
|
1041
|
+
consolidator = CognitiveConsolidator(db=engine._db)
|
|
1042
|
+
result = consolidator.run_pipeline(pid)
|
|
1043
|
+
return {
|
|
1044
|
+
"ok": True,
|
|
1045
|
+
"profile_id": pid,
|
|
1046
|
+
"clusters_processed": result.clusters_processed,
|
|
1047
|
+
"blocks_created": result.blocks_created,
|
|
1048
|
+
}
|
|
1049
|
+
except Exception as exc:
|
|
1050
|
+
raise HTTPException(500, detail=str(exc))
|
|
1051
|
+
|
|
1052
|
+
# v3.4.26: run_maintenance via daemon so MCP doesn't import
|
|
1053
|
+
# EbbinghausCurve, ForgettingScheduler, or ConsolidationWorker.
|
|
1054
|
+
@application.post("/maintenance/run")
|
|
1055
|
+
async def run_maintenance_endpoint(body: dict):
|
|
1056
|
+
_update_activity()
|
|
1057
|
+
engine = _get_engine_or_503()
|
|
1058
|
+
try:
|
|
1059
|
+
pid = body.get("profile_id") or engine.profile_id
|
|
1060
|
+
results: dict = {}
|
|
1061
|
+
try:
|
|
1062
|
+
from superlocalmemory.core.maintenance import run_maintenance as _run_maint
|
|
1063
|
+
maint_result = _run_maint(engine._db, engine._config, pid)
|
|
1064
|
+
results["langevin"] = {"updated": maint_result.get("updated", 0)}
|
|
1065
|
+
except Exception as exc:
|
|
1066
|
+
results["langevin"] = {"error": str(exc)}
|
|
1067
|
+
try:
|
|
1068
|
+
from superlocalmemory.math.ebbinghaus import EbbinghausCurve
|
|
1069
|
+
from superlocalmemory.learning.forgetting_scheduler import (
|
|
1070
|
+
ForgettingScheduler,
|
|
1071
|
+
)
|
|
1072
|
+
ebb = EbbinghausCurve(engine._config.forgetting)
|
|
1073
|
+
sched = ForgettingScheduler(
|
|
1074
|
+
engine._db, ebb, engine._config.forgetting,
|
|
1075
|
+
)
|
|
1076
|
+
results["forgetting"] = sched.run_decay_cycle(pid, force=False)
|
|
1077
|
+
except Exception as exc:
|
|
1078
|
+
results["forgetting"] = {"error": str(exc)}
|
|
1079
|
+
try:
|
|
1080
|
+
from superlocalmemory.learning.consolidation_worker import (
|
|
1081
|
+
ConsolidationWorker,
|
|
1082
|
+
)
|
|
1083
|
+
cw = ConsolidationWorker(
|
|
1084
|
+
engine._db.db_path,
|
|
1085
|
+
engine._db.db_path.parent / "learning.db",
|
|
1086
|
+
)
|
|
1087
|
+
count = cw._generate_patterns(pid, False)
|
|
1088
|
+
results["behavioral"] = {"patterns_mined": count}
|
|
1089
|
+
except Exception as exc:
|
|
1090
|
+
results["behavioral"] = {"error": str(exc)}
|
|
1091
|
+
return {"ok": True, "profile": pid, **results}
|
|
1092
|
+
except Exception as exc:
|
|
1093
|
+
raise HTTPException(500, detail=str(exc))
|
|
1094
|
+
|
|
993
1095
|
@application.get("/status")
|
|
994
1096
|
async def status():
|
|
995
1097
|
_update_activity()
|
|
@@ -1104,6 +1206,20 @@ def start_server(port: int = _DEFAULT_PORT) -> None:
|
|
|
1104
1206
|
_PORT_FILE.write_text(str(port))
|
|
1105
1207
|
_start_time = time.monotonic()
|
|
1106
1208
|
|
|
1209
|
+
try:
|
|
1210
|
+
from superlocalmemory.migrations.v3_4_25_to_v3_4_26 import (
|
|
1211
|
+
is_ready as _is_ready, migrate as _migrate,
|
|
1212
|
+
)
|
|
1213
|
+
_data = Path(os.environ.get("SLM_DATA_DIR")
|
|
1214
|
+
or Path.home() / ".superlocalmemory")
|
|
1215
|
+
if not _is_ready(_data):
|
|
1216
|
+
_migrate(_data)
|
|
1217
|
+
except Exception as exc:
|
|
1218
|
+
import logging as _logging
|
|
1219
|
+
_logging.getLogger(__name__).warning(
|
|
1220
|
+
"v3.4.26 migration on daemon start failed: %s", exc,
|
|
1221
|
+
)
|
|
1222
|
+
|
|
1107
1223
|
# v3.4.7: Start memory watchdog to prevent runaway workers
|
|
1108
1224
|
_start_memory_watchdog()
|
|
1109
1225
|
|
|
@@ -53,15 +53,17 @@
|
|
|
53
53
|
<div class="stat-bg"></div>
|
|
54
54
|
<i class="bi bi-journal-text stat-icon text-primary"></i>
|
|
55
55
|
<h3 class="mt-2 mb-0 stat-value" id="stat-memories">-</h3>
|
|
56
|
-
<small class="text-muted">
|
|
56
|
+
<small class="text-muted">Memories</small>
|
|
57
|
+
<small class="text-muted d-block mt-1" id="stat-memories-sub" style="font-size: 0.75rem;"> </small>
|
|
57
58
|
</div>
|
|
58
59
|
</div>
|
|
59
60
|
<div class="col-md-3 col-6 mb-3">
|
|
60
|
-
<div class="card stat-card stat-card-
|
|
61
|
+
<div class="card stat-card stat-card-facts text-center p-3">
|
|
61
62
|
<div class="stat-bg"></div>
|
|
62
|
-
<i class="bi bi-
|
|
63
|
-
<h3 class="mt-2 mb-0 stat-value" id="stat-
|
|
64
|
-
<small class="text-muted">
|
|
63
|
+
<i class="bi bi-diagram-3 stat-icon text-info"></i>
|
|
64
|
+
<h3 class="mt-2 mb-0 stat-value" id="stat-facts">-</h3>
|
|
65
|
+
<small class="text-muted">Atomic Facts</small>
|
|
66
|
+
<small class="text-muted d-block mt-1" id="stat-facts-sub" style="font-size: 0.75rem;"> </small>
|
|
65
67
|
</div>
|
|
66
68
|
</div>
|
|
67
69
|
<div class="col-md-3 col-6 mb-3">
|
|
@@ -477,27 +479,34 @@
|
|
|
477
479
|
<div id="recall-lab-results"></div>
|
|
478
480
|
</div>
|
|
479
481
|
|
|
480
|
-
<input type="hidden" id="search-query" value="" aria-hidden="true">
|
|
481
|
-
|
|
482
482
|
<div class="card p-3 mb-3">
|
|
483
|
-
<div class="d-flex justify-content-between align-items-center mb-3">
|
|
484
|
-
<h5 class="mb-0"><i class="bi bi-list-ul"></i> Browse
|
|
485
|
-
<div class="
|
|
486
|
-
<
|
|
487
|
-
<i class="bi bi-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
<
|
|
499
|
-
|
|
500
|
-
|
|
483
|
+
<div class="d-flex justify-content-between align-items-center mb-3 flex-wrap gap-2">
|
|
484
|
+
<h5 class="mb-0"><i class="bi bi-list-ul"></i> Browse atomic facts</h5>
|
|
485
|
+
<div class="d-flex align-items-center gap-2 flex-wrap">
|
|
486
|
+
<div class="input-group input-group-sm" style="width: 280px;">
|
|
487
|
+
<span class="input-group-text"><i class="bi bi-search"></i></span>
|
|
488
|
+
<input type="search" class="form-control" id="search-query" placeholder="Search memories..." autocomplete="off">
|
|
489
|
+
</div>
|
|
490
|
+
<div class="dropdown export-dropdown">
|
|
491
|
+
<button class="btn btn-sm btn-outline-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
|
|
492
|
+
<i class="bi bi-download"></i> Export
|
|
493
|
+
</button>
|
|
494
|
+
<ul class="dropdown-menu dropdown-menu-end">
|
|
495
|
+
<li><a class="dropdown-item" href="#" onclick="exportAll('json'); return false;">
|
|
496
|
+
<i class="bi bi-filetype-json"></i> Export All (JSON)
|
|
497
|
+
</a></li>
|
|
498
|
+
<li><a class="dropdown-item" href="#" onclick="exportAll('jsonl'); return false;">
|
|
499
|
+
<i class="bi bi-file-text"></i> Export All (JSONL)
|
|
500
|
+
</a></li>
|
|
501
|
+
<li><a class="dropdown-item" href="#" onclick="exportAll('csv'); return false;">
|
|
502
|
+
<i class="bi bi-filetype-csv"></i> Export All (CSV)
|
|
503
|
+
</a></li>
|
|
504
|
+
<li><hr class="dropdown-divider"></li>
|
|
505
|
+
<li><a class="dropdown-item" href="#" id="export-search-btn" onclick="exportSearchResults(); return false;" style="display:none;">
|
|
506
|
+
<i class="bi bi-funnel"></i> Export Search Results
|
|
507
|
+
</a></li>
|
|
508
|
+
</ul>
|
|
509
|
+
</div>
|
|
501
510
|
</div>
|
|
502
511
|
</div>
|
|
503
512
|
<div class="row g-2 mb-3">
|
|
@@ -543,6 +552,7 @@
|
|
|
543
552
|
<div>Loading memories...</div>
|
|
544
553
|
</div>
|
|
545
554
|
</div>
|
|
555
|
+
<div id="memories-pagination"></div>
|
|
546
556
|
</div>
|
|
547
557
|
|
|
548
558
|
<div class="card p-3 mt-3">
|
|
@@ -274,16 +274,32 @@ async function loadStats() {
|
|
|
274
274
|
var response = await slmFetch('/api/stats');
|
|
275
275
|
var data = await response.json();
|
|
276
276
|
var ov = data.overview || {};
|
|
277
|
-
|
|
278
|
-
|
|
277
|
+
var memories = ov.total_memories || 0;
|
|
278
|
+
var facts = ov.total_facts || 0;
|
|
279
|
+
var ratio = ov.facts_per_memory || 0;
|
|
280
|
+
|
|
281
|
+
animateCounter('stat-memories', memories);
|
|
282
|
+
animateCounter('stat-facts', facts);
|
|
279
283
|
animateCounter('stat-nodes', ov.graph_nodes || 0);
|
|
280
284
|
animateCounter('stat-edges', ov.graph_edges || 0);
|
|
285
|
+
|
|
286
|
+
var mSub = document.getElementById('stat-memories-sub');
|
|
287
|
+
if (mSub) {
|
|
288
|
+
mSub.textContent = memories > 0
|
|
289
|
+
? 'what you stored'
|
|
290
|
+
: '\u00a0';
|
|
291
|
+
}
|
|
292
|
+
var fSub = document.getElementById('stat-facts-sub');
|
|
293
|
+
if (fSub) {
|
|
294
|
+
fSub.textContent = memories > 0
|
|
295
|
+
? 'avg ' + ratio + ' per memory'
|
|
296
|
+
: '\u00a0';
|
|
297
|
+
}
|
|
281
298
|
populateFilters(data.categories || [], data.projects || []);
|
|
282
299
|
} catch (error) {
|
|
283
300
|
console.error('Error loading stats:', error);
|
|
284
|
-
// On error (fresh install, server starting), show 0 instead of "-"
|
|
285
301
|
animateCounter('stat-memories', 0);
|
|
286
|
-
animateCounter('stat-
|
|
302
|
+
animateCounter('stat-facts', 0);
|
|
287
303
|
animateCounter('stat-nodes', 0);
|
|
288
304
|
animateCounter('stat-edges', 0);
|
|
289
305
|
}
|
|
@@ -1,92 +1,81 @@
|
|
|
1
1
|
// SuperLocalMemory V3 — Fact Detail View
|
|
2
|
-
//
|
|
2
|
+
// v3.4.31: scoped click listener, real fact_id lookup, no text-based re-query.
|
|
3
|
+
//
|
|
4
|
+
// Scope: only `.fact-result-item` elements (search results view), NEVER
|
|
5
|
+
// fires on the main memories table rows (those use openMemoryDetail via
|
|
6
|
+
// memories.js). This prevents the two listeners from colliding.
|
|
3
7
|
|
|
4
8
|
document.addEventListener('click', function(e) {
|
|
5
|
-
var item = e.target.closest('[data-fact-id]');
|
|
9
|
+
var item = e.target.closest('.fact-result-item[data-fact-id]');
|
|
6
10
|
if (!item) return;
|
|
7
11
|
|
|
8
|
-
//
|
|
12
|
+
// Don't interfere if the click was on an action button/link inside the row
|
|
13
|
+
if (e.target.closest('button, a, [data-bs-toggle]')) return;
|
|
14
|
+
|
|
9
15
|
var existingDetail = item.querySelector('.fact-detail-panel');
|
|
10
16
|
if (existingDetail) {
|
|
11
17
|
existingDetail.remove();
|
|
12
18
|
return;
|
|
13
19
|
}
|
|
14
20
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
if (!queryText) return;
|
|
18
|
-
|
|
19
|
-
fetch('/api/v3/recall/trace', {
|
|
20
|
-
method: 'POST',
|
|
21
|
-
headers: { 'Content-Type': 'application/json' },
|
|
22
|
-
body: JSON.stringify({ query: queryText, limit: 1 })
|
|
23
|
-
}).then(function(r) {
|
|
24
|
-
return r.json();
|
|
25
|
-
}).then(function(data) {
|
|
26
|
-
var result = (data.results || [])[0];
|
|
27
|
-
if (!result) return;
|
|
28
|
-
|
|
29
|
-
var panel = document.createElement('div');
|
|
30
|
-
panel.className = 'fact-detail-panel card mt-2 mb-2 border-info';
|
|
31
|
-
|
|
32
|
-
var cardBody = document.createElement('div');
|
|
33
|
-
cardBody.className = 'card-body small';
|
|
34
|
-
|
|
35
|
-
// Score / Trust / Confidence row
|
|
36
|
-
var row1 = document.createElement('div');
|
|
37
|
-
row1.className = 'row';
|
|
21
|
+
var factId = item.getAttribute('data-fact-id');
|
|
22
|
+
if (!factId) return;
|
|
38
23
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
col1.appendChild(col1Label);
|
|
44
|
-
col1.appendChild(document.createTextNode(result.score || 0));
|
|
45
|
-
row1.appendChild(col1);
|
|
24
|
+
fetch('/api/facts/' + encodeURIComponent(factId))
|
|
25
|
+
.then(function(r) { return r.ok ? r.json() : null; })
|
|
26
|
+
.then(function(data) {
|
|
27
|
+
if (!data || !data.fact_id) return;
|
|
46
28
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
col2.appendChild(col2Label);
|
|
52
|
-
col2.appendChild(document.createTextNode(result.trust_score || 0));
|
|
53
|
-
row1.appendChild(col2);
|
|
29
|
+
var panel = document.createElement('div');
|
|
30
|
+
panel.className = 'fact-detail-panel card mt-2 mb-2 border-info';
|
|
31
|
+
var body = document.createElement('div');
|
|
32
|
+
body.className = 'card-body small';
|
|
54
33
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
34
|
+
var head = document.createElement('div');
|
|
35
|
+
head.className = 'mb-2';
|
|
36
|
+
var h = document.createElement('strong');
|
|
37
|
+
h.textContent = 'Atomic fact';
|
|
38
|
+
head.appendChild(h);
|
|
39
|
+
head.appendChild(document.createTextNode(
|
|
40
|
+
' · ' + (data.fact_type || '-') +
|
|
41
|
+
' · confidence ' + (data.confidence || 0) +
|
|
42
|
+
' · importance ' + (data.importance || 0)
|
|
43
|
+
));
|
|
44
|
+
body.appendChild(head);
|
|
62
45
|
|
|
63
|
-
|
|
46
|
+
if (data.source_memory_content) {
|
|
47
|
+
var src = document.createElement('div');
|
|
48
|
+
src.className = 'text-muted small mt-2';
|
|
49
|
+
var srcLabel = document.createElement('strong');
|
|
50
|
+
srcLabel.textContent = 'From memory: ';
|
|
51
|
+
src.appendChild(srcLabel);
|
|
52
|
+
var srcText = String(data.source_memory_content);
|
|
53
|
+
src.appendChild(document.createTextNode(
|
|
54
|
+
srcText.length > 200 ? srcText.substring(0, 200) + '...' : srcText
|
|
55
|
+
));
|
|
56
|
+
body.appendChild(src);
|
|
57
|
+
}
|
|
64
58
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
label.className = 'mt-2';
|
|
71
|
-
var labelStrong = document.createElement('strong');
|
|
72
|
-
labelStrong.textContent = 'Channel Scores:';
|
|
73
|
-
label.appendChild(labelStrong);
|
|
74
|
-
cardBody.appendChild(label);
|
|
59
|
+
var ids = document.createElement('div');
|
|
60
|
+
ids.className = 'text-muted mt-2';
|
|
61
|
+
ids.style.fontSize = '0.75rem';
|
|
62
|
+
ids.textContent = 'Fact ID: ' + data.fact_id + ' · Memory ID: ' + (data.memory_id || '-');
|
|
63
|
+
body.appendChild(ids);
|
|
75
64
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
var
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
}
|
|
65
|
+
if (data.entities && data.entities.length > 0) {
|
|
66
|
+
var ent = document.createElement('div');
|
|
67
|
+
ent.className = 'mt-2';
|
|
68
|
+
var entLabel = document.createElement('strong');
|
|
69
|
+
entLabel.textContent = 'Entities: ';
|
|
70
|
+
ent.appendChild(entLabel);
|
|
71
|
+
ent.appendChild(document.createTextNode(data.entities.join(', ')));
|
|
72
|
+
body.appendChild(ent);
|
|
73
|
+
}
|
|
86
74
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
75
|
+
panel.appendChild(body);
|
|
76
|
+
item.appendChild(panel);
|
|
77
|
+
})
|
|
78
|
+
.catch(function(err) {
|
|
79
|
+
console.warn('Fact detail error:', err);
|
|
80
|
+
});
|
|
92
81
|
});
|
|
@@ -20,10 +20,18 @@ function setMemoryFilter(name) {
|
|
|
20
20
|
loadMemories();
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
// v3.4.31 pagination state
|
|
24
|
+
var _slmPage = 0;
|
|
25
|
+
var _slmPageSize = 50;
|
|
26
|
+
var _slmLastTotal = 0;
|
|
27
|
+
|
|
28
|
+
async function loadMemories(page) {
|
|
29
|
+
if (typeof page === 'number') _slmPage = Math.max(0, page);
|
|
24
30
|
var category = document.getElementById('filter-category').value;
|
|
25
31
|
var project = document.getElementById('filter-project').value;
|
|
26
|
-
var
|
|
32
|
+
var limit = _slmPageSize;
|
|
33
|
+
var offset = _slmPage * limit;
|
|
34
|
+
var url = '/api/memories?limit=' + limit + '&offset=' + offset;
|
|
27
35
|
if (category) url += '&category=' + encodeURIComponent(category);
|
|
28
36
|
if (project) url += '&project_name=' + encodeURIComponent(project);
|
|
29
37
|
if (_slmMemoryFilter) {
|
|
@@ -37,13 +45,37 @@ async function loadMemories() {
|
|
|
37
45
|
lastSearchResults = null;
|
|
38
46
|
var exportBtn = document.getElementById('export-search-btn');
|
|
39
47
|
if (exportBtn) exportBtn.style.display = 'none';
|
|
48
|
+
_slmLastTotal = data.total || 0;
|
|
40
49
|
renderMemoriesTable(data.memories, false);
|
|
50
|
+
renderPaginationControls(data);
|
|
41
51
|
} catch (error) {
|
|
42
52
|
console.error('Error loading memories:', error);
|
|
43
53
|
showEmpty('memories-list', 'exclamation-triangle', 'Failed to load memories');
|
|
44
54
|
}
|
|
45
55
|
}
|
|
46
56
|
|
|
57
|
+
function renderPaginationControls(data) {
|
|
58
|
+
var el = document.getElementById('memories-pagination');
|
|
59
|
+
if (!el) return;
|
|
60
|
+
var total = data.total || 0;
|
|
61
|
+
var limit = data.limit || _slmPageSize;
|
|
62
|
+
var offset = data.offset || 0;
|
|
63
|
+
var showing = Math.min(offset + limit, total);
|
|
64
|
+
var firstIdx = total === 0 ? 0 : offset + 1;
|
|
65
|
+
var lastPage = Math.max(0, Math.ceil(total / limit) - 1);
|
|
66
|
+
var prevDisabled = _slmPage <= 0 ? 'disabled' : '';
|
|
67
|
+
var nextDisabled = _slmPage >= lastPage ? 'disabled' : '';
|
|
68
|
+
el.innerHTML =
|
|
69
|
+
'<div class="d-flex justify-content-between align-items-center mt-3 small">' +
|
|
70
|
+
'<span class="text-muted">Showing ' + firstIdx + '\u2013' + showing + ' of ' + total + ' memories</span>' +
|
|
71
|
+
'<div class="btn-group btn-group-sm">' +
|
|
72
|
+
'<button type="button" class="btn btn-outline-secondary" ' + prevDisabled + ' onclick="loadMemories(' + (_slmPage - 1) + ')"><i class="bi bi-chevron-left"></i> Prev</button>' +
|
|
73
|
+
'<button type="button" class="btn btn-outline-secondary disabled">Page ' + (_slmPage + 1) + ' / ' + (lastPage + 1) + '</button>' +
|
|
74
|
+
'<button type="button" class="btn btn-outline-secondary" ' + nextDisabled + ' onclick="loadMemories(' + (_slmPage + 1) + ')">Next <i class="bi bi-chevron-right"></i></button>' +
|
|
75
|
+
'</div>' +
|
|
76
|
+
'</div>';
|
|
77
|
+
}
|
|
78
|
+
|
|
47
79
|
function renderMemoriesTable(memories, showScores) {
|
|
48
80
|
var container = document.getElementById('memories-list');
|
|
49
81
|
if (!memories || memories.length === 0) {
|
|
@@ -40,11 +40,18 @@ function openMemoryDetail(mem, source) {
|
|
|
40
40
|
var dl = document.createElement('dl');
|
|
41
41
|
dl.className = 'memory-detail-meta row';
|
|
42
42
|
|
|
43
|
+
// v3.4.31: disambiguate Fact ID (the atomic unit) from Memory ID (the parent).
|
|
44
|
+
var factId = mem.fact_id || mem.id || '';
|
|
45
|
+
var memoryId = mem.memory_id || mem.id || '';
|
|
46
|
+
|
|
43
47
|
// Left column
|
|
44
48
|
var col1 = document.createElement('div');
|
|
45
49
|
col1.className = 'col-md-6';
|
|
46
|
-
addDetailRow(col1, 'ID', String(
|
|
47
|
-
|
|
50
|
+
addDetailRow(col1, 'Memory ID', String(memoryId || '-'));
|
|
51
|
+
if (factId && factId !== memoryId) {
|
|
52
|
+
addDetailRow(col1, 'Fact ID', String(factId));
|
|
53
|
+
}
|
|
54
|
+
addDetailBadgeRow(col1, 'Category', mem.category || mem.fact_type || 'None', 'bg-primary');
|
|
48
55
|
addDetailRow(col1, 'Project', mem.project_name || '-');
|
|
49
56
|
addDetailTagsRow(col1, 'Tags', tags);
|
|
50
57
|
dl.appendChild(col1);
|
|
@@ -65,6 +72,38 @@ function openMemoryDetail(mem, source) {
|
|
|
65
72
|
|
|
66
73
|
body.appendChild(dl);
|
|
67
74
|
|
|
75
|
+
// v3.4.31: hydrate with full memory + fact list from /api/memories/{id}/detail
|
|
76
|
+
if (memoryId) {
|
|
77
|
+
fetch('/api/memories/' + encodeURIComponent(memoryId) + '/detail')
|
|
78
|
+
.then(function(r) { return r.ok ? r.json() : null; })
|
|
79
|
+
.then(function(data) {
|
|
80
|
+
if (!data || !data.memory) return;
|
|
81
|
+
var hydration = document.getElementById('memory-detail-hydration');
|
|
82
|
+
if (hydration) hydration.remove();
|
|
83
|
+
var block = document.createElement('div');
|
|
84
|
+
block.id = 'memory-detail-hydration';
|
|
85
|
+
block.className = 'mt-3';
|
|
86
|
+
var h = document.createElement('h6');
|
|
87
|
+
h.innerHTML = '<i class="bi bi-diagram-3"></i> Atomic facts extracted from this memory (' + (data.fact_count || 0) + ')';
|
|
88
|
+
block.appendChild(h);
|
|
89
|
+
var list = document.createElement('div');
|
|
90
|
+
list.className = 'list-group list-group-flush';
|
|
91
|
+
(data.facts || []).forEach(function(f) {
|
|
92
|
+
var row = document.createElement('div');
|
|
93
|
+
row.className = 'list-group-item list-group-item-action small fact-result-item';
|
|
94
|
+
row.setAttribute('data-fact-id', f.fact_id);
|
|
95
|
+
row.style.cursor = 'pointer';
|
|
96
|
+
var badge = '<span class="badge bg-secondary me-2">' + (f.fact_type || '-') + '</span>';
|
|
97
|
+
var confText = ' · confidence ' + (f.confidence || 0).toFixed(2);
|
|
98
|
+
row.innerHTML = badge + escapeHtml(String(f.content || '')) + '<small class="text-muted">' + escapeHtml(confText) + '</small>';
|
|
99
|
+
list.appendChild(row);
|
|
100
|
+
});
|
|
101
|
+
block.appendChild(list);
|
|
102
|
+
body.appendChild(block);
|
|
103
|
+
})
|
|
104
|
+
.catch(function(err) { console.warn('Hydration error:', err); });
|
|
105
|
+
}
|
|
106
|
+
|
|
68
107
|
// Context-aware action buttons
|
|
69
108
|
if (mem.id) {
|
|
70
109
|
body.appendChild(document.createElement('hr'));
|