@simbimbo/memory-ocmemog 0.1.12 → 0.1.13
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 +14 -0
- package/ocmemog/runtime/config.py +0 -1
- package/ocmemog/runtime/inference.py +0 -1
- package/ocmemog/runtime/memory/api.py +169 -6
- package/ocmemog/runtime/memory/health.py +36 -0
- package/ocmemog/runtime/model_roles.py +0 -1
- package/ocmemog/runtime/model_router.py +0 -1
- package/ocmemog/runtime/providers.py +0 -1
- package/ocmemog/runtime/state_store.py +0 -2
- package/ocmemog/sidecar/app.py +111 -37
- package/ocmemog/sidecar/transcript_watcher.py +9 -2
- package/package.json +1 -1
- package/scripts/ocmemog-integrated-proof.py +5 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.13 — 2026-03-22
|
|
4
|
+
|
|
5
|
+
Final hardening release before a possible 1.0 cut.
|
|
6
|
+
|
|
7
|
+
### Highlights
|
|
8
|
+
- 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
|
|
9
|
+
- fixed a supersession-governance regression that could suppress `supersession_recommendation` generation and break governance queue/review/summary and auto-resolve flows
|
|
10
|
+
- moved dashboard supersession plain-English rewriting out of the render path so live dashboard loads stay fast while recommendations still carry human-readable text
|
|
11
|
+
- 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
|
|
12
|
+
- simplified Governance Review UI output into more concise, single-row plain-English review items
|
|
13
|
+
- hardened supersession summary generation against polluted transcript/log content with tighter preview normalization, aggressive noise stripping, bounded local-model rewriting, and safe heuristic fallbacks
|
|
14
|
+
- fixed dashboard cursor handling so the dashboard route tolerates minimal/mock DB cursor implementations used by tests
|
|
15
|
+
- hardened the integrated proof token/session identifiers to avoid fresh-state collisions during repeated release validation
|
|
16
|
+
|
|
3
17
|
## 0.1.12 — 2026-03-21
|
|
4
18
|
|
|
5
19
|
Release hardening, integrated proof validation, and native-ownership cleanup.
|
|
@@ -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")
|
|
@@ -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":
|
|
1085
|
+
"summary": summary,
|
|
925
1086
|
"actions": _review_actions(kind, relationship),
|
|
926
|
-
"source":
|
|
927
|
-
"target":
|
|
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
|
|
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
|
+
}
|
package/ocmemog/sidecar/app.py
CHANGED
|
@@ -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
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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.
|
|
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(
|
|
1647
|
-
"missing": max(int(counts.get(table, 0) or 0) - int(
|
|
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
|
-
|
|
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.
|
|
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>
|
|
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="
|
|
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="
|
|
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
|
|
1905
|
-
const
|
|
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(
|
|
1920
|
-
<span class="muted">${{escapeHtml(
|
|
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:
|
|
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="
|
|
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 =
|
|
122
|
-
|
|
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
|
@@ -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
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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.",
|