@simbimbo/memory-ocmemog 0.1.12 → 0.1.14

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 CHANGED
@@ -1,5 +1,27 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.14 — 2026-03-22
4
+
5
+ Corrective follow-up to make the published release fully version-aligned.
6
+
7
+ ### Highlights
8
+ - aligned package/source/documentation versioning after the 0.1.13 publish so source fallback version, README current-main note, and package metadata all agree on the shipped release
9
+ - preserves the full 0.1.13 hardening/test matrix and release validation while fixing version/documentation drift
10
+
11
+ ## 0.1.13 — 2026-03-22
12
+
13
+ Final hardening release before a possible 1.0 cut.
14
+
15
+ ### Highlights
16
+ - tested the full shipped surface aggressively: full pytest suite, release gate, live sidecar contract smoke, packaging dry-run, installer surfaces, and governance summary responsiveness checks all passed together
17
+ - fixed a supersession-governance regression that could suppress `supersession_recommendation` generation and break governance queue/review/summary and auto-resolve flows
18
+ - moved dashboard supersession plain-English rewriting out of the render path so live dashboard loads stay fast while recommendations still carry human-readable text
19
+ - added a lightweight cached governance review summary path for the dashboard, reducing review load time from multi-second scans to sub-second first load and near-instant cached refresh
20
+ - simplified Governance Review UI output into more concise, single-row plain-English review items
21
+ - hardened supersession summary generation against polluted transcript/log content with tighter preview normalization, aggressive noise stripping, bounded local-model rewriting, and safe heuristic fallbacks
22
+ - fixed dashboard cursor handling so the dashboard route tolerates minimal/mock DB cursor implementations used by tests
23
+ - hardened the integrated proof token/session identifiers to avoid fresh-state collisions during repeated release validation
24
+
3
25
  ## 0.1.12 — 2026-03-21
4
26
 
5
27
  Release hardening, integrated proof validation, and native-ownership cleanup.
package/README.md CHANGED
@@ -213,7 +213,7 @@ launchctl bootstrap gui/$UID scripts/launchagents/com.openclaw.ocmemog.guard.pli
213
213
 
214
214
  ## Recent changes
215
215
 
216
- ### 0.1.12 (current main)
216
+ ### 0.1.14 (current main)
217
217
 
218
218
  Current main now includes:
219
219
  - integrated release-gate validation with a fresh-state memory contract proof
@@ -7,6 +7,6 @@ from importlib.metadata import PackageNotFoundError, version as _package_version
7
7
  try:
8
8
  __version__ = _package_version("ocmemog-sidecar")
9
9
  except PackageNotFoundError: # pragma: no cover - package metadata may be unavailable in source layouts.
10
- __version__ = "0.1.12"
10
+ __version__ = "0.1.14"
11
11
 
12
12
  __all__ = ["__version__"]
@@ -4,7 +4,6 @@ from __future__ import annotations
4
4
 
5
5
  import os
6
6
 
7
- __wrapped_from__ = "brain.runtime.config"
8
7
 
9
8
  OCMEMOG_EMBED_MODEL_LOCAL = os.environ.get("OCMEMOG_EMBED_MODEL_LOCAL", "")
10
9
  OCMEMOG_EMBED_LOCAL_MODEL = OCMEMOG_EMBED_MODEL_LOCAL or os.environ.get("BRAIN_EMBED_MODEL_LOCAL", "simple")
@@ -9,7 +9,6 @@ import urllib.request
9
9
 
10
10
  from . import config, instrumentation, state_store
11
11
 
12
- __wrapped_from__ = "brain.runtime.inference"
13
12
 
14
13
  LOGFILE = state_store.report_log_path()
15
14
 
@@ -2,6 +2,9 @@ from __future__ import annotations
2
2
 
3
3
  import json
4
4
  import os
5
+ import re
6
+ import threading
7
+ from queue import Queue
5
8
  from typing import List, Dict, Any, Optional
6
9
 
7
10
  from ocmemog.runtime import inference
@@ -9,6 +12,8 @@ from ocmemog.runtime.instrumentation import emit_event
9
12
  from . import provenance, store
10
13
  from ocmemog.runtime.security import redaction
11
14
 
