@simbimbo/memory-ocmemog 0.1.8 → 0.1.9
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 +12 -0
- package/brain/runtime/memory/api.py +270 -23
- package/brain/runtime/memory/candidate.py +101 -3
- package/brain/runtime/memory/conversation_state.py +1 -1
- package/brain/runtime/memory/distill.py +6 -1
- package/brain/runtime/memory/health.py +3 -3
- package/brain/runtime/memory/integrity.py +3 -1
- package/brain/runtime/memory/pondering_engine.py +1 -1
- package/brain/runtime/memory/promote.py +39 -3
- package/brain/runtime/memory/provenance.py +1 -1
- package/brain/runtime/memory/retrieval.py +1 -8
- package/brain/runtime/memory/store.py +31 -0
- package/brain/runtime/memory/vector_index.py +1 -8
- package/ocmemog/sidecar/app.py +334 -14
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.9 — 2026-03-19
|
|
4
|
+
|
|
5
|
+
Memory quality, governance, and review release.
|
|
6
|
+
|
|
7
|
+
### Highlights
|
|
8
|
+
- added near-duplicate collapse for transcript/session double-ingest candidate generation
|
|
9
|
+
- added conservative reflection reclassification and new durable buckets for `preferences` and `identity`
|
|
10
|
+
- wired new buckets through storage, retrieval, embeddings, health, integrity, and promotion/demotion paths
|
|
11
|
+
- hardened governance auto-promotion for duplicates and supersessions with stricter thresholds and guardrails
|
|
12
|
+
- added governance review endpoints plus dashboard review panel with filters and approve/reject actions
|
|
13
|
+
- fixed release-blocking distill fallback behavior in no-model environments and removed stale hard-coded bucket drift
|
|
14
|
+
|
|
3
15
|
## 0.1.8 — 2026-03-19
|
|
4
16
|
|
|
5
17
|
Documentation and release follow-through after the llama.cpp migration and repo grooming pass.
|
|
@@ -9,6 +9,27 @@ from brain.runtime import inference
|
|
|
9
9
|
from brain.runtime.instrumentation import emit_event
|
|
10
10
|
from brain.runtime.security import redaction
|
|
11
11
|
|
|
12
|
+
_REVIEW_KIND_METADATA: Dict[str, Dict[str, str]] = {
|
|
13
|
+
"duplicate_candidate": {
|
|
14
|
+
"relationship": "duplicate_of",
|
|
15
|
+
"label": "Duplicate candidate",
|
|
16
|
+
"approve_label": "Approve duplicate merge",
|
|
17
|
+
"reject_label": "Reject duplicate merge",
|
|
18
|
+
},
|
|
19
|
+
"contradiction_candidate": {
|
|
20
|
+
"relationship": "contradicts",
|
|
21
|
+
"label": "Contradiction candidate",
|
|
22
|
+
"approve_label": "Mark as contradiction",
|
|
23
|
+
"reject_label": "Dismiss contradiction",
|
|
24
|
+
},
|
|
25
|
+
"supersession_recommendation": {
|
|
26
|
+
"relationship": "supersedes",
|
|
27
|
+
"label": "Supersession recommendation",
|
|
28
|
+
"approve_label": "Approve supersession",
|
|
29
|
+
"reject_label": "Dismiss supersession",
|
|
30
|
+
},
|
|
31
|
+
}
|
|
32
|
+
|
|
12
33
|
|
|
13
34
|
def _sanitize(text: str) -> str:
|
|
14
35
|
redacted, _ = redaction.redact_text(text)
|
|
@@ -72,8 +93,6 @@ def _recommend_supersession_from_contradictions(
|
|
|
72
93
|
|
|
73
94
|
signal_threshold = float(os.environ.get("OCMEMOG_GOVERNANCE_SUPERSESSION_RECOMMEND_SIGNAL", "0.9") or 0.9)
|
|
74
95
|
model_conf_threshold = float(os.environ.get("OCMEMOG_GOVERNANCE_SUPERSESSION_MODEL_CONFIDENCE", "0.9") or 0.9)
|
|
75
|
-
auto_apply = os.environ.get("OCMEMOG_GOVERNANCE_AUTOPROMOTE_SUPERSESSION", "false").strip().lower() in {"1", "true", "yes"}
|
|
76
|
-
|
|
77
96
|
ranked = sorted(contradiction_candidates, key=lambda item: float(item.get("signal") or 0.0), reverse=True)
|
|
78
97
|
top = ranked[0]
|
|
79
98
|
signal = float(top.get("signal") or 0.0)
|
|
@@ -105,28 +124,38 @@ def _recommend_supersession_from_contradictions(
|
|
|
105
124
|
"model_hint": model_hint,
|
|
106
125
|
})
|
|
107
126
|
|
|
108
|
-
if auto_apply:
|
|
109
|
-
merged = mark_memory_relationship(reference, relationship="supersedes", target_reference=target, status="active")
|
|
110
|
-
recommendation["auto_applied"] = merged is not None
|
|
111
|
-
recommendation["reason"] = "auto_applied_supersession" if merged is not None else "auto_apply_failed"
|
|
112
|
-
|
|
113
127
|
return recommendation
|
|
114
128
|
|
|
115
129
|
|
|
116
|
-
def
|
|
130
|
+
def _canonicalize_duplicate_target(reference: str) -> str:
|
|
131
|
+
payload = provenance.fetch_reference(reference) or {}
|
|
132
|
+
metadata = payload.get("metadata") or {}
|
|
133
|
+
prov = metadata.get("provenance") if isinstance(metadata.get("provenance"), dict) else {}
|
|
134
|
+
canonical = str(prov.get("canonical_reference") or prov.get("duplicate_of") or reference).strip()
|
|
135
|
+
return canonical or reference
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _token_signature(text: str) -> frozenset[str]:
|
|
139
|
+
return frozenset(_tokenize(text))
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _auto_promote_duplicate_candidate(
|
|
117
143
|
reference: str,
|
|
118
144
|
*,
|
|
119
145
|
duplicate_candidates: List[Dict[str, Any]],
|
|
120
146
|
contradiction_candidates: List[Dict[str, Any]],
|
|
121
147
|
) -> Dict[str, Any]:
|
|
122
148
|
auto_promote_enabled = os.environ.get("OCMEMOG_GOVERNANCE_AUTOPROMOTE", "true").strip().lower() in {"1", "true", "yes"}
|
|
123
|
-
|
|
149
|
+
allow_with_contradictions = os.environ.get("OCMEMOG_GOVERNANCE_AUTOPROMOTE_ALLOW_CONTRADICTIONS", "false").strip().lower() in {"1", "true", "yes"}
|
|
150
|
+
duplicate_threshold = float(os.environ.get("OCMEMOG_GOVERNANCE_DUPLICATE_AUTOPROMOTE_SIMILARITY", "0.98") or 0.98)
|
|
151
|
+
duplicate_margin = float(os.environ.get("OCMEMOG_GOVERNANCE_DUPLICATE_AUTOPROMOTE_MARGIN", "0.02") or 0.02)
|
|
152
|
+
require_exact_tokens = os.environ.get("OCMEMOG_GOVERNANCE_DUPLICATE_AUTOPROMOTE_REQUIRE_EXACT_TOKENS", "true").strip().lower() in {"1", "true", "yes"}
|
|
124
153
|
promoted: Dict[str, Any] = {"duplicate_of": None, "promoted": False, "reason": "disabled" if not auto_promote_enabled else "none"}
|
|
125
154
|
|
|
126
155
|
if not auto_promote_enabled:
|
|
127
156
|
return promoted
|
|
128
157
|
|
|
129
|
-
if contradiction_candidates:
|
|
158
|
+
if contradiction_candidates and not allow_with_contradictions:
|
|
130
159
|
promoted["reason"] = "blocked_by_contradiction_candidates"
|
|
131
160
|
return promoted
|
|
132
161
|
|
|
@@ -134,13 +163,29 @@ def _auto_promote_governance_candidates(
|
|
|
134
163
|
promoted["reason"] = "no_duplicate_candidates"
|
|
135
164
|
return promoted
|
|
136
165
|
|
|
137
|
-
|
|
166
|
+
payload = provenance.fetch_reference(reference) or {}
|
|
167
|
+
reference_content = str(payload.get("content") or "")
|
|
168
|
+
reference_signature = _token_signature(reference_content)
|
|
169
|
+
ranked = sorted(duplicate_candidates, key=lambda item: float(item.get("similarity") or 0.0), reverse=True)
|
|
170
|
+
top = ranked[0]
|
|
138
171
|
similarity = float(top.get("similarity") or 0.0)
|
|
139
|
-
target = str(top.get("reference") or "")
|
|
140
|
-
if not target or similarity < duplicate_threshold:
|
|
172
|
+
target = _canonicalize_duplicate_target(str(top.get("reference") or ""))
|
|
173
|
+
if not target or target == reference or similarity < duplicate_threshold:
|
|
141
174
|
promoted["reason"] = "similarity_below_threshold"
|
|
142
175
|
return promoted
|
|
143
176
|
|
|
177
|
+
if len(ranked) > 1:
|
|
178
|
+
runner_up = float(ranked[1].get("similarity") or 0.0)
|
|
179
|
+
if similarity - runner_up < duplicate_margin:
|
|
180
|
+
promoted["reason"] = "ambiguous_duplicate_candidates"
|
|
181
|
+
return promoted
|
|
182
|
+
|
|
183
|
+
target_payload = provenance.fetch_reference(target) or {}
|
|
184
|
+
target_content = str(target_payload.get("content") or "")
|
|
185
|
+
if require_exact_tokens and _token_signature(target_content) != reference_signature:
|
|
186
|
+
promoted["reason"] = "token_signature_mismatch"
|
|
187
|
+
return promoted
|
|
188
|
+
|
|
144
189
|
merged = mark_memory_relationship(reference, relationship="duplicate_of", target_reference=target, status="duplicate")
|
|
145
190
|
promoted.update({
|
|
146
191
|
"duplicate_of": target,
|
|
@@ -151,17 +196,70 @@ def _auto_promote_governance_candidates(
|
|
|
151
196
|
return promoted
|
|
152
197
|
|
|
153
198
|
|
|
199
|
+
def _auto_apply_supersession_recommendation(
|
|
200
|
+
reference: str,
|
|
201
|
+
*,
|
|
202
|
+
contradiction_candidates: List[Dict[str, Any]],
|
|
203
|
+
supersession_recommendation: Dict[str, Any],
|
|
204
|
+
) -> Dict[str, Any]:
|
|
205
|
+
recommendation = dict(supersession_recommendation or {})
|
|
206
|
+
if not recommendation:
|
|
207
|
+
return {"recommended": False, "auto_applied": False, "reason": "missing_recommendation", "target_reference": None, "signal": 0.0}
|
|
208
|
+
|
|
209
|
+
auto_apply = os.environ.get("OCMEMOG_GOVERNANCE_AUTOPROMOTE_SUPERSESSION", "false").strip().lower() in {"1", "true", "yes"}
|
|
210
|
+
allow_with_contradictions = os.environ.get("OCMEMOG_GOVERNANCE_AUTOPROMOTE_ALLOW_CONTRADICTIONS", "false").strip().lower() in {"1", "true", "yes"}
|
|
211
|
+
auto_apply_signal = float(os.environ.get("OCMEMOG_GOVERNANCE_SUPERSESSION_AUTOPROMOTE_SIGNAL", "0.97") or 0.97)
|
|
212
|
+
model_conf_threshold = float(os.environ.get("OCMEMOG_GOVERNANCE_SUPERSESSION_AUTOPROMOTE_MODEL_CONFIDENCE", "0.97") or 0.97)
|
|
213
|
+
|
|
214
|
+
recommendation.setdefault("auto_applied", False)
|
|
215
|
+
if not recommendation.get("recommended"):
|
|
216
|
+
recommendation["reason"] = recommendation.get("reason") or "not_recommended"
|
|
217
|
+
return recommendation
|
|
218
|
+
|
|
219
|
+
if not auto_apply:
|
|
220
|
+
return recommendation
|
|
221
|
+
|
|
222
|
+
if contradiction_candidates and not allow_with_contradictions:
|
|
223
|
+
recommendation["reason"] = "blocked_by_contradiction_candidates"
|
|
224
|
+
return recommendation
|
|
225
|
+
|
|
226
|
+
signal = float(recommendation.get("signal") or 0.0)
|
|
227
|
+
if signal < auto_apply_signal:
|
|
228
|
+
recommendation["reason"] = "signal_below_autopromote_threshold"
|
|
229
|
+
return recommendation
|
|
230
|
+
|
|
231
|
+
model_hint = recommendation.get("model_hint") if isinstance(recommendation.get("model_hint"), dict) else {}
|
|
232
|
+
if not model_hint or not model_hint.get("contradiction") or float(model_hint.get("confidence") or 0.0) < model_conf_threshold:
|
|
233
|
+
recommendation["reason"] = "model_hint_below_autopromote_threshold"
|
|
234
|
+
return recommendation
|
|
235
|
+
|
|
236
|
+
target = str(recommendation.get("target_reference") or "").strip()
|
|
237
|
+
if not target or target == reference:
|
|
238
|
+
recommendation["reason"] = "missing_target"
|
|
239
|
+
return recommendation
|
|
240
|
+
|
|
241
|
+
merged = mark_memory_relationship(reference, relationship="supersedes", target_reference=target, status="active")
|
|
242
|
+
recommendation["auto_applied"] = merged is not None
|
|
243
|
+
recommendation["reason"] = "auto_applied_supersession" if merged is not None else "auto_apply_failed"
|
|
244
|
+
return recommendation
|
|
245
|
+
|
|
246
|
+
|
|
154
247
|
def _auto_attach_governance_candidates(reference: str) -> Dict[str, Any]:
|
|
155
248
|
duplicate_candidates = find_duplicate_candidates(reference, limit=5, min_similarity=0.72)
|
|
156
249
|
contradiction_candidates = find_contradiction_candidates(reference, limit=5, min_signal=0.55, use_model=True)
|
|
157
|
-
|
|
250
|
+
supersession_recommendation = _recommend_supersession_from_contradictions(
|
|
251
|
+
reference,
|
|
252
|
+
contradiction_candidates=contradiction_candidates,
|
|
253
|
+
)
|
|
254
|
+
auto_promotion = _auto_promote_duplicate_candidate(
|
|
158
255
|
reference,
|
|
159
256
|
duplicate_candidates=duplicate_candidates,
|
|
160
257
|
contradiction_candidates=contradiction_candidates,
|
|
161
258
|
)
|
|
162
|
-
supersession_recommendation =
|
|
259
|
+
supersession_recommendation = _auto_apply_supersession_recommendation(
|
|
163
260
|
reference,
|
|
164
261
|
contradiction_candidates=contradiction_candidates,
|
|
262
|
+
supersession_recommendation=supersession_recommendation,
|
|
165
263
|
)
|
|
166
264
|
payload = {
|
|
167
265
|
"duplicate_candidates": [item.get("reference") for item in duplicate_candidates if item.get("reference")],
|
|
@@ -196,7 +294,7 @@ def store_memory(
|
|
|
196
294
|
) -> int:
|
|
197
295
|
content = _sanitize(content)
|
|
198
296
|
table = memory_type.strip().lower() if memory_type else "knowledge"
|
|
199
|
-
allowed =
|
|
297
|
+
allowed = set(store.MEMORY_TABLES)
|
|
200
298
|
if table not in allowed:
|
|
201
299
|
table = "knowledge"
|
|
202
300
|
normalized_metadata = provenance.normalize_metadata(metadata, source=source)
|
|
@@ -344,7 +442,7 @@ def find_duplicate_candidates(
|
|
|
344
442
|
payload = provenance.fetch_reference(reference) or {}
|
|
345
443
|
table = str(payload.get("table") or payload.get("type") or "")
|
|
346
444
|
content = str(payload.get("content") or "")
|
|
347
|
-
if table not in
|
|
445
|
+
if table not in set(store.MEMORY_TABLES):
|
|
348
446
|
return []
|
|
349
447
|
row_id = payload.get("id")
|
|
350
448
|
conn = store.connect()
|
|
@@ -395,7 +493,7 @@ def find_contradiction_candidates(
|
|
|
395
493
|
payload = provenance.fetch_reference(reference) or {}
|
|
396
494
|
table = str(payload.get("table") or payload.get("type") or "")
|
|
397
495
|
content = str(payload.get("content") or "")
|
|
398
|
-
if table not in
|
|
496
|
+
if table not in set(store.MEMORY_TABLES):
|
|
399
497
|
return []
|
|
400
498
|
row_id = payload.get("id")
|
|
401
499
|
conn = store.connect()
|
|
@@ -494,7 +592,7 @@ def list_governance_candidates(
|
|
|
494
592
|
categories: Optional[List[str]] = None,
|
|
495
593
|
limit: int = 50,
|
|
496
594
|
) -> List[Dict[str, Any]]:
|
|
497
|
-
allowed =
|
|
595
|
+
allowed = set(store.MEMORY_TABLES)
|
|
498
596
|
tables = [table for table in (categories or list(allowed)) if table in allowed]
|
|
499
597
|
conn = store.connect()
|
|
500
598
|
try:
|
|
@@ -532,6 +630,95 @@ def _remove_from_list(values: Any, target: str) -> List[str]:
|
|
|
532
630
|
return [str(item) for item in (values or []) if str(item) and str(item) != target]
|
|
533
631
|
|
|
534
632
|
|
|
633
|
+
def _review_item_context(reference: str, *, depth: int = 1) -> Dict[str, Any]:
|
|
634
|
+
payload = provenance.hydrate_reference(reference, depth=depth) or {"reference": reference}
|
|
635
|
+
metadata = payload.get("metadata") if isinstance(payload.get("metadata"), dict) else {}
|
|
636
|
+
prov = metadata.get("provenance") if isinstance(metadata.get("provenance"), dict) else {}
|
|
637
|
+
return {
|
|
638
|
+
"reference": reference,
|
|
639
|
+
"bucket": payload.get("table"),
|
|
640
|
+
"id": payload.get("id"),
|
|
641
|
+
"timestamp": payload.get("timestamp"),
|
|
642
|
+
"content": payload.get("content"),
|
|
643
|
+
"memory_status": prov.get("memory_status") or metadata.get("memory_status") or "active",
|
|
644
|
+
"provenance_preview": payload.get("provenance_preview") or provenance.preview_from_metadata(metadata),
|
|
645
|
+
"metadata": metadata,
|
|
646
|
+
"links": payload.get("links") or [],
|
|
647
|
+
"backlinks": payload.get("backlinks") or [],
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
|
|
651
|
+
def _review_item_summary(kind: str, reference: str, target_reference: str) -> str:
|
|
652
|
+
if kind == "duplicate_candidate":
|
|
653
|
+
return f"{reference} may duplicate {target_reference}"
|
|
654
|
+
if kind == "contradiction_candidate":
|
|
655
|
+
return f"{reference} may contradict {target_reference}"
|
|
656
|
+
if kind == "supersession_recommendation":
|
|
657
|
+
return f"{reference} may supersede {target_reference}"
|
|
658
|
+
return f"{reference} requires review against {target_reference}"
|
|
659
|
+
|
|
660
|
+
|
|
661
|
+
def _review_actions(kind: str, relationship: str) -> List[Dict[str, Any]]:
|
|
662
|
+
meta = _REVIEW_KIND_METADATA.get(kind, {})
|
|
663
|
+
return [
|
|
664
|
+
{
|
|
665
|
+
"decision": "approve",
|
|
666
|
+
"approved": True,
|
|
667
|
+
"relationship": relationship,
|
|
668
|
+
"label": meta.get("approve_label") or "Approve",
|
|
669
|
+
},
|
|
670
|
+
{
|
|
671
|
+
"decision": "reject",
|
|
672
|
+
"approved": False,
|
|
673
|
+
"relationship": relationship,
|
|
674
|
+
"label": meta.get("reject_label") or "Reject",
|
|
675
|
+
},
|
|
676
|
+
]
|
|
677
|
+
|
|
678
|
+
|
|
679
|
+
def _relationship_for_review(kind: str | None = None, relationship: str | None = None) -> str:
|
|
680
|
+
resolved = (relationship or "").strip().lower()
|
|
681
|
+
if resolved:
|
|
682
|
+
return resolved
|
|
683
|
+
kind_key = (kind or "").strip().lower()
|
|
684
|
+
return _REVIEW_KIND_METADATA.get(kind_key, {}).get("relationship", "")
|
|
685
|
+
|
|
686
|
+
|
|
687
|
+
def list_governance_review_items(
|
|
688
|
+
*,
|
|
689
|
+
categories: Optional[List[str]] = None,
|
|
690
|
+
limit: int = 100,
|
|
691
|
+
context_depth: int = 1,
|
|
692
|
+
) -> List[Dict[str, Any]]:
|
|
693
|
+
items = governance_queue(categories=categories, limit=limit)
|
|
694
|
+
review_items: List[Dict[str, Any]] = []
|
|
695
|
+
for item in items:
|
|
696
|
+
kind = str(item.get("kind") or "")
|
|
697
|
+
relationship = _relationship_for_review(kind=kind)
|
|
698
|
+
reference = str(item.get("reference") or "")
|
|
699
|
+
target_reference = str(item.get("target_reference") or "")
|
|
700
|
+
if not reference or not target_reference or not relationship:
|
|
701
|
+
continue
|
|
702
|
+
review_items.append({
|
|
703
|
+
"review_id": f"{kind}:{reference}->{target_reference}",
|
|
704
|
+
"kind": kind,
|
|
705
|
+
"kind_label": _REVIEW_KIND_METADATA.get(kind, {}).get("label") or kind.replace("_", " "),
|
|
706
|
+
"relationship": relationship,
|
|
707
|
+
"priority": int(item.get("priority") or 0),
|
|
708
|
+
"timestamp": item.get("timestamp"),
|
|
709
|
+
"bucket": item.get("bucket"),
|
|
710
|
+
"signal": float(item.get("signal") or 0.0),
|
|
711
|
+
"reason": item.get("reason"),
|
|
712
|
+
"reference": reference,
|
|
713
|
+
"target_reference": target_reference,
|
|
714
|
+
"summary": _review_item_summary(kind, reference, target_reference),
|
|
715
|
+
"actions": _review_actions(kind, relationship),
|
|
716
|
+
"source": _review_item_context(reference, depth=context_depth),
|
|
717
|
+
"target": _review_item_context(target_reference, depth=context_depth),
|
|
718
|
+
})
|
|
719
|
+
return review_items
|
|
720
|
+
|
|
721
|
+
|
|
535
722
|
def apply_governance_decision(
|
|
536
723
|
reference: str,
|
|
537
724
|
*,
|
|
@@ -541,7 +728,26 @@ def apply_governance_decision(
|
|
|
541
728
|
) -> Dict[str, Any] | None:
|
|
542
729
|
relationship = (relationship or "").strip().lower()
|
|
543
730
|
if approved:
|
|
544
|
-
|
|
731
|
+
merged = mark_memory_relationship(reference, relationship=relationship, target_reference=target_reference)
|
|
732
|
+
if merged is None:
|
|
733
|
+
return None
|
|
734
|
+
updates: Dict[str, Any] = {}
|
|
735
|
+
if relationship == "duplicate_of":
|
|
736
|
+
current = provenance.fetch_reference(reference) or {}
|
|
737
|
+
metadata = current.get("metadata") or {}
|
|
738
|
+
prov = metadata.get("provenance") if isinstance(metadata.get("provenance"), dict) else {}
|
|
739
|
+
updates["duplicate_candidates"] = _remove_from_list(prov.get("duplicate_candidates"), target_reference)
|
|
740
|
+
elif relationship == "contradicts":
|
|
741
|
+
current = provenance.fetch_reference(reference) or {}
|
|
742
|
+
metadata = current.get("metadata") or {}
|
|
743
|
+
prov = metadata.get("provenance") if isinstance(metadata.get("provenance"), dict) else {}
|
|
744
|
+
updates["contradiction_candidates"] = _remove_from_list(prov.get("contradiction_candidates"), target_reference)
|
|
745
|
+
elif relationship == "supersedes":
|
|
746
|
+
updates["supersession_recommendation"] = None
|
|
747
|
+
if updates:
|
|
748
|
+
merged = provenance.force_update_memory_metadata(reference, updates) or merged
|
|
749
|
+
_emit(f"apply_governance_decision_{relationship}_approved")
|
|
750
|
+
return merged
|
|
545
751
|
|
|
546
752
|
current = provenance.fetch_reference(reference) or {}
|
|
547
753
|
metadata = current.get("metadata") or {}
|
|
@@ -552,14 +758,55 @@ def apply_governance_decision(
|
|
|
552
758
|
elif relationship == "contradicts":
|
|
553
759
|
updates["contradiction_candidates"] = _remove_from_list(prov.get("contradiction_candidates"), target_reference)
|
|
554
760
|
elif relationship == "supersedes":
|
|
761
|
+
recommendation = prov.get("supersession_recommendation") if isinstance(prov.get("supersession_recommendation"), dict) else {}
|
|
762
|
+
if not recommendation or str(recommendation.get("target_reference") or "") == target_reference:
|
|
763
|
+
updates["supersession_recommendation"] = None
|
|
555
764
|
updates["supersedes"] = None
|
|
556
765
|
else:
|
|
557
766
|
return None
|
|
558
|
-
merged = provenance.
|
|
767
|
+
merged = provenance.force_update_memory_metadata(reference, updates)
|
|
559
768
|
_emit(f"apply_governance_decision_{relationship}_{'approved' if approved else 'rejected'}")
|
|
560
769
|
return merged
|
|
561
770
|
|
|
562
771
|
|
|
772
|
+
def apply_governance_review_decision(
|
|
773
|
+
reference: str,
|
|
774
|
+
*,
|
|
775
|
+
target_reference: str,
|
|
776
|
+
approved: bool = True,
|
|
777
|
+
kind: str | None = None,
|
|
778
|
+
relationship: str | None = None,
|
|
779
|
+
context_depth: int = 1,
|
|
780
|
+
) -> Dict[str, Any] | None:
|
|
781
|
+
resolved_relationship = _relationship_for_review(kind=kind, relationship=relationship)
|
|
782
|
+
if not resolved_relationship:
|
|
783
|
+
return None
|
|
784
|
+
result = apply_governance_decision(
|
|
785
|
+
reference,
|
|
786
|
+
relationship=resolved_relationship,
|
|
787
|
+
target_reference=target_reference,
|
|
788
|
+
approved=approved,
|
|
789
|
+
)
|
|
790
|
+
if result is None:
|
|
791
|
+
return None
|
|
792
|
+
resolved_kind = (kind or "").strip().lower()
|
|
793
|
+
if not resolved_kind:
|
|
794
|
+
for candidate_kind, meta in _REVIEW_KIND_METADATA.items():
|
|
795
|
+
if meta.get("relationship") == resolved_relationship:
|
|
796
|
+
resolved_kind = candidate_kind
|
|
797
|
+
break
|
|
798
|
+
return {
|
|
799
|
+
"reference": reference,
|
|
800
|
+
"target_reference": target_reference,
|
|
801
|
+
"approved": bool(approved),
|
|
802
|
+
"kind": resolved_kind or None,
|
|
803
|
+
"relationship": resolved_relationship,
|
|
804
|
+
"result": result,
|
|
805
|
+
"source": _review_item_context(reference, depth=context_depth),
|
|
806
|
+
"target": _review_item_context(target_reference, depth=context_depth),
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
|
|
563
810
|
def rollback_governance_decision(
|
|
564
811
|
reference: str,
|
|
565
812
|
*,
|
|
@@ -617,7 +864,7 @@ def rollback_governance_decision(
|
|
|
617
864
|
|
|
618
865
|
|
|
619
866
|
def governance_queue(*, categories: Optional[List[str]] = None, limit: int = 100) -> List[Dict[str, Any]]:
|
|
620
|
-
allowed =
|
|
867
|
+
allowed = set(store.MEMORY_TABLES)
|
|
621
868
|
tables = [table for table in (categories or list(allowed)) if table in allowed]
|
|
622
869
|
conn = store.connect()
|
|
623
870
|
try:
|
|
@@ -883,7 +1130,7 @@ def governance_audit(*, limit: int = 100, kinds: Optional[List[str]] = None) ->
|
|
|
883
1130
|
|
|
884
1131
|
|
|
885
1132
|
def governance_summary(*, categories: Optional[List[str]] = None) -> Dict[str, Any]:
|
|
886
|
-
allowed =
|
|
1133
|
+
allowed = set(store.MEMORY_TABLES)
|
|
887
1134
|
tables = [table for table in (categories or list(allowed)) if table in allowed]
|
|
888
1135
|
conn = store.connect()
|
|
889
1136
|
try:
|
|
@@ -2,6 +2,8 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import uuid
|
|
4
4
|
import json
|
|
5
|
+
import re
|
|
6
|
+
from difflib import SequenceMatcher
|
|
5
7
|
from typing import Dict, Any
|
|
6
8
|
|
|
7
9
|
from brain.runtime.instrumentation import emit_event
|
|
@@ -11,6 +13,96 @@ from brain.runtime.security import redaction
|
|
|
11
13
|
|
|
12
14
|
|
|
13
15
|
LOGFILE = state_store.reports_dir() / "brain_memory.log.jsonl"
|
|
16
|
+
_NEAR_DUPLICATE_SIMILARITY = 0.85
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _normalize_summary(text: str) -> str:
|
|
20
|
+
return re.sub(r"\s+", " ", str(text or "").strip().lower())
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _tokenize(text: str) -> set[str]:
|
|
24
|
+
return {token for token in re.findall(r"[a-z0-9]+", _normalize_summary(text))}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _summary_similarity(left: str, right: str) -> float:
|
|
28
|
+
left_tokens = _tokenize(left)
|
|
29
|
+
right_tokens = _tokenize(right)
|
|
30
|
+
token_similarity = 0.0
|
|
31
|
+
if left_tokens and right_tokens:
|
|
32
|
+
overlap = len(left_tokens & right_tokens)
|
|
33
|
+
union = len(left_tokens | right_tokens)
|
|
34
|
+
token_similarity = overlap / max(1, union)
|
|
35
|
+
sequence_similarity = SequenceMatcher(None, _normalize_summary(left), _normalize_summary(right)).ratio()
|
|
36
|
+
return max(token_similarity, sequence_similarity)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _ranges_overlap(left: Dict[str, Any], right: Dict[str, Any]) -> bool:
|
|
40
|
+
if str(left.get("path") or "") != str(right.get("path") or ""):
|
|
41
|
+
return False
|
|
42
|
+
|
|
43
|
+
def _as_int(value: Any) -> int | None:
|
|
44
|
+
try:
|
|
45
|
+
return int(value) if value is not None else None
|
|
46
|
+
except Exception:
|
|
47
|
+
return None
|
|
48
|
+
|
|
49
|
+
left_start = _as_int(left.get("start_line"))
|
|
50
|
+
left_end = _as_int(left.get("end_line")) or left_start
|
|
51
|
+
right_start = _as_int(right.get("start_line"))
|
|
52
|
+
right_end = _as_int(right.get("end_line")) or right_start
|
|
53
|
+
|
|
54
|
+
if left_start is None and right_start is None:
|
|
55
|
+
return True
|
|
56
|
+
if left_start is None or right_start is None:
|
|
57
|
+
return False
|
|
58
|
+
return max(left_start, right_start) <= min(left_end or left_start, right_end or right_start)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _shares_provenance_anchor(left: Dict[str, Any], right: Dict[str, Any]) -> bool:
|
|
62
|
+
left_meta = provenance.normalize_metadata(left)
|
|
63
|
+
right_meta = provenance.normalize_metadata(right)
|
|
64
|
+
left_prov = left_meta.get("provenance") if isinstance(left_meta.get("provenance"), dict) else {}
|
|
65
|
+
right_prov = right_meta.get("provenance") if isinstance(right_meta.get("provenance"), dict) else {}
|
|
66
|
+
|
|
67
|
+
left_conv = left_prov.get("conversation") if isinstance(left_prov.get("conversation"), dict) else {}
|
|
68
|
+
right_conv = right_prov.get("conversation") if isinstance(right_prov.get("conversation"), dict) else {}
|
|
69
|
+
if left_conv.get("message_id") and left_conv.get("message_id") == right_conv.get("message_id"):
|
|
70
|
+
return True
|
|
71
|
+
|
|
72
|
+
left_transcript = left_prov.get("transcript_anchor") if isinstance(left_prov.get("transcript_anchor"), dict) else {}
|
|
73
|
+
right_transcript = right_prov.get("transcript_anchor") if isinstance(right_prov.get("transcript_anchor"), dict) else {}
|
|
74
|
+
if left_transcript.get("path") and right_transcript.get("path") and _ranges_overlap(left_transcript, right_transcript):
|
|
75
|
+
return True
|
|
76
|
+
|
|
77
|
+
left_refs = {str(item) for item in left_prov.get("source_references") or [] if str(item).strip()}
|
|
78
|
+
right_refs = {str(item) for item in right_prov.get("source_references") or [] if str(item).strip()}
|
|
79
|
+
return bool(left_refs & right_refs)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _find_near_duplicate_candidate(conn, source_event_id: int, summary: str, metadata: Dict[str, Any]) -> str | None:
|
|
83
|
+
rows = conn.execute(
|
|
84
|
+
"""
|
|
85
|
+
SELECT candidate_id, distilled_summary, metadata_json
|
|
86
|
+
FROM candidates
|
|
87
|
+
WHERE source_event_id != ?
|
|
88
|
+
ORDER BY created_at DESC, candidate_id DESC
|
|
89
|
+
LIMIT 250
|
|
90
|
+
""",
|
|
91
|
+
(source_event_id,),
|
|
92
|
+
).fetchall()
|
|
93
|
+
normalized_summary = _normalize_summary(summary)
|
|
94
|
+
for row in rows:
|
|
95
|
+
existing_summary = str(row["distilled_summary"] if isinstance(row, dict) else row[1] or "")
|
|
96
|
+
similarity = _summary_similarity(normalized_summary, existing_summary)
|
|
97
|
+
if similarity < _NEAR_DUPLICATE_SIMILARITY:
|
|
98
|
+
continue
|
|
99
|
+
try:
|
|
100
|
+
existing_metadata = json.loads(row["metadata_json"] if isinstance(row, dict) else row[2] or "{}")
|
|
101
|
+
except Exception:
|
|
102
|
+
existing_metadata = {}
|
|
103
|
+
if _shares_provenance_anchor(metadata, existing_metadata):
|
|
104
|
+
return str(row["candidate_id"] if isinstance(row, dict) else row[0])
|
|
105
|
+
return None
|
|
14
106
|
|
|
15
107
|
|
|
16
108
|
def create_candidate(
|
|
@@ -29,14 +121,20 @@ def create_candidate(
|
|
|
29
121
|
normalized_metadata = provenance.normalize_metadata(metadata, source="candidate")
|
|
30
122
|
|
|
31
123
|
conn = store.connect()
|
|
32
|
-
|
|
124
|
+
exact_row = conn.execute(
|
|
33
125
|
"SELECT candidate_id FROM candidates WHERE source_event_id=? AND distilled_summary=?",
|
|
34
126
|
(source_event_id, summary),
|
|
35
127
|
).fetchone()
|
|
36
|
-
if
|
|
128
|
+
if exact_row:
|
|
37
129
|
conn.close()
|
|
38
130
|
emit_event(LOGFILE, "brain_memory_candidate_duplicate", status="ok", source_event_id=source_event_id)
|
|
39
|
-
return {"candidate_id":
|
|
131
|
+
return {"candidate_id": exact_row[0], "duplicate": True}
|
|
132
|
+
|
|
133
|
+
near_duplicate_id = _find_near_duplicate_candidate(conn, source_event_id, summary, normalized_metadata)
|
|
134
|
+
if near_duplicate_id:
|
|
135
|
+
conn.close()
|
|
136
|
+
emit_event(LOGFILE, "brain_memory_candidate_duplicate", status="ok", source_event_id=source_event_id, duplicate_kind="near")
|
|
137
|
+
return {"candidate_id": near_duplicate_id, "duplicate": True}
|
|
40
138
|
|
|
41
139
|
candidate_id = str(uuid.uuid4())
|
|
42
140
|
verification_status = "verified" if verification_lines else "unverified"
|
|
@@ -9,7 +9,7 @@ from brain.runtime import state_store
|
|
|
9
9
|
from brain.runtime.instrumentation import emit_event
|
|
10
10
|
from brain.runtime.memory import memory_links, memory_salience, provenance, store, unresolved_state
|
|
11
11
|
|
|
12
|
-
_ALLOWED_MEMORY_TABLES = {
|
|
12
|
+
_ALLOWED_MEMORY_TABLES = {*store.MEMORY_TABLES, "candidates", "promotions"}
|
|
13
13
|
LOGFILE = state_store.reports_dir() / "brain_memory.log.jsonl"
|
|
14
14
|
_COMMITMENT_RE = re.compile(
|
|
15
15
|
r"\b(i(?:'m| am)? going to|i will|i'll|let me|i can(?:\s+now)?|next,? i(?:'ll| will)|i should be able to)\b",
|
|
@@ -97,7 +97,12 @@ def _reject_distilled_summary(summary: str, source: str) -> bool:
|
|
|
97
97
|
if lowered.startswith(("good job", "be proactive", "be thorough", "always check", "always remember")):
|
|
98
98
|
return True
|
|
99
99
|
if source and lowered == _normalize(source):
|
|
100
|
-
|
|
100
|
+
# In no-model environments the best available summary can be the
|
|
101
|
+
# original one-line experience. Keep rejecting verbose/source-equal
|
|
102
|
+
# fallbacks, but allow concise operational statements through.
|
|
103
|
+
compact_source = re.sub(r"\s+", " ", str(source or "")).strip()
|
|
104
|
+
if "\n" in compact_source or len(compact_source) > 120:
|
|
105
|
+
return True
|
|
101
106
|
return False
|
|
102
107
|
|
|
103
108
|
|
|
@@ -5,13 +5,13 @@ from typing import Dict, Any
|
|
|
5
5
|
from brain.runtime.memory import store, integrity
|
|
6
6
|
|
|
7
7
|
|
|
8
|
-
EMBED_TABLES = (
|
|
8
|
+
EMBED_TABLES = tuple(store.MEMORY_TABLES)
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
def get_memory_health() -> Dict[str, Any]:
|
|
12
12
|
conn = store.connect()
|
|
13
13
|
counts: Dict[str, int] = {}
|
|
14
|
-
for table in ["experiences", "candidates", "promotions", "memory_index",
|
|
14
|
+
for table in ["experiences", "candidates", "promotions", "memory_index", *store.MEMORY_TABLES, "vector_embeddings"]:
|
|
15
15
|
try:
|
|
16
16
|
counts[table] = conn.execute(f"SELECT COUNT(*) FROM {table}").fetchone()[0]
|
|
17
17
|
except Exception:
|
|
@@ -20,7 +20,7 @@ def get_memory_health() -> Dict[str, Any]:
|
|
|
20
20
|
vector_count = 0
|
|
21
21
|
try:
|
|
22
22
|
vector_count = conn.execute(
|
|
23
|
-
"SELECT COUNT(*) FROM vector_embeddings WHERE source_type IN ('knowledge','
|
|
23
|
+
"SELECT COUNT(*) FROM vector_embeddings WHERE source_type IN ('knowledge','preferences','identity','reflections','directives','tasks','runbooks','lessons')"
|
|
24
24
|
).fetchone()[0]
|
|
25
25
|
except Exception:
|
|
26
26
|
vector_count = 0
|
|
@@ -7,7 +7,7 @@ from brain.runtime import state_store
|
|
|
7
7
|
from brain.runtime.memory import store
|
|
8
8
|
|
|
9
9
|
|
|
10
|
-
EMBED_TABLES = (
|
|
10
|
+
EMBED_TABLES = tuple(store.MEMORY_TABLES)
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
def run_integrity_check() -> Dict[str, Any]:
|
|
@@ -21,6 +21,8 @@ def run_integrity_check() -> Dict[str, Any]:
|
|
|
21
21
|
required = {
|
|
22
22
|
"experiences",
|
|
23
23
|
"knowledge",
|
|
24
|
+
"preferences",
|
|
25
|
+
"identity",
|
|
24
26
|
"reflections",
|
|
25
27
|
"tasks",
|
|
26
28
|
"directives",
|
|
@@ -12,7 +12,7 @@ from brain.runtime.instrumentation import emit_event
|
|
|
12
12
|
from brain.runtime.memory import api, integrity, memory_consolidation, memory_links, provenance, store, unresolved_state, vector_index
|
|
13
13
|
|
|
14
14
|
LOGFILE = state_store.reports_dir() / "brain_memory.log.jsonl"
|
|
15
|
-
_WRITABLE_MEMORY_TABLES =
|
|
15
|
+
_WRITABLE_MEMORY_TABLES = set(store.MEMORY_TABLES)
|
|
16
16
|
_SUMMARY_PREFIX_RE = re.compile(r"^(?:insight|recommendation|lesson)\s*:\s*", re.IGNORECASE)
|
|
17
17
|
|
|
18
18
|
|
|
@@ -11,6 +11,38 @@ from brain.runtime import config
|
|
|
11
11
|
|
|
12
12
|
LOGFILE = state_store.reports_dir() / "brain_memory.log.jsonl"
|
|
13
13
|
|
|
14
|
+
_PREFERENCE_PATTERNS = (
|
|
15
|
+
"prefer",
|
|
16
|
+
"preference",
|
|
17
|
+
"favorite",
|
|
18
|
+
"favourite",
|
|
19
|
+
"likes",
|
|
20
|
+
"like ",
|
|
21
|
+
"loves",
|
|
22
|
+
"enjoys",
|
|
23
|
+
"dislikes",
|
|
24
|
+
"hate ",
|
|
25
|
+
"avoids",
|
|
26
|
+
)
|
|
27
|
+
_IDENTITY_PATTERNS = (
|
|
28
|
+
"my name is",
|
|
29
|
+
"i am ",
|
|
30
|
+
"i'm ",
|
|
31
|
+
"my pronouns are",
|
|
32
|
+
"i live in",
|
|
33
|
+
"we live in",
|
|
34
|
+
"i work at",
|
|
35
|
+
"we work at",
|
|
36
|
+
"i study at",
|
|
37
|
+
"we study at",
|
|
38
|
+
"my timezone is",
|
|
39
|
+
"my time zone is",
|
|
40
|
+
"my email is",
|
|
41
|
+
"my phone number is",
|
|
42
|
+
"my birthday is",
|
|
43
|
+
"allergic to",
|
|
44
|
+
)
|
|
45
|
+
|
|
14
46
|
|
|
15
47
|
def _should_promote(confidence: float, threshold: float | None = None) -> bool:
|
|
16
48
|
threshold = config.OCMEMOG_PROMOTION_THRESHOLD if threshold is None else threshold
|
|
@@ -23,6 +55,10 @@ def _destination_table(summary: str) -> str:
|
|
|
23
55
|
return "runbooks"
|
|
24
56
|
if "lesson" in lowered or "postmortem" in lowered or "learned" in lowered:
|
|
25
57
|
return "lessons"
|
|
58
|
+
if any(pattern in lowered for pattern in _PREFERENCE_PATTERNS):
|
|
59
|
+
return "preferences"
|
|
60
|
+
if any(pattern in lowered for pattern in _IDENTITY_PATTERNS):
|
|
61
|
+
return "identity"
|
|
26
62
|
return "knowledge"
|
|
27
63
|
|
|
28
64
|
|
|
@@ -139,7 +175,7 @@ def promote_candidate(candidate: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
139
175
|
)
|
|
140
176
|
emit_event(LOGFILE, "brain_memory_reinforcement_created", status="ok")
|
|
141
177
|
if memory_id:
|
|
142
|
-
vector_index.insert_memory(memory_id, candidate.get("distilled_summary", ""), confidence)
|
|
178
|
+
vector_index.insert_memory(memory_id, candidate.get("distilled_summary", ""), confidence, source_type=destination)
|
|
143
179
|
try:
|
|
144
180
|
from brain.runtime.memory import api as memory_api
|
|
145
181
|
|
|
@@ -176,7 +212,7 @@ def demote_memory(reference: str, reason: str = "low_confidence", new_confidence
|
|
|
176
212
|
table, sep, raw_id = reference.partition(":")
|
|
177
213
|
if not sep or not raw_id.isdigit():
|
|
178
214
|
return {"ok": False, "error": "invalid_reference"}
|
|
179
|
-
allowed =
|
|
215
|
+
allowed = set(store.MEMORY_TABLES)
|
|
180
216
|
if table not in allowed:
|
|
181
217
|
return {"ok": False, "error": "unsupported_table"}
|
|
182
218
|
conn = store.connect()
|
|
@@ -212,7 +248,7 @@ def demote_memory(reference: str, reason: str = "low_confidence", new_confidence
|
|
|
212
248
|
|
|
213
249
|
def demote_by_confidence(limit: int = 20, threshold: float | None = None, force: bool = False) -> Dict[str, Any]:
|
|
214
250
|
threshold = config.OCMEMOG_DEMOTION_THRESHOLD if threshold is None else threshold
|
|
215
|
-
tables = (
|
|
251
|
+
tables = tuple(store.MEMORY_TABLES)
|
|
216
252
|
conn = store.connect()
|
|
217
253
|
rows = []
|
|
218
254
|
for table in tables:
|
|
@@ -6,7 +6,7 @@ from typing import Any, Dict, Iterable, List, Optional, Set
|
|
|
6
6
|
|
|
7
7
|
from brain.runtime.memory import memory_links, store
|
|
8
8
|
|
|
9
|
-
_MEMORY_TABLES =
|
|
9
|
+
_MEMORY_TABLES = set(store.MEMORY_TABLES)
|
|
10
10
|
_FETCHABLE_TABLES = _MEMORY_TABLES | {"promotions", "experiences", "conversation_turns", "conversation_checkpoints"}
|
|
11
11
|
_SYNTHETIC_PREFIXES = {"conversation", "session", "thread", "message", "label", "transcript"}
|
|
12
12
|
|
|
@@ -55,14 +55,7 @@ def _recency_score(timestamp: str | None) -> float:
|
|
|
55
55
|
return 0.0
|
|
56
56
|
|
|
57
57
|
|
|
58
|
-
MEMORY_BUCKETS: Tuple[str, ...] = (
|
|
59
|
-
"knowledge",
|
|
60
|
-
"reflections",
|
|
61
|
-
"directives",
|
|
62
|
-
"tasks",
|
|
63
|
-
"runbooks",
|
|
64
|
-
"lessons",
|
|
65
|
-
)
|
|
58
|
+
MEMORY_BUCKETS: Tuple[str, ...] = tuple(store.MEMORY_TABLES)
|
|
66
59
|
|
|
67
60
|
|
|
68
61
|
def _empty_results() -> Dict[str, List[Dict[str, Any]]]:
|
|
@@ -8,6 +8,17 @@ from brain.runtime import state_store
|
|
|
8
8
|
|
|
9
9
|
SCHEMA_VERSION = "v1"
|
|
10
10
|
|
|
11
|
+
MEMORY_TABLES = (
|
|
12
|
+
"knowledge",
|
|
13
|
+
"preferences",
|
|
14
|
+
"identity",
|
|
15
|
+
"reflections",
|
|
16
|
+
"directives",
|
|
17
|
+
"tasks",
|
|
18
|
+
"runbooks",
|
|
19
|
+
"lessons",
|
|
20
|
+
)
|
|
21
|
+
|
|
11
22
|
SCHEMA_SQL = """
|
|
12
23
|
CREATE TABLE IF NOT EXISTS memory_events (
|
|
13
24
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
@@ -110,6 +121,26 @@ CREATE TABLE IF NOT EXISTS knowledge (
|
|
|
110
121
|
schema_version TEXT NOT NULL
|
|
111
122
|
);
|
|
112
123
|
|
|
124
|
+
CREATE TABLE IF NOT EXISTS preferences (
|
|
125
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
126
|
+
timestamp TEXT NOT NULL DEFAULT (datetime('now')),
|
|
127
|
+
source TEXT,
|
|
128
|
+
confidence REAL NOT NULL DEFAULT 1.0,
|
|
129
|
+
metadata_json TEXT DEFAULT '{}',
|
|
130
|
+
content TEXT NOT NULL,
|
|
131
|
+
schema_version TEXT NOT NULL
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
CREATE TABLE IF NOT EXISTS identity (
|
|
135
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
136
|
+
timestamp TEXT NOT NULL DEFAULT (datetime('now')),
|
|
137
|
+
source TEXT,
|
|
138
|
+
confidence REAL NOT NULL DEFAULT 1.0,
|
|
139
|
+
metadata_json TEXT DEFAULT '{}',
|
|
140
|
+
content TEXT NOT NULL,
|
|
141
|
+
schema_version TEXT NOT NULL
|
|
142
|
+
);
|
|
143
|
+
|
|
113
144
|
CREATE TABLE IF NOT EXISTS runbooks (
|
|
114
145
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
115
146
|
timestamp TEXT NOT NULL DEFAULT (datetime('now')),
|
|
@@ -14,14 +14,7 @@ from brain.runtime.security import redaction
|
|
|
14
14
|
|
|
15
15
|
LOGFILE = state_store.reports_dir() / "brain_memory.log.jsonl"
|
|
16
16
|
|
|
17
|
-
EMBEDDING_TABLES: tuple[str, ...] = (
|
|
18
|
-
"knowledge",
|
|
19
|
-
"runbooks",
|
|
20
|
-
"lessons",
|
|
21
|
-
"directives",
|
|
22
|
-
"reflections",
|
|
23
|
-
"tasks",
|
|
24
|
-
)
|
|
17
|
+
EMBEDDING_TABLES: tuple[str, ...] = tuple(store.MEMORY_TABLES)
|
|
25
18
|
_REBUILD_LOCK = threading.Lock()
|
|
26
19
|
_WRITE_CHUNK_SIZE = 64
|
|
27
20
|
_EMBEDDING_TEXT_LIMIT = 8000
|
package/ocmemog/sidecar/app.py
CHANGED
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
4
|
import os
|
|
5
|
+
import re
|
|
5
6
|
import threading
|
|
6
7
|
import time
|
|
7
8
|
from pathlib import Path
|
|
@@ -17,9 +18,9 @@ from brain.runtime.memory import api, conversation_state, distill, health, memor
|
|
|
17
18
|
from ocmemog.sidecar.compat import flatten_results, probe_runtime
|
|
18
19
|
from ocmemog.sidecar.transcript_watcher import watch_forever
|
|
19
20
|
|
|
20
|
-
DEFAULT_CATEGORIES = (
|
|
21
|
+
DEFAULT_CATEGORIES = tuple(store.MEMORY_TABLES)
|
|
21
22
|
|
|
22
|
-
app = FastAPI(title="ocmemog sidecar", version="0.1.
|
|
23
|
+
app = FastAPI(title="ocmemog sidecar", version="0.1.9")
|
|
23
24
|
|
|
24
25
|
API_TOKEN = os.environ.get("OCMEMOG_API_TOKEN")
|
|
25
26
|
|
|
@@ -34,6 +35,35 @@ QUEUE_STATS = {
|
|
|
34
35
|
}
|
|
35
36
|
|
|
36
37
|
|
|
38
|
+
_REFLECTION_RECLASSIFY_PREFERENCE_PATTERNS = (
|
|
39
|
+
re.compile(r"\b(?:i|we)\s+(?:prefer|like|love|enjoy)\b", re.IGNORECASE),
|
|
40
|
+
re.compile(r"\b(?:i|we)\s+(?:dislike|hate|avoid)\b", re.IGNORECASE),
|
|
41
|
+
re.compile(r"\bmy favorite\b", re.IGNORECASE),
|
|
42
|
+
re.compile(r"\b(?:the )?user\s+(?:prefers|likes|loves|enjoys|dislikes|hates|avoids)\b", re.IGNORECASE),
|
|
43
|
+
)
|
|
44
|
+
_REFLECTION_RECLASSIFY_IDENTITY_PATTERNS = (
|
|
45
|
+
re.compile(r"\b(?:i am|i'm)\s+(?!thinking\b|trying\b|working on\b|going to\b|not\b)", re.IGNORECASE),
|
|
46
|
+
re.compile(r"\bmy name is\b", re.IGNORECASE),
|
|
47
|
+
re.compile(r"\b(?:i|we)\s+(?:live|work)\s+in\b", re.IGNORECASE),
|
|
48
|
+
re.compile(r"\b(?:i|we)\s+(?:work|study)\s+at\b", re.IGNORECASE),
|
|
49
|
+
re.compile(r"\bmy pronouns are\b", re.IGNORECASE),
|
|
50
|
+
re.compile(r"\bmy (?:time zone|timezone) is\b", re.IGNORECASE),
|
|
51
|
+
re.compile(r"\b(?:the )?user\s+(?:is|works|lives)\b", re.IGNORECASE),
|
|
52
|
+
)
|
|
53
|
+
_REFLECTION_RECLASSIFY_FACT_PATTERNS = (
|
|
54
|
+
re.compile(r"\b(?:i|we)\s+use\b", re.IGNORECASE),
|
|
55
|
+
re.compile(r"\b(?:i|we)\s+have\b", re.IGNORECASE),
|
|
56
|
+
re.compile(r"\b(?:i am|i'm)\s+allergic to\b", re.IGNORECASE),
|
|
57
|
+
re.compile(r"\bmy (?:birthday|email|phone number) is\b", re.IGNORECASE),
|
|
58
|
+
re.compile(r"\b(?:the )?user\s+has\b", re.IGNORECASE),
|
|
59
|
+
)
|
|
60
|
+
_REFLECTION_RECLASSIFY_BLOCKLIST_PATTERNS = (
|
|
61
|
+
re.compile(r"\b(?:reflect|reflection|reflections)\b", re.IGNORECASE),
|
|
62
|
+
re.compile(r"\b(?:i think|i wonder|maybe|perhaps)\b", re.IGNORECASE),
|
|
63
|
+
re.compile(r"\?$"),
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
37
67
|
def _queue_stats_path() -> Path:
|
|
38
68
|
path = state_store.data_dir() / "queue_stats.json"
|
|
39
69
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
@@ -255,6 +285,12 @@ class GovernanceCandidatesRequest(BaseModel):
|
|
|
255
285
|
limit: int = Field(default=50, ge=1, le=200)
|
|
256
286
|
|
|
257
287
|
|
|
288
|
+
class GovernanceReviewRequest(BaseModel):
|
|
289
|
+
categories: Optional[List[str]] = None
|
|
290
|
+
limit: int = Field(default=100, ge=1, le=500)
|
|
291
|
+
context_depth: int = Field(default=1, ge=0, le=2)
|
|
292
|
+
|
|
293
|
+
|
|
258
294
|
class GovernanceDecisionRequest(BaseModel):
|
|
259
295
|
reference: str
|
|
260
296
|
relationship: str
|
|
@@ -262,6 +298,15 @@ class GovernanceDecisionRequest(BaseModel):
|
|
|
262
298
|
approved: bool = True
|
|
263
299
|
|
|
264
300
|
|
|
301
|
+
class GovernanceReviewDecisionRequest(BaseModel):
|
|
302
|
+
reference: str
|
|
303
|
+
target_reference: str
|
|
304
|
+
approved: bool = True
|
|
305
|
+
kind: Optional[str] = None
|
|
306
|
+
relationship: Optional[str] = None
|
|
307
|
+
context_depth: int = Field(default=1, ge=0, le=2)
|
|
308
|
+
|
|
309
|
+
|
|
265
310
|
class GovernanceSummaryRequest(BaseModel):
|
|
266
311
|
categories: Optional[List[str]] = None
|
|
267
312
|
|
|
@@ -311,7 +356,7 @@ class PonderRequest(BaseModel):
|
|
|
311
356
|
class IngestRequest(BaseModel):
|
|
312
357
|
content: str
|
|
313
358
|
kind: str = Field(default="experience", description="experience or memory")
|
|
314
|
-
memory_type: Optional[str] = Field(default=None, description="knowledge|reflections|directives|tasks|runbooks|lessons")
|
|
359
|
+
memory_type: Optional[str] = Field(default=None, description="knowledge|preferences|identity|reflections|directives|tasks|runbooks|lessons")
|
|
315
360
|
source: Optional[str] = None
|
|
316
361
|
task_id: Optional[str] = None
|
|
317
362
|
conversation_id: Optional[str] = None
|
|
@@ -440,6 +485,23 @@ def _runtime_payload() -> Dict[str, Any]:
|
|
|
440
485
|
}
|
|
441
486
|
|
|
442
487
|
|
|
488
|
+
def _retune_reflection_memory_type(content: str, memory_type: str) -> str:
|
|
489
|
+
if memory_type != "reflections":
|
|
490
|
+
return memory_type
|
|
491
|
+
text = re.sub(r"\s+", " ", str(content or "")).strip()
|
|
492
|
+
if not text or len(text) > 280:
|
|
493
|
+
return memory_type
|
|
494
|
+
if any(pattern.search(text) for pattern in _REFLECTION_RECLASSIFY_BLOCKLIST_PATTERNS):
|
|
495
|
+
return memory_type
|
|
496
|
+
if any(pattern.search(text) for pattern in _REFLECTION_RECLASSIFY_PREFERENCE_PATTERNS):
|
|
497
|
+
return "preferences"
|
|
498
|
+
if any(pattern.search(text) for pattern in _REFLECTION_RECLASSIFY_IDENTITY_PATTERNS):
|
|
499
|
+
return "identity"
|
|
500
|
+
if any(pattern.search(text) for pattern in _REFLECTION_RECLASSIFY_FACT_PATTERNS):
|
|
501
|
+
return "identity"
|
|
502
|
+
return memory_type
|
|
503
|
+
|
|
504
|
+
|
|
443
505
|
def _fallback_search(query: str, limit: int, categories: List[str]) -> List[Dict[str, Any]]:
|
|
444
506
|
conn = store.connect()
|
|
445
507
|
try:
|
|
@@ -468,12 +530,7 @@ def _fallback_search(query: str, limit: int, categories: List[str]) -> List[Dict
|
|
|
468
530
|
|
|
469
531
|
|
|
470
532
|
_ALLOWED_MEMORY_REFERENCE_TYPES = {
|
|
471
|
-
|
|
472
|
-
"reflections",
|
|
473
|
-
"directives",
|
|
474
|
-
"tasks",
|
|
475
|
-
"runbooks",
|
|
476
|
-
"lessons",
|
|
533
|
+
*store.MEMORY_TABLES,
|
|
477
534
|
"conversation_turns",
|
|
478
535
|
"conversation_checkpoints",
|
|
479
536
|
}
|
|
@@ -497,7 +554,7 @@ def _get_row(reference: str) -> Optional[Dict[str, Any]]:
|
|
|
497
554
|
prefix, identifier = parsed
|
|
498
555
|
if prefix not in _ALLOWED_MEMORY_REFERENCE_TYPES:
|
|
499
556
|
return None
|
|
500
|
-
if prefix in {
|
|
557
|
+
if prefix in {*store.MEMORY_TABLES, "conversation_turns", "conversation_checkpoints"} and not identifier.isdigit():
|
|
501
558
|
return None
|
|
502
559
|
return provenance.hydrate_reference(reference, depth=2)
|
|
503
560
|
|
|
@@ -699,6 +756,24 @@ def memory_governance_candidates(request: GovernanceCandidatesRequest) -> dict[s
|
|
|
699
756
|
}
|
|
700
757
|
|
|
701
758
|
|
|
759
|
+
@app.post("/memory/governance/review")
|
|
760
|
+
def memory_governance_review(request: GovernanceReviewRequest) -> dict[str, Any]:
|
|
761
|
+
runtime = _runtime_payload()
|
|
762
|
+
items = api.list_governance_review_items(
|
|
763
|
+
categories=request.categories,
|
|
764
|
+
limit=request.limit,
|
|
765
|
+
context_depth=request.context_depth,
|
|
766
|
+
)
|
|
767
|
+
return {
|
|
768
|
+
"ok": True,
|
|
769
|
+
"categories": request.categories,
|
|
770
|
+
"limit": request.limit,
|
|
771
|
+
"context_depth": request.context_depth,
|
|
772
|
+
"items": items,
|
|
773
|
+
**runtime,
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
|
|
702
777
|
@app.post("/memory/governance/decision")
|
|
703
778
|
def memory_governance_decision(request: GovernanceDecisionRequest) -> dict[str, Any]:
|
|
704
779
|
runtime = _runtime_payload()
|
|
@@ -719,6 +794,30 @@ def memory_governance_decision(request: GovernanceDecisionRequest) -> dict[str,
|
|
|
719
794
|
}
|
|
720
795
|
|
|
721
796
|
|
|
797
|
+
@app.post("/memory/governance/review/decision")
|
|
798
|
+
def memory_governance_review_decision(request: GovernanceReviewDecisionRequest) -> dict[str, Any]:
|
|
799
|
+
runtime = _runtime_payload()
|
|
800
|
+
result = api.apply_governance_review_decision(
|
|
801
|
+
request.reference,
|
|
802
|
+
target_reference=request.target_reference,
|
|
803
|
+
approved=request.approved,
|
|
804
|
+
kind=request.kind,
|
|
805
|
+
relationship=request.relationship,
|
|
806
|
+
context_depth=request.context_depth,
|
|
807
|
+
)
|
|
808
|
+
return {
|
|
809
|
+
"ok": result is not None,
|
|
810
|
+
"reference": request.reference,
|
|
811
|
+
"target_reference": request.target_reference,
|
|
812
|
+
"approved": request.approved,
|
|
813
|
+
"kind": request.kind,
|
|
814
|
+
"relationship": request.relationship,
|
|
815
|
+
"context_depth": request.context_depth,
|
|
816
|
+
"result": result,
|
|
817
|
+
**runtime,
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
|
|
722
821
|
@app.post("/memory/governance/summary")
|
|
723
822
|
def memory_governance_summary(request: GovernanceSummaryRequest) -> dict[str, Any]:
|
|
724
823
|
runtime = _runtime_payload()
|
|
@@ -815,7 +914,7 @@ def memory_get(request: GetRequest) -> dict[str, Any]:
|
|
|
815
914
|
"reference": request.reference,
|
|
816
915
|
**runtime,
|
|
817
916
|
}
|
|
818
|
-
if prefix in {
|
|
917
|
+
if prefix in {*store.MEMORY_TABLES, "conversation_turns", "conversation_checkpoints"} and not identifier.isdigit():
|
|
819
918
|
return {
|
|
820
919
|
"ok": False,
|
|
821
920
|
"error": "invalid_reference_id",
|
|
@@ -1072,9 +1171,10 @@ def _ingest_request(request: IngestRequest) -> dict[str, Any]:
|
|
|
1072
1171
|
kind = (request.kind or "experience").lower()
|
|
1073
1172
|
if kind == "memory":
|
|
1074
1173
|
memory_type = (request.memory_type or "knowledge").lower()
|
|
1075
|
-
allowed =
|
|
1174
|
+
allowed = set(store.MEMORY_TABLES)
|
|
1076
1175
|
if memory_type not in allowed:
|
|
1077
1176
|
memory_type = "knowledge"
|
|
1177
|
+
memory_type = _retune_reflection_memory_type(content, memory_type)
|
|
1078
1178
|
metadata = {
|
|
1079
1179
|
"conversation_id": request.conversation_id,
|
|
1080
1180
|
"session_id": request.session_id,
|
|
@@ -1249,7 +1349,7 @@ def metrics() -> dict[str, Any]:
|
|
|
1249
1349
|
counts["queue_processed"] = QUEUE_STATS.get("processed", 0)
|
|
1250
1350
|
counts["queue_errors"] = QUEUE_STATS.get("errors", 0)
|
|
1251
1351
|
payload["counts"] = counts
|
|
1252
|
-
coverage_tables =
|
|
1352
|
+
coverage_tables = list(store.MEMORY_TABLES)
|
|
1253
1353
|
conn = store.connect()
|
|
1254
1354
|
try:
|
|
1255
1355
|
payload["coverage"] = [
|
|
@@ -1302,7 +1402,7 @@ def _tail_events(limit: int = 50) -> str:
|
|
|
1302
1402
|
def dashboard() -> HTMLResponse:
|
|
1303
1403
|
metrics_payload = health.get_memory_health()
|
|
1304
1404
|
counts = metrics_payload.get("counts", {})
|
|
1305
|
-
coverage_tables =
|
|
1405
|
+
coverage_tables = list(store.MEMORY_TABLES)
|
|
1306
1406
|
conn = store.connect()
|
|
1307
1407
|
try:
|
|
1308
1408
|
coverage_rows = []
|
|
@@ -1370,6 +1470,15 @@ def dashboard() -> HTMLResponse:
|
|
|
1370
1470
|
body {{ font-family: system-ui, sans-serif; padding: 20px; }}
|
|
1371
1471
|
.metrics {{ display: flex; gap: 12px; flex-wrap: wrap; }}
|
|
1372
1472
|
.card {{ border: 1px solid #ddd; padding: 10px 14px; border-radius: 8px; min-width: 140px; }}
|
|
1473
|
+
.panel {{ margin-top: 24px; }}
|
|
1474
|
+
.controls {{ display: flex; gap: 12px; flex-wrap: wrap; align-items: center; margin: 8px 0; }}
|
|
1475
|
+
.controls label {{ display: flex; gap: 6px; align-items: center; }}
|
|
1476
|
+
table {{ width: 100%; border-collapse: collapse; margin-top: 8px; }}
|
|
1477
|
+
th, td {{ border: 1px solid #ddd; padding: 8px; text-align: left; vertical-align: top; }}
|
|
1478
|
+
th {{ background: #f7f7f7; }}
|
|
1479
|
+
button {{ padding: 6px 10px; }}
|
|
1480
|
+
.muted {{ color: #666; }}
|
|
1481
|
+
.error {{ color: #a40000; }}
|
|
1373
1482
|
pre {{ background: #f7f7f7; padding: 10px; height: 320px; overflow: auto; }}
|
|
1374
1483
|
</style>
|
|
1375
1484
|
</head>
|
|
@@ -1380,6 +1489,45 @@ def dashboard() -> HTMLResponse:
|
|
|
1380
1489
|
<div class="metrics" id="local-cognition">{local_html}</div>
|
|
1381
1490
|
<h3>Vector coverage</h3>
|
|
1382
1491
|
<div class="metrics" id="coverage">{coverage_html}</div>
|
|
1492
|
+
<div class="panel">
|
|
1493
|
+
<h3>Governance review</h3>
|
|
1494
|
+
<div class="controls">
|
|
1495
|
+
<label>Kind
|
|
1496
|
+
<select id="review-kind-filter">
|
|
1497
|
+
<option value="">All</option>
|
|
1498
|
+
<option value="duplicate_candidate">Duplicate</option>
|
|
1499
|
+
<option value="contradiction_candidate">Contradiction</option>
|
|
1500
|
+
<option value="supersession_recommendation">Supersession</option>
|
|
1501
|
+
</select>
|
|
1502
|
+
</label>
|
|
1503
|
+
<label>Priority
|
|
1504
|
+
<select id="review-priority-filter">
|
|
1505
|
+
<option value="">All</option>
|
|
1506
|
+
<option value="90">90</option>
|
|
1507
|
+
<option value="70">70</option>
|
|
1508
|
+
<option value="40">40</option>
|
|
1509
|
+
</select>
|
|
1510
|
+
</label>
|
|
1511
|
+
<button id="review-refresh" type="button">Refresh</button>
|
|
1512
|
+
</div>
|
|
1513
|
+
<div id="review-note" class="muted">Loading review items...</div>
|
|
1514
|
+
<div id="review-error" class="error"></div>
|
|
1515
|
+
<table>
|
|
1516
|
+
<thead>
|
|
1517
|
+
<tr>
|
|
1518
|
+
<th>Priority</th>
|
|
1519
|
+
<th>Kind</th>
|
|
1520
|
+
<th>Source</th>
|
|
1521
|
+
<th>Target</th>
|
|
1522
|
+
<th>Summary</th>
|
|
1523
|
+
<th>Actions</th>
|
|
1524
|
+
</tr>
|
|
1525
|
+
</thead>
|
|
1526
|
+
<tbody id="review-table-body">
|
|
1527
|
+
<tr><td colspan="6" class="muted">Loading...</td></tr>
|
|
1528
|
+
</tbody>
|
|
1529
|
+
</table>
|
|
1530
|
+
</div>
|
|
1383
1531
|
<h3>Ponder recommendations</h3>
|
|
1384
1532
|
<div id="ponder-meta" style="margin-bottom:8px; color:#666;"></div>
|
|
1385
1533
|
<div id="ponder"></div>
|
|
@@ -1388,9 +1536,109 @@ def dashboard() -> HTMLResponse:
|
|
|
1388
1536
|
<script>
|
|
1389
1537
|
const metricsEl = document.getElementById('metrics');
|
|
1390
1538
|
const coverageEl = document.getElementById('coverage');
|
|
1539
|
+
const reviewKindFilterEl = document.getElementById('review-kind-filter');
|
|
1540
|
+
const reviewPriorityFilterEl = document.getElementById('review-priority-filter');
|
|
1541
|
+
const reviewRefreshEl = document.getElementById('review-refresh');
|
|
1542
|
+
const reviewNoteEl = document.getElementById('review-note');
|
|
1543
|
+
const reviewErrorEl = document.getElementById('review-error');
|
|
1544
|
+
const reviewTableBodyEl = document.getElementById('review-table-body');
|
|
1391
1545
|
const ponderEl = document.getElementById('ponder');
|
|
1392
1546
|
const ponderMetaEl = document.getElementById('ponder-meta');
|
|
1393
1547
|
const eventsEl = document.getElementById('events');
|
|
1548
|
+
let reviewItems = [];
|
|
1549
|
+
let reviewLastRefresh = null;
|
|
1550
|
+
const pendingReviewIds = new Set();
|
|
1551
|
+
|
|
1552
|
+
/**
|
|
1553
|
+
* @typedef {{Object}} GovernanceReviewContext
|
|
1554
|
+
* @property {{string}} reference
|
|
1555
|
+
* @property {{string | undefined}} bucket
|
|
1556
|
+
* @property {{number | undefined}} id
|
|
1557
|
+
* @property {{string | undefined}} timestamp
|
|
1558
|
+
* @property {{string | undefined}} content
|
|
1559
|
+
* @property {{string | undefined}} memory_status
|
|
1560
|
+
*
|
|
1561
|
+
* @typedef {{Object}} GovernanceReviewItem
|
|
1562
|
+
* @property {{string}} review_id
|
|
1563
|
+
* @property {{string}} kind
|
|
1564
|
+
* @property {{string | undefined}} kind_label
|
|
1565
|
+
* @property {{string | undefined}} relationship
|
|
1566
|
+
* @property {{number}} priority
|
|
1567
|
+
* @property {{string | undefined}} timestamp
|
|
1568
|
+
* @property {{number | undefined}} signal
|
|
1569
|
+
* @property {{string | undefined}} summary
|
|
1570
|
+
* @property {{string}} reference
|
|
1571
|
+
* @property {{string}} target_reference
|
|
1572
|
+
* @property {{GovernanceReviewContext | undefined}} source
|
|
1573
|
+
* @property {{GovernanceReviewContext | undefined}} target
|
|
1574
|
+
*/
|
|
1575
|
+
|
|
1576
|
+
function escapeHtml(value) {{
|
|
1577
|
+
return String(value ?? '')
|
|
1578
|
+
.replaceAll('&', '&')
|
|
1579
|
+
.replaceAll('<', '<')
|
|
1580
|
+
.replaceAll('>', '>')
|
|
1581
|
+
.replaceAll('"', '"')
|
|
1582
|
+
.replaceAll("'", ''');
|
|
1583
|
+
}}
|
|
1584
|
+
|
|
1585
|
+
function formatTimestamp(value) {{
|
|
1586
|
+
if (!value) {{
|
|
1587
|
+
return 'n/a';
|
|
1588
|
+
}}
|
|
1589
|
+
const parsed = new Date(value);
|
|
1590
|
+
return Number.isNaN(parsed.getTime()) ? String(value) : parsed.toLocaleString();
|
|
1591
|
+
}}
|
|
1592
|
+
|
|
1593
|
+
function renderReviewTable() {{
|
|
1594
|
+
const kindFilter = reviewKindFilterEl.value;
|
|
1595
|
+
const priorityFilter = reviewPriorityFilterEl.value;
|
|
1596
|
+
/** @type {{GovernanceReviewItem[]}} */
|
|
1597
|
+
const filtered = reviewItems.filter((item) => {{
|
|
1598
|
+
if (kindFilter && item.kind !== kindFilter) {{
|
|
1599
|
+
return false;
|
|
1600
|
+
}}
|
|
1601
|
+
if (priorityFilter && String(item.priority) !== priorityFilter) {{
|
|
1602
|
+
return false;
|
|
1603
|
+
}}
|
|
1604
|
+
return true;
|
|
1605
|
+
}});
|
|
1606
|
+
|
|
1607
|
+
reviewNoteEl.textContent = `${{filtered.length}} items shown${{reviewItems.length !== filtered.length ? ` of ${{reviewItems.length}}` : ''}} • Last refresh: ${{reviewLastRefresh ? formatTimestamp(reviewLastRefresh) : 'n/a'}}`;
|
|
1608
|
+
|
|
1609
|
+
if (!filtered.length) {{
|
|
1610
|
+
reviewTableBodyEl.innerHTML = '<tr><td colspan="6" class="muted">No review items match the current filters.</td></tr>';
|
|
1611
|
+
return;
|
|
1612
|
+
}}
|
|
1613
|
+
|
|
1614
|
+
reviewTableBodyEl.innerHTML = filtered.map((item) => {{
|
|
1615
|
+
const disabled = pendingReviewIds.has(item.review_id) ? 'disabled' : '';
|
|
1616
|
+
const sourceContent = item.source?.content || item.reference;
|
|
1617
|
+
const targetContent = item.target?.content || item.target_reference;
|
|
1618
|
+
return `
|
|
1619
|
+
<tr>
|
|
1620
|
+
<td>${{escapeHtml(item.priority)}}</td>
|
|
1621
|
+
<td>${{escapeHtml(item.kind_label || item.kind)}}</td>
|
|
1622
|
+
<td>
|
|
1623
|
+
<strong>${{escapeHtml(item.reference)}}</strong><br/>
|
|
1624
|
+
<span class="muted">${{escapeHtml(sourceContent)}}</span>
|
|
1625
|
+
</td>
|
|
1626
|
+
<td>
|
|
1627
|
+
<strong>${{escapeHtml(item.target_reference)}}</strong><br/>
|
|
1628
|
+
<span class="muted">${{escapeHtml(targetContent)}}</span>
|
|
1629
|
+
</td>
|
|
1630
|
+
<td>
|
|
1631
|
+
<strong>${{escapeHtml(item.summary || '')}}</strong><br/>
|
|
1632
|
+
<span class="muted">${{escapeHtml(item.relationship || '')}}${{item.signal ? ` • signal ${{item.signal}}` : ''}}</span>
|
|
1633
|
+
</td>
|
|
1634
|
+
<td>
|
|
1635
|
+
<button type="button" data-review-id="${{escapeHtml(item.review_id)}}" data-approved="true" ${{disabled}}>Approve</button>
|
|
1636
|
+
<button type="button" data-review-id="${{escapeHtml(item.review_id)}}" data-approved="false" ${{disabled}}>Reject</button>
|
|
1637
|
+
</td>
|
|
1638
|
+
</tr>
|
|
1639
|
+
`;
|
|
1640
|
+
}}).join('');
|
|
1641
|
+
}}
|
|
1394
1642
|
|
|
1395
1643
|
async function refreshMetrics() {{
|
|
1396
1644
|
const res = await fetch('/metrics');
|
|
@@ -1423,9 +1671,81 @@ def dashboard() -> HTMLResponse:
|
|
|
1423
1671
|
).join('');
|
|
1424
1672
|
}}
|
|
1425
1673
|
|
|
1674
|
+
async function refreshGovernanceReview() {{
|
|
1675
|
+
reviewErrorEl.textContent = '';
|
|
1676
|
+
try {{
|
|
1677
|
+
const res = await fetch('/memory/governance/review', {{
|
|
1678
|
+
method: 'POST',
|
|
1679
|
+
headers: {{ 'Content-Type': 'application/json' }},
|
|
1680
|
+
body: JSON.stringify({{ limit: 100, context_depth: 1 }}),
|
|
1681
|
+
}});
|
|
1682
|
+
const data = await res.json();
|
|
1683
|
+
if (!res.ok || !data.ok) {{
|
|
1684
|
+
throw new Error(data.error || `review request failed: ${{res.status}}`);
|
|
1685
|
+
}}
|
|
1686
|
+
reviewItems = Array.isArray(data.items) ? data.items : [];
|
|
1687
|
+
reviewLastRefresh = new Date().toISOString();
|
|
1688
|
+
renderReviewTable();
|
|
1689
|
+
}} catch (error) {{
|
|
1690
|
+
reviewErrorEl.textContent = error instanceof Error ? error.message : String(error);
|
|
1691
|
+
reviewTableBodyEl.innerHTML = '<tr><td colspan="6" class="muted">Unable to load review items.</td></tr>';
|
|
1692
|
+
}}
|
|
1693
|
+
}}
|
|
1694
|
+
|
|
1695
|
+
async function applyGovernanceReviewDecision(reviewId, approved) {{
|
|
1696
|
+
const item = reviewItems.find((entry) => entry.review_id === reviewId);
|
|
1697
|
+
if (!item) {{
|
|
1698
|
+
return;
|
|
1699
|
+
}}
|
|
1700
|
+
pendingReviewIds.add(reviewId);
|
|
1701
|
+
renderReviewTable();
|
|
1702
|
+
reviewErrorEl.textContent = '';
|
|
1703
|
+
try {{
|
|
1704
|
+
const res = await fetch('/memory/governance/review/decision', {{
|
|
1705
|
+
method: 'POST',
|
|
1706
|
+
headers: {{ 'Content-Type': 'application/json' }},
|
|
1707
|
+
body: JSON.stringify({{
|
|
1708
|
+
reference: item.reference,
|
|
1709
|
+
target_reference: item.target_reference,
|
|
1710
|
+
approved,
|
|
1711
|
+
kind: item.kind,
|
|
1712
|
+
relationship: item.relationship,
|
|
1713
|
+
context_depth: 1,
|
|
1714
|
+
}}),
|
|
1715
|
+
}});
|
|
1716
|
+
const data = await res.json();
|
|
1717
|
+
if (!res.ok || !data.ok) {{
|
|
1718
|
+
throw new Error(data.error || `decision request failed: ${{res.status}}`);
|
|
1719
|
+
}}
|
|
1720
|
+
await refreshGovernanceReview();
|
|
1721
|
+
}} catch (error) {{
|
|
1722
|
+
reviewErrorEl.textContent = error instanceof Error ? error.message : String(error);
|
|
1723
|
+
}} finally {{
|
|
1724
|
+
pendingReviewIds.delete(reviewId);
|
|
1725
|
+
renderReviewTable();
|
|
1726
|
+
}}
|
|
1727
|
+
}}
|
|
1728
|
+
|
|
1729
|
+
reviewKindFilterEl.addEventListener('change', renderReviewTable);
|
|
1730
|
+
reviewPriorityFilterEl.addEventListener('change', renderReviewTable);
|
|
1731
|
+
reviewRefreshEl.addEventListener('click', refreshGovernanceReview);
|
|
1732
|
+
reviewTableBodyEl.addEventListener('click', (event) => {{
|
|
1733
|
+
const target = event.target;
|
|
1734
|
+
if (!(target instanceof HTMLButtonElement)) {{
|
|
1735
|
+
return;
|
|
1736
|
+
}}
|
|
1737
|
+
const reviewId = target.dataset.reviewId;
|
|
1738
|
+
if (!reviewId) {{
|
|
1739
|
+
return;
|
|
1740
|
+
}}
|
|
1741
|
+
applyGovernanceReviewDecision(reviewId, target.dataset.approved === 'true');
|
|
1742
|
+
}});
|
|
1743
|
+
|
|
1426
1744
|
refreshMetrics();
|
|
1745
|
+
refreshGovernanceReview();
|
|
1427
1746
|
refreshPonder();
|
|
1428
1747
|
setInterval(refreshMetrics, 5000);
|
|
1748
|
+
setInterval(refreshGovernanceReview, 15000);
|
|
1429
1749
|
setInterval(refreshPonder, 10000);
|
|
1430
1750
|
|
|
1431
1751
|
const es = new EventSource('/events');
|
package/package.json
CHANGED