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.
Files changed (55) hide show
  1. package/CHANGELOG.md +92 -0
  2. package/README.md +8 -1
  3. package/package.json +1 -1
  4. package/pyproject.toml +3 -1
  5. package/src/superlocalmemory/__init__.py +1 -1
  6. package/src/superlocalmemory/cli/daemon.py +90 -16
  7. package/src/superlocalmemory/cli/doctor_cmd.py +152 -0
  8. package/src/superlocalmemory/cli/main.py +28 -0
  9. package/src/superlocalmemory/cli/pending_store.py +55 -3
  10. package/src/superlocalmemory/cli/post_install.py +15 -0
  11. package/src/superlocalmemory/cli/setup_wizard.py +20 -0
  12. package/src/superlocalmemory/cli/version_banner.py +183 -0
  13. package/src/superlocalmemory/cli/wizard_v3426_options.py +129 -0
  14. package/src/superlocalmemory/core/clock_monitor.py +45 -0
  15. package/src/superlocalmemory/core/db_pool.py +80 -0
  16. package/src/superlocalmemory/core/engine.py +75 -30
  17. package/src/superlocalmemory/core/engine_capabilities.py +24 -0
  18. package/src/superlocalmemory/core/engine_lock.py +75 -0
  19. package/src/superlocalmemory/core/error_catalog.py +113 -0
  20. package/src/superlocalmemory/core/error_envelope.py +60 -0
  21. package/src/superlocalmemory/core/file_lock.py +92 -0
  22. package/src/superlocalmemory/core/loop_watchdog.py +56 -0
  23. package/src/superlocalmemory/core/maintenance_scheduler.py +8 -0
  24. package/src/superlocalmemory/core/priority_queue.py +61 -0
  25. package/src/superlocalmemory/core/queue_dispatcher.py +73 -0
  26. package/src/superlocalmemory/core/rate_limit.py +151 -0
  27. package/src/superlocalmemory/core/recall_queue.py +370 -0
  28. package/src/superlocalmemory/core/recall_worker.py +10 -0
  29. package/src/superlocalmemory/core/safe_fs.py +108 -0
  30. package/src/superlocalmemory/hooks/auto_capture.py +34 -12
  31. package/src/superlocalmemory/hooks/auto_recall.py +36 -9
  32. package/src/superlocalmemory/learning/signals.py +7 -1
  33. package/src/superlocalmemory/mcp/_daemon_proxy.py +107 -0
  34. package/src/superlocalmemory/mcp/_pool_adapter.py +121 -0
  35. package/src/superlocalmemory/mcp/resources.py +8 -5
  36. package/src/superlocalmemory/mcp/server.py +38 -9
  37. package/src/superlocalmemory/mcp/tools_active.py +21 -9
  38. package/src/superlocalmemory/mcp/tools_core.py +13 -9
  39. package/src/superlocalmemory/mcp/tools_evolution.py +4 -2
  40. package/src/superlocalmemory/mcp/tools_learning.py +5 -3
  41. package/src/superlocalmemory/mcp/tools_mesh.py +5 -3
  42. package/src/superlocalmemory/mcp/tools_v3.py +18 -22
  43. package/src/superlocalmemory/mcp/tools_v33.py +65 -2
  44. package/src/superlocalmemory/migrations/__init__.py +5 -0
  45. package/src/superlocalmemory/migrations/v3_4_25_to_v3_4_26.py +144 -0
  46. package/src/superlocalmemory/server/routes/data_io.py +21 -2
  47. package/src/superlocalmemory/server/routes/memories.py +91 -0
  48. package/src/superlocalmemory/server/routes/stats.py +16 -2
  49. package/src/superlocalmemory/server/unified_daemon.py +128 -12
  50. package/src/superlocalmemory/ui/index.html +35 -25
  51. package/src/superlocalmemory/ui/js/core.js +20 -4
  52. package/src/superlocalmemory/ui/js/fact-detail.js +62 -73
  53. package/src/superlocalmemory/ui/js/memories.js +34 -2
  54. package/src/superlocalmemory/ui/js/modal.js +41 -2
  55. 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 = total_memories
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
- results = [
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
- "fact_type": getattr(r.fact.fact_type, 'value', str(r.fact.fact_type)),
960
- "fact_id": r.fact.fact_id,
977
+ "confidence": round(r.confidence, 4),
978
+ "trust_score": round(r.trust_score, 4),
961
979
  "channel_scores": {
962
- k: round(v, 4) for k, v in r.channel_scores.items()
963
- } if r.channel_scores else {},
964
- }
965
- for r in response.results
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
- "results": results,
969
- "count": len(results),
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">Total Memories</small>
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;">&nbsp;</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-clusters text-center p-3">
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-collection stat-icon text-success"></i>
63
- <h3 class="mt-2 mb-0 stat-value" id="stat-clusters">-</h3>
64
- <small class="text-muted">Clusters</small>
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;">&nbsp;</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 all memories</h5>
485
- <div class="dropdown export-dropdown">
486
- <button class="btn btn-sm btn-outline-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
487
- <i class="bi bi-download"></i> Export
488
- </button>
489
- <ul class="dropdown-menu dropdown-menu-end">
490
- <li><a class="dropdown-item" href="#" onclick="exportAll('json'); return false;">
491
- <i class="bi bi-filetype-json"></i> Export All (JSON)
492
- </a></li>
493
- <li><a class="dropdown-item" href="#" onclick="exportAll('jsonl'); return false;">
494
- <i class="bi bi-file-text"></i> Export All (JSONL)
495
- </a></li>
496
- <li><hr class="dropdown-divider"></li>
497
- <li><a class="dropdown-item" href="#" id="export-search-btn" onclick="exportSearchResults(); return false;" style="display:none;">
498
- <i class="bi bi-funnel"></i> Export Search Results
499
- </a></li>
500
- </ul>
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
- animateCounter('stat-memories', ov.total_memories || 0);
278
- animateCounter('stat-clusters', ov.total_clusters || 0);
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-clusters', 0);
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
- // Adds click-to-expand on memory list items to show channel scores and trust data.
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
- // Toggle: if detail panel already exists, remove it
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
- // Extract query text from the item (first 100 chars)
16
- var queryText = (item.textContent || '').substring(0, 100).trim();
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
- var col1 = document.createElement('div');
40
- col1.className = 'col-md-4';
41
- var col1Label = document.createElement('strong');
42
- col1Label.textContent = 'Score: ';
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
- var col2 = document.createElement('div');
48
- col2.className = 'col-md-4';
49
- var col2Label = document.createElement('strong');
50
- col2Label.textContent = 'Trust: ';
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
- var col3 = document.createElement('div');
56
- col3.className = 'col-md-4';
57
- var col3Label = document.createElement('strong');
58
- col3Label.textContent = 'Confidence: ';
59
- col3.appendChild(col3Label);
60
- col3.appendChild(document.createTextNode(result.confidence || 0));
61
- row1.appendChild(col3);
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
- cardBody.appendChild(row1);
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
- // Channel scores section
66
- var channels = result.channel_scores || {};
67
- var channelKeys = Object.keys(channels);
68
- if (channelKeys.length > 0) {
69
- var label = document.createElement('div');
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
- var row2 = document.createElement('div');
77
- row2.className = 'row text-muted';
78
- channelKeys.forEach(function(chKey) {
79
- var chCol = document.createElement('div');
80
- chCol.className = 'col-md-3';
81
- chCol.textContent = chKey + ': ' + channels[chKey];
82
- row2.appendChild(chCol);
83
- });
84
- cardBody.appendChild(row2);
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
- panel.appendChild(cardBody);
88
- item.appendChild(panel);
89
- }).catch(function(e) {
90
- console.log('Fact detail error:', e);
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
- async function loadMemories() {
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 url = '/api/memories?limit=50';
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(mem.id || '-'));
47
- addDetailBadgeRow(col1, 'Category', mem.category || 'None', 'bg-primary');
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'));