15
+ _SUPERSESSION_SUMMARY_CACHE: Dict[str, str] = {}
16
+
12
17
  _REVIEW_KIND_METADATA: Dict[str, Dict[str, str]] = {
13
18
  "duplicate_candidate": {
14
19
  "relationship": "duplicate_of",
@@ -36,6 +41,26 @@ def _sanitize(text: str) -> str:
36
41
  return redacted
37
42
 
38
43
 
44
+ def _run_with_timeout(fn, timeout_s: float, default: Any) -> Any:
45
+ result_queue: Queue[tuple[str, Any]] = Queue(maxsize=1)
46
+
47
+ def _target() -> None:
48
+ try:
49
+ result_queue.put(("ok", fn()))
50
+ except Exception as exc:
51
+ result_queue.put(("error", exc))
52
+
53
+ worker = threading.Thread(target=_target, name="ocmemog-governance-summary", daemon=True)
54
+ worker.start()
55
+ worker.join(timeout_s)
56
+ if worker.is_alive() or result_queue.empty():
57
+ return default
58
+ status, payload = result_queue.get_nowait()
59
+ if status != "ok":
60
+ return default
61
+ return payload
62
+
63
+
39
64
  def _parse_memory_reference(reference: str) -> tuple[str, str] | None:
40
65
  if ":" not in str(reference or ""):
41
66
  return None
@@ -320,6 +345,17 @@ def _auto_attach_governance_candidates(reference: str, *, use_model: bool = True
320
345
  duplicate_candidates=duplicate_candidates,
321
346
  contradiction_candidates=contradiction_candidates,
322
347
  )
348
+ if supersession_recommendation.get("recommended"):
349
+ target_reference = str(supersession_recommendation.get("target_reference") or "").strip()
350
+ target_payload = provenance.fetch_reference(target_reference) or {} if target_reference else {}
351
+ supersession_recommendation["plain_english"] = _plain_english_supersession_summary(
352
+ reference=reference,
353
+ target_reference=target_reference,
354
+ source_content=str(payload.get("content") or ""),
355
+ target_content=str(target_payload.get("content") or ""),
356
+ reason=str(supersession_recommendation.get("reason") or ""),
357
+ )
358
+
323
359
  supersession_recommendation = _auto_apply_supersession_recommendation(
324
360
  reference,
325
361
  contradiction_candidates=contradiction_candidates,
@@ -841,6 +877,23 @@ def _remove_from_list(values: Any, target: str) -> List[str]:
841
877
 
842
878
 
843
879
  def _review_item_context(reference: str, *, depth: int = 1) -> Dict[str, Any]:
880
+ if depth <= 0:
881
+ payload = provenance.fetch_reference(reference) or {"reference": reference}
882
+ metadata = payload.get("metadata") if isinstance(payload.get("metadata"), dict) else {}
883
+ prov = metadata.get("provenance") if isinstance(metadata.get("provenance"), dict) else {}
884
+ return {
885
+ "reference": reference,
886
+ "bucket": payload.get("table"),
887
+ "id": payload.get("id"),
888
+ "timestamp": payload.get("timestamp"),
889
+ "content": payload.get("content"),
890
+ "memory_status": prov.get("memory_status") or metadata.get("memory_status") or "active",
891
+ "provenance_preview": provenance.preview_from_metadata(metadata),
892
+ "metadata": metadata,
893
+ "links": [],
894
+ "backlinks": [],
895
+ }
896
+
844
897
  payload = provenance.hydrate_reference(reference, depth=depth) or {"reference": reference}
845
898
  metadata = payload.get("metadata") if isinstance(payload.get("metadata"), dict) else {}
846
899
  prov = metadata.get("provenance") if isinstance(metadata.get("provenance"), dict) else {}
@@ -868,6 +921,106 @@ def _review_item_summary(kind: str, reference: str, target_reference: str) -> st
868
921
  return f"{reference} requires review against {target_reference}"
869
922
 
870
923
 
924
+ def _normalize_supersession_preview(text: str, fallback: str) -> str:
925
+ raw = str(text or "").strip()
926
+ if not raw:
927
+ return fallback
928
+ cleaned = _sanitize(raw)
929
+ cleaned = re.sub(r"\[\[reply_to_current\]\]", "", cleaned, flags=re.IGNORECASE)
930
+ cleaned = re.sub(r"\([^\)]*assistant\)", "", cleaned, flags=re.IGNORECASE)
931
+ cleaned = re.sub(r"\b\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z\b", "", cleaned)
932
+ cleaned = re.sub(r"https?://\S+", "", cleaned)
933
+ cleaned = re.sub(r"\s+", " ", cleaned).strip()
934
+ lower = cleaned.lower()
935
+ noisy_markers = [
936
+ "provider monitor",
937
+ "memory-sync",
938
+ "launchctl",
939
+ "reply_to_current",
940
+ "openclaw gateway restart",
941
+ "python3 -u -m",
942
+ "logged to",
943
+ "checkpoint saved",
944
+ ]
945
+ if any(marker in lower for marker in noisy_markers):
946
+ return fallback
947
+ if len(cleaned) > 140:
948
+ sentence = re.split(r"(?<=[.!?])\s+", cleaned, maxsplit=1)[0].strip()
949
+ cleaned = sentence or cleaned[:140]
950
+ cleaned = cleaned[:140].rstrip(" ,;:-")
951
+ return cleaned or fallback
952
+
953
+
954
+ def _heuristic_supersession_summary(
955
+ reference: str,
956
+ target_reference: str,
957
+ source_content: str,
958
+ target_content: str,
959
+ reason: str,
960
+ ) -> str:
961
+ source = _normalize_supersession_preview(source_content, "a newer consolidated memory")
962
+ target = _normalize_supersession_preview(target_content, "an older noisier memory")
963
+ because = _normalize_supersession_preview(reason, "the newer memory appears cleaner and more useful")
964
+ return f"This newer memory probably replaces an older one: new = {source}; old = {target}; reason = {because}."[:220].rstrip()
965
+
966
+
967
+ def _plain_english_supersession_summary(
968
+ *,
969
+ reference: str,
970
+ target_reference: str,
971
+ source_content: str,
972
+ target_content: str,
973
+ reason: str,
974
+ ) -> str:
975
+ cache_key = json.dumps(
976
+ {
977
+ "reference": reference,
978
+ "target_reference": target_reference,
979
+ "source_content": source_content,
980
+ "target_content": target_content,
981
+ "reason": reason,
982
+ },
983
+ sort_keys=True,
984
+ ensure_ascii=False,
985
+ )
986
+ cached = _SUPERSESSION_SUMMARY_CACHE.get(cache_key)
987
+ if cached:
988
+ return cached
989
+
990
+ fallback = _heuristic_supersession_summary(reference, target_reference, source_content, target_content, reason)
991
+ source_preview = _normalize_supersession_preview(source_content, "a newer consolidated memory")
992
+ target_preview = _normalize_supersession_preview(target_content, "an older noisier memory")
993
+ reason_preview = _normalize_supersession_preview(reason, "the newer memory appears cleaner and more useful")
994
+ prompt = (
995
+ "Rewrite this supersession recommendation as exactly one short plain-English sentence for a human dashboard. "
996
+ "Describe the relationship only. Do not quote or repeat full memory contents. Do not use JSON, bullets, markdown, timestamps, or command text. Keep it under 160 characters.\n\n"
997
+ f"Newer candidate reference: {reference}\n"
998
+ f"Potentially replaced reference: {target_reference}\n"
999
+ f"Newer candidate preview: {source_preview}\n"
1000
+ f"Older candidate preview: {target_preview}\n"
1001
+ f"Recommendation reason: {reason_preview}\n\n"
1002
+ "Plain-English dashboard sentence:"
1003
+ )
1004
+ result = _run_with_timeout(
1005
+ lambda: inference.infer(
1006
+ prompt,
1007
+ provider_name=os.environ.get("OCMEMOG_PONDER_MODEL", "local-openai:qwen2.5-7b-instruct"),
1008
+ ),
1009
+ 1.5,
1010
+ {"status": "timeout", "output": ""},
1011
+ )
1012
+ output = str((result or {}).get("output") or "").strip()
1013
+ cleaned = output.replace("\n", " ").strip(" -:\t")
1014
+ cleaned = re.sub(r"^(summary|sentence|plain-english dashboard sentence)\s*:\s*", "", cleaned, flags=re.IGNORECASE).strip()
1015
+ cleaned = _normalize_supersession_preview(cleaned, "")
1016
+ if cleaned and len(cleaned) >= 24:
1017
+ summary = cleaned[:160]
1018
+ else:
1019
+ summary = fallback
1020
+ _SUPERSESSION_SUMMARY_CACHE[cache_key] = summary
1021
+ return summary
1022
+
1023
+
871
1024
  def _review_actions(kind: str, relationship: str) -> List[Dict[str, Any]]:
872
1025
  meta = _REVIEW_KIND_METADATA.get(kind, {})
873
1026
  return [
@@ -899,8 +1052,9 @@ def list_governance_review_items(
899
1052
  categories: Optional[List[str]] = None,
900
1053
  limit: int = 100,
901
1054
  context_depth: int = 1,
1055
+ scan_limit: int = 3000,
902
1056
  ) -> List[Dict[str, Any]]:
903
- items = governance_queue(categories=categories, limit=limit)
1057
+ items = governance_queue(categories=categories, limit=limit, scan_limit=scan_limit)
904
1058
  review_items: List[Dict[str, Any]] = []
905
1059
  for item in items:
906
1060
  kind = str(item.get("kind") or "")
@@ -909,6 +1063,13 @@ def list_governance_review_items(
909
1063
  target_reference = str(item.get("target_reference") or "")
910
1064
  if not reference or not target_reference or not relationship:
911
1065
  continue
1066
+ source = _review_item_context(reference, depth=context_depth)
1067
+ target = _review_item_context(target_reference, depth=context_depth)
1068
+ summary = _review_item_summary(kind, reference, target_reference)
1069
+ if kind == "supersession_recommendation":
1070
+ plain_english = str(item.get("plain_english") or "").strip()
1071
+ if plain_english:
1072
+ summary = plain_english
912
1073
  review_items.append({
913
1074
  "review_id": f"{kind}:{reference}->{target_reference}",
914
1075
  "kind": kind,
@@ -921,10 +1082,10 @@ def list_governance_review_items(
921
1082
  "reason": item.get("reason"),
922
1083
  "reference": reference,
923
1084
  "target_reference": target_reference,
924
- "summary": _review_item_summary(kind, reference, target_reference),
1085
+ "summary": summary,
925
1086
  "actions": _review_actions(kind, relationship),
926
- "source": _review_item_context(reference, depth=context_depth),
927
- "target": _review_item_context(target_reference, depth=context_depth),
1087
+ "source": source,
1088
+ "target": target,
928
1089
  })
929
1090
  return review_items
930
1091
 
@@ -1073,15 +1234,17 @@ def rollback_governance_decision(
1073
1234
  return None
1074
1235
 
1075
1236
 
1076
- def governance_queue(*, categories: Optional[List[str]] = None, limit: int = 100) -> List[Dict[str, Any]]:
1237
+ def governance_queue(*, categories: Optional[List[str]] = None, limit: int = 100, scan_limit: int = 3000) -> List[Dict[str, Any]]:
1077
1238
  allowed = set(store.MEMORY_TABLES)
1078
1239
  tables = [table for table in (categories or list(allowed)) if table in allowed]
1240
+ per_table_scan_limit = max(int(scan_limit or 0), max(int(limit or 0), 1))
1079
1241
  conn = store.connect()
1080
1242
  try:
1081
1243
  items: List[Dict[str, Any]] = []
1082
1244
  for table in tables:
1083
1245
  rows = conn.execute(
1084
- f"SELECT id, timestamp, content, metadata_json FROM {table} ORDER BY id DESC LIMIT 3000"
1246
+ f"SELECT id, timestamp, content, metadata_json FROM {table} ORDER BY id DESC LIMIT ?",
1247
+ (per_table_scan_limit,),
1085
1248
  ).fetchall()
1086
1249
  for row in rows:
1087
1250
  reference = f"{table}:{row['id'] if isinstance(row, dict) else row[0]}"
@@ -55,3 +55,39 @@ def get_memory_health() -> Dict[str, Any]:
55
55
  "vector_index_integrity_status": integrity_result.get("ok"),
56
56
  "integrity": integrity_result,
57
57
  }
58
+
59
+
60
+ def get_memory_health_fast() -> Dict[str, Any]:
61
+ conn = store.connect()
62
+ counts: Dict[str, int] = {}
63
+ try:
64
+ for table in ["experiences", "candidates", "promotions", "memory_index", *store.MEMORY_TABLES, "vector_embeddings"]:
65
+ try:
66
+ counts[table] = conn.execute(f"SELECT COUNT(*) FROM {table}").fetchone()[0]
67
+ except Exception:
68
+ counts[table] = 0
69
+
70
+ vector_index_count = 0
71
+ for table in EMBED_TABLES:
72
+ try:
73
+ vector_index_count += conn.execute(
74
+ "SELECT COUNT(*) FROM vector_embeddings WHERE source_type=?",
75
+ (table,),
76
+ ).fetchone()[0]
77
+ except Exception:
78
+ continue
79
+ finally:
80
+ conn.close()
81
+
82
+ total_embed_sources = sum(counts.get(table, 0) for table in EMBED_TABLES)
83
+ coverage = 0.0
84
+ if total_embed_sources:
85
+ coverage = round(vector_index_count / total_embed_sources, 3)
86
+
87
+ return {
88
+ "counts": counts,
89
+ "vector_index_count": vector_index_count,
90
+ "vector_index_coverage": coverage,
91
+ "vector_index_integrity_status": None,
92
+ "integrity": {"ok": None, "mode": "deferred"},
93
+ }
@@ -4,7 +4,6 @@ from __future__ import annotations
4
4
 
5
5
  from . import config
6
6
 
7
- __wrapped_from__ = "brain.runtime.model_roles"
8
7
 
9
8
 
10
9
  def get_model_for_role(role: str) -> str:
@@ -6,7 +6,6 @@ from dataclasses import dataclass
6
6
 
7
7
  from . import config
8
8
 
9
- __wrapped_from__ = "brain.runtime.model_router"
10
9
 
11
10
 
12
11
  @dataclass(frozen=True)
@@ -8,7 +8,6 @@ import urllib.request
8
8
 
9
9
  from . import config, instrumentation, state_store
10
10
 
11
- __wrapped_from__ = "brain.runtime.providers"
12
11
 
13
12
  LOGFILE = state_store.report_log_path()
14
13
 
@@ -6,8 +6,6 @@ from pathlib import Path
6
6
 
7
7
  from . import storage_paths
8
8
 
9
- __wrapped_from__ = "brain.runtime.state_store"
10
- __wrapped_by__ = "ocmemog-runtime-bridge"
11
9
 
12
10
 
13
11
  def root_dir() -> Path:
@@ -6,6 +6,7 @@ import faulthandler
6
6
  import os
7
7
  import re
8
8
  import threading
9
+ import tempfile
9
10
  import time
10
11
  import sys
11
12
  from contextlib import asynccontextmanager
@@ -37,6 +38,8 @@ from ocmemog.sidecar.transcript_watcher import watch_forever
37
38
  DEFAULT_CATEGORIES = tuple(store.MEMORY_TABLES)
38
39
 
39
40
  API_TOKEN = os.environ.get("OCMEMOG_API_TOKEN")
41
+ _GOVERNANCE_REVIEW_CACHE_TTL_SECONDS = 15.0
42
+ _governance_review_cache: Dict[str, Any] = {"key": None, "expires_at": 0.0, "payload": None}
40
43
 
41
44
 
42
45
  _BOOL_TRUE_VALUES = {"1", "true", "yes", "on", "y", "t"}
@@ -198,9 +201,14 @@ def _load_queue_stats() -> None:
198
201
 
199
202
  def _save_queue_stats() -> None:
200
203
  path = _queue_stats_path()
201
- tmp = path.with_suffix('.tmp')
202
- tmp.write_text(json.dumps(QUEUE_STATS, indent=2, sort_keys=True), encoding='utf-8')
203
- tmp.replace(path)
204
+ path.parent.mkdir(parents=True, exist_ok=True)
205
+ payload = json.dumps(QUEUE_STATS, indent=2, sort_keys=True)
206
+ with tempfile.NamedTemporaryFile('w', encoding='utf-8', dir=str(path.parent), prefix='queue_stats.', suffix='.tmp', delete=False) as handle:
207
+ handle.write(payload)
208
+ handle.flush()
209
+ os.fsync(handle.fileno())
210
+ tmp_name = handle.name
211
+ Path(tmp_name).replace(path)
204
212
 
205
213
 
206
214
  @app.middleware("http")
@@ -546,6 +554,7 @@ class GovernanceReviewRequest(BaseModel):
546
554
  categories: Optional[List[str]] = None
547
555
  limit: int = Field(default=100, ge=1, le=500)
548
556
  context_depth: int = Field(default=1, ge=0, le=2)
557
+ scan_limit: int = Field(default=3000, ge=1, le=10000)
549
558
 
550
559
 
551
560
  class GovernanceDecisionRequest(BaseModel):
@@ -1044,17 +1053,59 @@ def memory_governance_review(request: GovernanceReviewRequest) -> dict[str, Any]
1044
1053
  categories=request.categories,
1045
1054
  limit=request.limit,
1046
1055
  context_depth=request.context_depth,
1056
+ scan_limit=request.scan_limit,
1047
1057
  )
1048
1058
  return {
1049
1059
  "ok": True,
1050
1060
  "categories": request.categories,
1051
1061
  "limit": request.limit,
1052
1062
  "context_depth": request.context_depth,
1063
+ "scan_limit": request.scan_limit,
1053
1064
  "items": items,
1054
1065
  **runtime,
1055
1066
  }
1056
1067
 
1057
1068
 
1069
+ @app.post("/memory/governance/review/summary")
1070
+ def memory_governance_review_summary(request: GovernanceReviewRequest) -> dict[str, Any]:
1071
+ runtime = _runtime_payload()
1072
+ limit = min(int(request.limit or 25), 50)
1073
+ scan_limit = min(int(request.scan_limit or max(limit * 10, 250)), 500)
1074
+ cache_key = json.dumps(
1075
+ {
1076
+ "categories": sorted(request.categories or []),
1077
+ "limit": limit,
1078
+ "context_depth": 0,
1079
+ "scan_limit": scan_limit,
1080
+ },
1081
+ sort_keys=True,
1082
+ )
1083
+ now = time.time()
1084
+ if _governance_review_cache.get("key") == cache_key and float(_governance_review_cache.get("expires_at") or 0.0) > now:
1085
+ cached_payload = _governance_review_cache.get("payload") or {}
1086
+ return {**cached_payload, **runtime, "cached": True}
1087
+
1088
+ items = api.list_governance_review_items(
1089
+ categories=request.categories,
1090
+ limit=limit,
1091
+ context_depth=0,
1092
+ scan_limit=scan_limit,
1093
+ )
1094
+ payload = {
1095
+ "ok": True,
1096
+ "categories": request.categories,
1097
+ "limit": limit,
1098
+ "context_depth": 0,
1099
+ "scan_limit": scan_limit,
1100
+ "items": items,
1101
+ "cached": False,
1102
+ }
1103
+ _governance_review_cache.update(
1104
+ {"key": cache_key, "expires_at": now + _GOVERNANCE_REVIEW_CACHE_TTL_SECONDS, "payload": payload}
1105
+ )
1106
+ return {**payload, **runtime}
1107
+
1108
+
1058
1109
  @app.post("/memory/governance/decision")
1059
1110
  def memory_governance_decision(request: GovernanceDecisionRequest) -> dict[str, Any]:
1060
1111
  runtime = _runtime_payload()
@@ -1630,26 +1681,29 @@ def memory_distill(request: DistillRequest) -> dict[str, Any]:
1630
1681
  @app.get("/metrics")
1631
1682
  def metrics() -> dict[str, Any]:
1632
1683
  runtime = _runtime_payload()
1633
- payload = health.get_memory_health()
1684
+ payload = health.get_memory_health_fast()
1634
1685
  counts = payload.get("counts", {})
1635
1686
  counts["queue_depth"] = _queue_depth()
1636
1687
  counts["queue_processed"] = QUEUE_STATS.get("processed", 0)
1637
1688
  counts["queue_errors"] = QUEUE_STATS.get("errors", 0)
1638
1689
  payload["counts"] = counts
1690
+
1639
1691
  coverage_tables = list(store.MEMORY_TABLES)
1640
1692
  conn = store.connect()
1641
1693
  try:
1694
+ vector_counts: Dict[str, int] = {str(row[0]): int(row[1] or 0) for row in conn.execute("SELECT source_type, COUNT(*) FROM vector_embeddings GROUP BY source_type")}
1642
1695
  payload["coverage"] = [
1643
1696
  {
1644
1697
  "table": table,
1645
1698
  "rows": int(counts.get(table, 0) or 0),
1646
- "vectors": int(conn.execute("SELECT COUNT(*) FROM vector_embeddings WHERE source_type=?", (table,)).fetchone()[0] or 0),
1647
- "missing": max(int(counts.get(table, 0) or 0) - int(conn.execute("SELECT COUNT(*) FROM vector_embeddings WHERE source_type=?", (table,)).fetchone()[0] or 0), 0),
1699
+ "vectors": int(vector_counts.get(table, 0) or 0),
1700
+ "missing": max(int(counts.get(table, 0) or 0) - int(vector_counts.get(table, 0) or 0), 0),
1648
1701
  }
1649
1702
  for table in coverage_tables
1650
1703
  ]
1651
1704
  finally:
1652
1705
  conn.close()
1706
+
1653
1707
  payload["queue"] = QUEUE_STATS
1654
1708
  return {"ok": True, "metrics": payload, **runtime}
1655
1709
 
@@ -1679,7 +1733,16 @@ def _tail_events(limit: int = 50) -> str:
1679
1733
  if not path.exists():
1680
1734
  return ""
1681
1735
  try:
1682
- lines = path.read_text(encoding="utf-8", errors="ignore").splitlines()
1736
+ size = path.stat().st_size
1737
+ # Read only the trailing chunk to avoid loading very large logs.
1738
+ # This bounds dashboard latency even when the report log grows huge.
1739
+ max_bytes = 256 * 1024
1740
+ with path.open("rb") as handle:
1741
+ if size > max_bytes:
1742
+ handle.seek(-max_bytes, 2)
1743
+ data = handle.read()
1744
+ text = data.decode("utf-8", errors="ignore")
1745
+ lines = text.splitlines()
1683
1746
  except Exception as exc:
1684
1747
  print(f"[ocmemog][events] tail_read_failed path={path} error={exc!r}", file=sys.stderr)
1685
1748
  return ""
@@ -1688,21 +1751,33 @@ def _tail_events(limit: int = 50) -> str:
1688
1751
 
1689
1752
  @app.get("/dashboard")
1690
1753
  def dashboard() -> HTMLResponse:
1691
- metrics_payload = health.get_memory_health()
1754
+ metrics_payload = health.get_memory_health_fast()
1692
1755
  counts = metrics_payload.get("counts", {})
1693
1756
  coverage_tables = list(store.MEMORY_TABLES)
1694
1757
  conn = store.connect()
1695
1758
  try:
1759
+ cursor = conn.execute("SELECT source_type, COUNT(*) FROM vector_embeddings GROUP BY source_type")
1760
+ try:
1761
+ vector_rows = list(cursor)
1762
+ except TypeError:
1763
+ fetchall = getattr(cursor, "fetchall", None)
1764
+ if callable(fetchall):
1765
+ vector_rows = fetchall()
1766
+ else:
1767
+ fetchone = getattr(cursor, "fetchone", None)
1768
+ row = fetchone() if callable(fetchone) else None
1769
+ vector_rows = [row] if row is not None else []
1770
+ vector_counts: Dict[str, int] = {}
1771
+ for row in vector_rows:
1772
+ if not isinstance(row, (list, tuple)) or len(row) < 2:
1773
+ continue
1774
+ vector_counts[str(row[0])] = int(row[1] or 0)
1775
+ if hasattr(cursor, "close"):
1776
+ cursor.close()
1696
1777
  coverage_rows = []
1697
1778
  for table in coverage_tables:
1698
1779
  total = int(counts.get(table, 0) or 0)
1699
- vectors = int(
1700
- conn.execute(
1701
- "SELECT COUNT(*) FROM vector_embeddings WHERE source_type=?",
1702
- (table,),
1703
- ).fetchone()[0]
1704
- or 0
1705
- )
1780
+ vectors = int(vector_counts.get(table, 0) or 0)
1706
1781
  missing = max(total - vectors, 0)
1707
1782
  coverage_rows.append({"table": table, "rows": total, "vectors": vectors, "missing": missing})
1708
1783
  finally:
@@ -1804,15 +1879,12 @@ def dashboard() -> HTMLResponse:
1804
1879
  <thead>
1805
1880
  <tr>
1806
1881
  <th>Priority</th>
1807
- <th>Kind</th>
1808
- <th>Source</th>
1809
- <th>Target</th>
1810
- <th>Summary</th>
1882
+ <th>Review</th>
1811
1883
  <th>Actions</th>
1812
1884
  </tr>
1813
1885
  </thead>
1814
1886
  <tbody id="review-table-body">
1815
- <tr><td colspan="6" class="muted">Loading...</td></tr>
1887
+ <tr><td colspan="3" class="muted">Loading...</td></tr>
1816
1888
  </tbody>
1817
1889
  </table>
1818
1890
  </div>
@@ -1878,6 +1950,17 @@ def dashboard() -> HTMLResponse:
1878
1950
  return Number.isNaN(parsed.getTime()) ? String(value) : parsed.toLocaleString();
1879
1951
  }}
1880
1952
 
1953
+ function summarizeReviewItem(item) {{
1954
+ const sourceRef = item.source?.reference || item.reference || 'source memory';
1955
+ const targetRef = item.target?.reference || item.target_reference || 'target memory';
1956
+ const sourceText = item.source?.content || sourceRef;
1957
+ const targetText = item.target?.content || targetRef;
1958
+ const relation = item.relationship || (item.kind_label || item.kind || 'relationship').toLowerCase();
1959
+ const when = item.timestamp ? ` Reviewed signal from ${{formatTimestamp(item.timestamp)}}.` : '';
1960
+ const signal = item.signal ? ` Signal score: ${{item.signal}}.` : '';
1961
+ return `${{sourceRef}} may ${{relation.replaceAll('_', ' ')}} ${{targetRef}}. Source: “${{sourceText}}” Target: “${{targetText}}”.${{signal}}${{when}}`;
1962
+ }}
1963
+
1881
1964
  function renderReviewTable() {{
1882
1965
  const kindFilter = reviewKindFilterEl.value;
1883
1966
  const priorityFilter = reviewPriorityFilterEl.value;
@@ -1895,29 +1978,20 @@ def dashboard() -> HTMLResponse:
1895
1978
  reviewNoteEl.textContent = `${{filtered.length}} items shown${{reviewItems.length !== filtered.length ? ` of ${{reviewItems.length}}` : ''}} • Last refresh: ${{reviewLastRefresh ? formatTimestamp(reviewLastRefresh) : 'n/a'}}`;
1896
1979
 
1897
1980
  if (!filtered.length) {{
1898
- reviewTableBodyEl.innerHTML = '<tr><td colspan="6" class="muted">No review items match the current filters.</td></tr>';
1981
+ reviewTableBodyEl.innerHTML = '<tr><td colspan="3" class="muted">No review items match the current filters.</td></tr>';
1899
1982
  return;
1900
1983
  }}
1901
1984
 
1902
1985
  reviewTableBodyEl.innerHTML = filtered.map((item) => {{
1903
1986
  const disabled = pendingReviewIds.has(item.review_id) ? 'disabled' : '';
1904
- const sourceContent = item.source?.content || item.reference;
1905
- const targetContent = item.target?.content || item.target_reference;
1987
+ const reviewText = summarizeReviewItem(item);
1988
+ const summaryBits = [item.kind_label || item.kind, item.summary].filter(Boolean).join(' • ');
1906
1989
  return `
1907
1990
  <tr>
1908
1991
  <td>${{escapeHtml(item.priority)}}</td>
1909
- <td>${{escapeHtml(item.kind_label || item.kind)}}</td>
1910
- <td>
1911
- <strong>${{escapeHtml(item.reference)}}</strong><br/>
1912
- <span class="muted">${{escapeHtml(sourceContent)}}</span>
1913
- </td>
1914
- <td>
1915
- <strong>${{escapeHtml(item.target_reference)}}</strong><br/>
1916
- <span class="muted">${{escapeHtml(targetContent)}}</span>
1917
- </td>
1918
1992
  <td>
1919
- <strong>${{escapeHtml(item.summary || '')}}</strong><br/>
1920
- <span class="muted">${{escapeHtml(item.relationship || '')}}${{item.signal ? ` • signal ${{item.signal}}` : ''}}</span>
1993
+ <strong>${{escapeHtml(summaryBits || 'Governance review item')}}</strong><br/>
1994
+ <span class="muted">${{escapeHtml(reviewText)}}</span>
1921
1995
  </td>
1922
1996
  <td>
1923
1997
  <button type="button" data-review-id="${{escapeHtml(item.review_id)}}" data-approved="true" ${{disabled}}>Approve</button>
@@ -1962,10 +2036,10 @@ def dashboard() -> HTMLResponse:
1962
2036
  async function refreshGovernanceReview() {{
1963
2037
  reviewErrorEl.textContent = '';
1964
2038
  try {{
1965
- const res = await fetch('/memory/governance/review', {{
2039
+ const res = await fetch('/memory/governance/review/summary', {{
1966
2040
  method: 'POST',
1967
2041
  headers: {{ 'Content-Type': 'application/json' }},
1968
- body: JSON.stringify({{ limit: 100, context_depth: 1 }}),
2042
+ body: JSON.stringify({{ limit: 20, context_depth: 0, scan_limit: 250 }}),
1969
2043
  }});
1970
2044
  const data = await res.json();
1971
2045
  if (!res.ok || !data.ok) {{
@@ -1976,7 +2050,7 @@ def dashboard() -> HTMLResponse:
1976
2050
  renderReviewTable();
1977
2051
  }} catch (error) {{
1978
2052
  reviewErrorEl.textContent = error instanceof Error ? error.message : String(error);
1979
- reviewTableBodyEl.innerHTML = '<tr><td colspan="6" class="muted">Unable to load review items.</td></tr>';
2053
+ reviewTableBodyEl.innerHTML = '<tr><td colspan="3" class="muted">Unable to load review items.</td></tr>';
1980
2054
  }}
1981
2055
  }}
1982
2056
 
@@ -118,8 +118,15 @@ def _pick_latest(path: Path, pattern: str) -> Optional[Path]:
118
118
  return path
119
119
  if not path.exists():
120
120
  return None
121
- files = sorted(path.glob(pattern), key=lambda p: p.stat().st_mtime)
122
- return files[-1] if files else None
121
+ files = []
122
+ for candidate in path.glob(pattern):
123
+ try:
124
+ mtime = candidate.stat().st_mtime
125
+ except FileNotFoundError:
126
+ continue
127
+ files.append((mtime, candidate))
128
+ files.sort(key=lambda item: item[0])
129
+ return files[-1][1] if files else None
123
130
 
124
131
 
125
132
  def _apply_auth_headers(req: urlrequest.Request) -> None:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@simbimbo/memory-ocmemog",
3
- "version": "0.1.12",
3
+ "version": "0.1.14",
4
4
  "description": "Advanced OpenClaw memory plugin with durable recall, transcript-backed continuity, and sidecar APIs",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -215,10 +215,11 @@ def _derive_port(endpoint: str) -> int | None:
215
215
 
216
216
 
217
217
  def run_probe(endpoint: str) -> dict[str, Any]:
218
- token = "proof-token-demo-12345"
219
- conversation = "proof-conv-demo"
220
- session = "proof-sess-demo"
221
- thread = "proof-thread-demo"
218
+ nonce = uuid.uuid4().hex
219
+ token = f"proof-token-{nonce}"
220
+ conversation = f"proof-conv-{nonce}"
221
+ session = f"proof-sess-{nonce}"
222
+ thread = f"proof-thread-{nonce}"
222
223
 
223
224
  ingest_payload = {
224
225
  "content": f"I learned that the {token} is the canonical token for this verification.",