@simbimbo/memory-ocmemog 0.1.11 → 0.1.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +83 -18
  3. package/brain/runtime/__init__.py +2 -12
  4. package/brain/runtime/config.py +1 -24
  5. package/brain/runtime/inference.py +1 -151
  6. package/brain/runtime/instrumentation.py +1 -15
  7. package/brain/runtime/memory/__init__.py +3 -13
  8. package/brain/runtime/memory/api.py +1 -1219
  9. package/brain/runtime/memory/candidate.py +1 -185
  10. package/brain/runtime/memory/conversation_state.py +1 -1823
  11. package/brain/runtime/memory/distill.py +1 -344
  12. package/brain/runtime/memory/embedding_engine.py +1 -92
  13. package/brain/runtime/memory/freshness.py +1 -112
  14. package/brain/runtime/memory/health.py +1 -40
  15. package/brain/runtime/memory/integrity.py +1 -186
  16. package/brain/runtime/memory/memory_consolidation.py +1 -58
  17. package/brain/runtime/memory/memory_links.py +1 -107
  18. package/brain/runtime/memory/memory_salience.py +1 -233
  19. package/brain/runtime/memory/memory_synthesis.py +1 -31
  20. package/brain/runtime/memory/memory_taxonomy.py +1 -33
  21. package/brain/runtime/memory/pondering_engine.py +1 -654
  22. package/brain/runtime/memory/promote.py +1 -277
  23. package/brain/runtime/memory/provenance.py +1 -406
  24. package/brain/runtime/memory/reinforcement.py +1 -71
  25. package/brain/runtime/memory/retrieval.py +1 -210
  26. package/brain/runtime/memory/semantic_search.py +1 -64
  27. package/brain/runtime/memory/store.py +1 -429
  28. package/brain/runtime/memory/unresolved_state.py +1 -91
  29. package/brain/runtime/memory/vector_index.py +1 -323
  30. package/brain/runtime/model_roles.py +1 -9
  31. package/brain/runtime/model_router.py +1 -22
  32. package/brain/runtime/providers.py +1 -66
  33. package/brain/runtime/security/redaction.py +1 -12
  34. package/brain/runtime/state_store.py +1 -23
  35. package/brain/runtime/storage_paths.py +1 -39
  36. package/docs/architecture/memory.md +20 -24
  37. package/docs/release-checklist.md +19 -6
  38. package/docs/usage.md +33 -17
  39. package/index.ts +8 -1
  40. package/ocmemog/__init__.py +11 -0
  41. package/ocmemog/doctor.py +1255 -0
  42. package/ocmemog/runtime/__init__.py +18 -0
  43. package/ocmemog/runtime/_compat_bridge.py +28 -0
  44. package/ocmemog/runtime/config.py +35 -0
  45. package/ocmemog/runtime/identity.py +115 -0
  46. package/ocmemog/runtime/inference.py +164 -0
  47. package/ocmemog/runtime/instrumentation.py +20 -0
  48. package/ocmemog/runtime/memory/__init__.py +91 -0
  49. package/ocmemog/runtime/memory/api.py +1431 -0
  50. package/ocmemog/runtime/memory/candidate.py +192 -0
  51. package/ocmemog/runtime/memory/conversation_state.py +1831 -0
  52. package/ocmemog/runtime/memory/distill.py +282 -0
  53. package/ocmemog/runtime/memory/embedding_engine.py +151 -0
  54. package/ocmemog/runtime/memory/freshness.py +114 -0
  55. package/ocmemog/runtime/memory/health.py +57 -0
  56. package/ocmemog/runtime/memory/integrity.py +208 -0
  57. package/ocmemog/runtime/memory/memory_consolidation.py +60 -0
  58. package/ocmemog/runtime/memory/memory_links.py +109 -0
  59. package/ocmemog/runtime/memory/memory_salience.py +235 -0
  60. package/ocmemog/runtime/memory/memory_synthesis.py +33 -0
  61. package/ocmemog/runtime/memory/memory_taxonomy.py +35 -0
  62. package/ocmemog/runtime/memory/pondering_engine.py +681 -0
  63. package/ocmemog/runtime/memory/promote.py +279 -0
  64. package/ocmemog/runtime/memory/provenance.py +408 -0
  65. package/ocmemog/runtime/memory/reinforcement.py +73 -0
  66. package/ocmemog/runtime/memory/retrieval.py +224 -0
  67. package/ocmemog/runtime/memory/semantic_search.py +66 -0
  68. package/ocmemog/runtime/memory/store.py +433 -0
  69. package/ocmemog/runtime/memory/unresolved_state.py +93 -0
  70. package/ocmemog/runtime/memory/vector_index.py +411 -0
  71. package/ocmemog/runtime/model_roles.py +16 -0
  72. package/ocmemog/runtime/model_router.py +29 -0
  73. package/ocmemog/runtime/providers.py +79 -0
  74. package/ocmemog/runtime/roles.py +92 -0
  75. package/ocmemog/runtime/security/__init__.py +8 -0
  76. package/ocmemog/runtime/security/redaction.py +17 -0
  77. package/ocmemog/runtime/state_store.py +34 -0
  78. package/ocmemog/runtime/storage_paths.py +70 -0
  79. package/ocmemog/sidecar/app.py +310 -23
  80. package/ocmemog/sidecar/compat.py +50 -13
  81. package/ocmemog/sidecar/transcript_watcher.py +318 -240
  82. package/openclaw.plugin.json +4 -0
  83. package/package.json +1 -1
  84. package/scripts/ocmemog-backfill-vectors.py +5 -3
  85. package/scripts/ocmemog-continuity-benchmark.py +1 -1
  86. package/scripts/ocmemog-demo.py +1 -1
  87. package/scripts/ocmemog-doctor.py +15 -0
  88. package/scripts/ocmemog-install.sh +29 -7
  89. package/scripts/ocmemog-integrated-proof.py +373 -0
  90. package/scripts/ocmemog-reindex-vectors.py +5 -3
  91. package/scripts/ocmemog-release-check.sh +330 -0
  92. package/scripts/ocmemog-sidecar.sh +4 -2
  93. package/scripts/ocmemog-test-rig.py +5 -3
  94. package/brain/runtime/memory/artifacts.py +0 -33
  95. package/brain/runtime/memory/context_builder.py +0 -112
  96. package/brain/runtime/memory/interaction_memory.py +0 -57
  97. package/brain/runtime/memory/memory_gate.py +0 -38
  98. package/brain/runtime/memory/memory_graph.py +0 -54
  99. package/brain/runtime/memory/person_identity.py +0 -83
  100. package/brain/runtime/memory/person_memory.py +0 -138
  101. package/brain/runtime/memory/sentiment_memory.py +0 -67
  102. package/brain/runtime/memory/tool_catalog.py +0 -68
@@ -0,0 +1,1431 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ from typing import List, Dict, Any, Optional
6
+
7
+ from ocmemog.runtime import inference
8
+ from ocmemog.runtime.instrumentation import emit_event
9
+ from . import provenance, store
10
+ from ocmemog.runtime.security import redaction
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
+
33
+
34
+ def _sanitize(text: str) -> str:
35
+ redacted, _ = redaction.redact_text(text)
36
+ return redacted
37
+
38
+
39
+ def _parse_memory_reference(reference: str) -> tuple[str, str] | None:
40
+ if ":" not in str(reference or ""):
41
+ return None
42
+ table, identifier = str(reference).split(":", 1)
43
+ table = table.strip()
44
+ identifier = identifier.strip()
45
+ if not table or not identifier:
46
+ return None
47
+ try:
48
+ int(identifier)
49
+ except Exception:
50
+ return None
51
+ return table, identifier
52
+
53
+
54
+ def _auto_attach_model_hints_enabled() -> bool:
55
+ return os.environ.get("OCMEMOG_AUTO_ATTACH_GOVERNANCE_USE_MODEL_HINTS", "true").strip().lower() in {"1", "true", "yes"}
56
+
57
+
58
+ def _auto_attach_model_hint_budget() -> int:
59
+ raw = os.environ.get("OCMEMOG_AUTO_ATTACH_GOVERNANCE_MODEL_HINT_BUDGET", "2")
60
+ try:
61
+ return max(0, int(raw or 0))
62
+ except Exception:
63
+ return 2
64
+
65
+
66
+ def _auto_attach_min_tokens() -> int:
67
+ raw = os.environ.get("OCMEMOG_AUTO_ATTACH_GOVERNANCE_MIN_TOKENS", "4")
68
+ try:
69
+ return max(2, int(raw or 0))
70
+ except Exception:
71
+ return 4
72
+
73
+
74
+ def _governance_candidates_significant(content: str) -> bool:
75
+ tokens = set(_tokenize(content))
76
+ if len(tokens) >= _auto_attach_min_tokens():
77
+ return True
78
+ if _extract_literals(content):
79
+ return True
80
+ return False
81
+
82
+
83
+ def _emit(event: str) -> None:
84
+ emit_event(store.state_store.report_log_path(), event, status="ok")
85
+
86
+
87
+ def record_event(event_type: str, payload: str, *, source: str | None = None) -> None:
88
+ payload = _sanitize(payload)
89
+ details_json = json.dumps({"payload": payload})
90
+ def _write() -> None:
91
+ conn = store.connect()
92
+ try:
93
+ conn.execute(
94
+ "INSERT INTO memory_events (event_type, source, details_json, schema_version) VALUES (?, ?, ?, ?)",
95
+ (event_type, source, details_json, store.SCHEMA_VERSION),
96
+ )
97
+ conn.commit()
98
+ finally:
99
+ conn.close()
100
+
101
+ store.submit_write(_write, timeout=30.0)
102
+ _emit("record_event")
103
+
104
+
105
+ def record_task(task_id: str, status: str, *, source: str | None = None) -> None:
106
+ status = _sanitize(status)
107
+ metadata_json = json.dumps({"task_id": task_id})
108
+ def _write() -> None:
109
+ conn = store.connect()
110
+ try:
111
+ conn.execute(
112
+ "INSERT INTO tasks (source, confidence, metadata_json, content, schema_version) VALUES (?, ?, ?, ?, ?)",
113
+ (source, 1.0, metadata_json, status, store.SCHEMA_VERSION),
114
+ )
115
+ conn.commit()
116
+ finally:
117
+ conn.close()
118
+
119
+ store.submit_write(_write, timeout=30.0)
120
+ _emit("record_task")
121
+
122
+
123
+ def _recommend_supersession_from_contradictions(
124
+ reference: str,
125
+ *,
126
+ contradiction_candidates: List[Dict[str, Any]],
127
+ ) -> Dict[str, Any]:
128
+ recommendation = {
129
+ "recommended": False,
130
+ "target_reference": None,
131
+ "reason": "no_candidates",
132
+ "signal": 0.0,
133
+ "auto_applied": False,
134
+ }
135
+ if not contradiction_candidates:
136
+ return recommendation
137
+
138
+ signal_threshold = float(os.environ.get("OCMEMOG_GOVERNANCE_SUPERSESSION_RECOMMEND_SIGNAL", "0.9") or 0.9)
139
+ model_conf_threshold = float(os.environ.get("OCMEMOG_GOVERNANCE_SUPERSESSION_MODEL_CONFIDENCE", "0.9") or 0.9)
140
+ ranked = sorted(contradiction_candidates, key=lambda item: float(item.get("signal") or 0.0), reverse=True)
141
+ top = ranked[0]
142
+ signal = float(top.get("signal") or 0.0)
143
+ model_hint = top.get("model_hint") if isinstance(top.get("model_hint"), dict) else {}
144
+ model_contradiction = bool(model_hint.get("contradiction"))
145
+ model_confidence = float(model_hint.get("confidence") or 0.0)
146
+
147
+ if signal < signal_threshold:
148
+ recommendation["reason"] = "signal_below_threshold"
149
+ recommendation["signal"] = signal
150
+ return recommendation
151
+
152
+ if model_hint and (not model_contradiction or model_confidence < model_conf_threshold):
153
+ recommendation["reason"] = "model_hint_not_strong_enough"
154
+ recommendation["signal"] = signal
155
+ return recommendation
156
+
157
+ target = str(top.get("reference") or "")
158
+ if not target:
159
+ recommendation["reason"] = "missing_target"
160
+ recommendation["signal"] = signal
161
+ return recommendation
162
+
163
+ recommendation.update({
164
+ "recommended": True,
165
+ "target_reference": target,
166
+ "reason": "high_confidence_contradiction",
167
+ "signal": signal,
168
+ "model_hint": model_hint,
169
+ })
170
+
171
+ return recommendation
172
+
173
+
174
+ def _canonicalize_duplicate_target(reference: str) -> str:
175
+ payload = provenance.fetch_reference(reference) or {}
176
+ metadata = payload.get("metadata") or {}
177
+ prov = metadata.get("provenance") if isinstance(metadata.get("provenance"), dict) else {}
178
+ canonical = str(prov.get("canonical_reference") or prov.get("duplicate_of") or reference).strip()
179
+ return canonical or reference
180
+
181
+
182
+ def _token_signature(text: str) -> frozenset[str]:
183
+ return frozenset(_tokenize(text))
184
+
185
+
186
+ def _auto_promote_duplicate_candidate(
187
+ reference: str,
188
+ *,
189
+ duplicate_candidates: List[Dict[str, Any]],
190
+ contradiction_candidates: List[Dict[str, Any]],
191
+ ) -> Dict[str, Any]:
192
+ auto_promote_enabled = os.environ.get("OCMEMOG_GOVERNANCE_AUTOPROMOTE", "true").strip().lower() in {"1", "true", "yes"}
193
+ allow_with_contradictions = os.environ.get("OCMEMOG_GOVERNANCE_AUTOPROMOTE_ALLOW_CONTRADICTIONS", "false").strip().lower() in {"1", "true", "yes"}
194
+ duplicate_threshold = float(os.environ.get("OCMEMOG_GOVERNANCE_DUPLICATE_AUTOPROMOTE_SIMILARITY", "0.98") or 0.98)
195
+ duplicate_margin = float(os.environ.get("OCMEMOG_GOVERNANCE_DUPLICATE_AUTOPROMOTE_MARGIN", "0.02") or 0.02)
196
+ require_exact_tokens = os.environ.get("OCMEMOG_GOVERNANCE_DUPLICATE_AUTOPROMOTE_REQUIRE_EXACT_TOKENS", "true").strip().lower() in {"1", "true", "yes"}
197
+ promoted: Dict[str, Any] = {"duplicate_of": None, "promoted": False, "reason": "disabled" if not auto_promote_enabled else "none"}
198
+
199
+ if not auto_promote_enabled:
200
+ return promoted
201
+
202
+ if contradiction_candidates and not allow_with_contradictions:
203
+ promoted["reason"] = "blocked_by_contradiction_candidates"
204
+ return promoted
205
+
206
+ if not duplicate_candidates:
207
+ promoted["reason"] = "no_duplicate_candidates"
208
+ return promoted
209
+
210
+ payload = provenance.fetch_reference(reference) or {}
211
+ reference_content = str(payload.get("content") or "")
212
+ reference_signature = _token_signature(reference_content)
213
+ ranked = sorted(duplicate_candidates, key=lambda item: float(item.get("similarity") or 0.0), reverse=True)
214
+ top = ranked[0]
215
+ similarity = float(top.get("similarity") or 0.0)
216
+ target = _canonicalize_duplicate_target(str(top.get("reference") or ""))
217
+ if not target or target == reference or similarity < duplicate_threshold:
218
+ promoted["reason"] = "similarity_below_threshold"
219
+ return promoted
220
+
221
+ if len(ranked) > 1:
222
+ runner_up = float(ranked[1].get("similarity") or 0.0)
223
+ if similarity - runner_up < duplicate_margin:
224
+ promoted["reason"] = "ambiguous_duplicate_candidates"
225
+ return promoted
226
+
227
+ target_payload = provenance.fetch_reference(target) or {}
228
+ target_content = str(target_payload.get("content") or "")
229
+ if require_exact_tokens and _token_signature(target_content) != reference_signature:
230
+ promoted["reason"] = "token_signature_mismatch"
231
+ return promoted
232
+
233
+ merged = mark_memory_relationship(reference, relationship="duplicate_of", target_reference=target, status="duplicate")
234
+ promoted.update({
235
+ "duplicate_of": target,
236
+ "promoted": merged is not None,
237
+ "reason": "duplicate_high_confidence" if merged is not None else "promotion_failed",
238
+ "similarity": similarity,
239
+ })
240
+ return promoted
241
+
242
+
243
+ def _auto_apply_supersession_recommendation(
244
+ reference: str,
245
+ *,
246
+ contradiction_candidates: List[Dict[str, Any]],
247
+ supersession_recommendation: Dict[str, Any],
248
+ ) -> Dict[str, Any]:
249
+ recommendation = dict(supersession_recommendation or {})
250
+ if not recommendation:
251
+ return {"recommended": False, "auto_applied": False, "reason": "missing_recommendation", "target_reference": None, "signal": 0.0}
252
+
253
+ auto_apply = os.environ.get("OCMEMOG_GOVERNANCE_AUTOPROMOTE_SUPERSESSION", "false").strip().lower() in {"1", "true", "yes"}
254
+ allow_with_contradictions = os.environ.get("OCMEMOG_GOVERNANCE_AUTOPROMOTE_ALLOW_CONTRADICTIONS", "false").strip().lower() in {"1", "true", "yes"}
255
+ auto_apply_signal = float(os.environ.get("OCMEMOG_GOVERNANCE_SUPERSESSION_AUTOPROMOTE_SIGNAL", "0.97") or 0.97)
256
+ model_conf_threshold = float(os.environ.get("OCMEMOG_GOVERNANCE_SUPERSESSION_AUTOPROMOTE_MODEL_CONFIDENCE", "0.97") or 0.97)
257
+
258
+ recommendation.setdefault("auto_applied", False)
259
+ if not recommendation.get("recommended"):
260
+ recommendation["reason"] = recommendation.get("reason") or "not_recommended"
261
+ return recommendation
262
+
263
+ if not auto_apply:
264
+ return recommendation
265
+
266
+ if contradiction_candidates and not allow_with_contradictions:
267
+ recommendation["reason"] = "blocked_by_contradiction_candidates"
268
+ return recommendation
269
+
270
+ signal = float(recommendation.get("signal") or 0.0)
271
+ if signal < auto_apply_signal:
272
+ recommendation["reason"] = "signal_below_autopromote_threshold"
273
+ return recommendation
274
+
275
+ model_hint = recommendation.get("model_hint") if isinstance(recommendation.get("model_hint"), dict) else {}
276
+ if not model_hint or not model_hint.get("contradiction") or float(model_hint.get("confidence") or 0.0) < model_conf_threshold:
277
+ recommendation["reason"] = "model_hint_below_autopromote_threshold"
278
+ return recommendation
279
+
280
+ target = str(recommendation.get("target_reference") or "").strip()
281
+ if not target or target == reference:
282
+ recommendation["reason"] = "missing_target"
283
+ return recommendation
284
+
285
+ merged = mark_memory_relationship(reference, relationship="supersedes", target_reference=target, status="active")
286
+ recommendation["auto_applied"] = merged is not None
287
+ recommendation["reason"] = "auto_applied_supersession" if merged is not None else "auto_apply_failed"
288
+ return recommendation
289
+
290
+
291
+ def _auto_attach_governance_candidates(reference: str, *, use_model: bool = True) -> Dict[str, Any]:
292
+ payload = provenance.fetch_reference(reference) or {}
293
+ content = str(payload.get("content") or "")
294
+ if not _governance_candidates_significant(content):
295
+ return {
296
+ "duplicate_candidates": [],
297
+ "contradiction_candidates": [],
298
+ "auto_promotion": {"duplicate_of": None, "promoted": False, "reason": "insufficient_governance_signal"},
299
+ "supersession_recommendation": {
300
+ "recommended": False,
301
+ "auto_applied": False,
302
+ "reason": "insufficient_governance_signal",
303
+ },
304
+ }
305
+
306
+ duplicate_candidates = find_duplicate_candidates(reference, limit=5, min_similarity=0.72)
307
+ contradiction_candidates = find_contradiction_candidates(
308
+ reference,
309
+ limit=5,
310
+ min_signal=0.55,
311
+ use_model=use_model,
312
+ max_model_hints=_auto_attach_model_hint_budget() if use_model else 0,
313
+ )
314
+ supersession_recommendation = _recommend_supersession_from_contradictions(
315
+ reference,
316
+ contradiction_candidates=contradiction_candidates,
317
+ )
318
+ auto_promotion = _auto_promote_duplicate_candidate(
319
+ reference,
320
+ duplicate_candidates=duplicate_candidates,
321
+ contradiction_candidates=contradiction_candidates,
322
+ )
323
+ supersession_recommendation = _auto_apply_supersession_recommendation(
324
+ reference,
325
+ contradiction_candidates=contradiction_candidates,
326
+ supersession_recommendation=supersession_recommendation,
327
+ )
328
+ payload = {
329
+ "duplicate_candidates": [item.get("reference") for item in duplicate_candidates if item.get("reference")],
330
+ "contradiction_candidates": [item.get("reference") for item in contradiction_candidates if item.get("reference")],
331
+ "auto_promotion": auto_promotion,
332
+ "supersession_recommendation": supersession_recommendation,
333
+ }
334
+ provenance.update_memory_metadata(reference, payload)
335
+ emit_event(
336
+ store.state_store.report_log_path(),
337
+ "store_memory_governance_candidates",
338
+ status="ok",
339
+ reference=reference,
340
+ duplicates=len(payload["duplicate_candidates"]),
341
+ contradictions=len(payload["contradiction_candidates"]),
342
+ auto_promoted=bool(auto_promotion.get("promoted")),
343
+ auto_promotion_reason=str(auto_promotion.get("reason") or "none"),
344
+ supersession_recommended=bool(supersession_recommendation.get("recommended")),
345
+ supersession_auto_applied=bool(supersession_recommendation.get("auto_applied")),
346
+ supersession_reason=str(supersession_recommendation.get("reason") or "none"),
347
+ )
348
+ return payload
349
+
350
+
351
+ def store_memory(
352
+ memory_type: str,
353
+ content: str,
354
+ *,
355
+ source: str | None = None,
356
+ metadata: Dict[str, Any] | None = None,
357
+ timestamp: str | None = None,
358
+ post_process: bool = True,
359
+ skip_embedding_provider: bool = False,
360
+ ) -> int:
361
+ content = _sanitize(content)
362
+ table = memory_type.strip().lower() if memory_type else "knowledge"
363
+ allowed = set(store.MEMORY_TABLES)
364
+ if table not in allowed:
365
+ table = "knowledge"
366
+ normalized_metadata = provenance.normalize_metadata(metadata, source=source)
367
+
368
+ def _write() -> int:
369
+ conn = store.connect()
370
+ try:
371
+ if timestamp:
372
+ cur = conn.execute(
373
+ f"INSERT INTO {table} (source, confidence, metadata_json, content, schema_version, timestamp) VALUES (?, ?, ?, ?, ?, ?)",
374
+ (source, 1.0, json.dumps(normalized_metadata, ensure_ascii=False), content, store.SCHEMA_VERSION, timestamp),
375
+ )
376
+ else:
377
+ cur = conn.execute(
378
+ f"INSERT INTO {table} (source, confidence, metadata_json, content, schema_version) VALUES (?, ?, ?, ?, ?)",
379
+ (source, 1.0, json.dumps(normalized_metadata, ensure_ascii=False), content, store.SCHEMA_VERSION),
380
+ )
381
+ conn.commit()
382
+ return int(cur.lastrowid)
383
+ finally:
384
+ conn.close()
385
+
386
+ last_row_id = store.submit_write(_write, timeout=30.0)
387
+ reference = f"{table}:{last_row_id}"
388
+ provenance.apply_links(reference, normalized_metadata)
389
+ if post_process:
390
+ try:
391
+ from . import vector_index
392
+
393
+ vector_index.insert_memory(last_row_id, content, 1.0, source_type=table, skip_provider=skip_embedding_provider)
394
+ except Exception as exc:
395
+ emit_event(store.state_store.report_log_path(), "store_memory_index_failed", status="error", error=str(exc), memory_type=table)
396
+ try:
397
+ _auto_attach_governance_candidates(reference, use_model=_auto_attach_model_hints_enabled())
398
+ except Exception as exc:
399
+ emit_event(
400
+ store.state_store.report_log_path(),
401
+ "store_memory_governance_failed",
402
+ status="error",
403
+ error=str(exc),
404
+ reference=reference,
405
+ )
406
+ _emit("store_memory")
407
+ return last_row_id
408
+
409
+
410
+ def postprocess_stored_memory(
411
+ reference: str,
412
+ *,
413
+ run_embedding: bool = True,
414
+ run_governance: bool = True,
415
+ skip_embedding_provider: bool = False,
416
+ ) -> Dict[str, Any]:
417
+ parsed = _parse_memory_reference(reference)
418
+ if not parsed:
419
+ emit_event(
420
+ store.state_store.report_log_path(),
421
+ "store_memory_postprocess_skipped",
422
+ status="error",
423
+ reason="invalid_reference",
424
+ reference=reference,
425
+ )
426
+ return {"ok": False, "reference": reference, "error": "invalid_reference"}
427
+ table, identifier = parsed
428
+ if table not in set(store.MEMORY_TABLES):
429
+ emit_event(
430
+ store.state_store.report_log_path(),
431
+ "store_memory_postprocess_skipped",
432
+ status="error",
433
+ reason="invalid_table",
434
+ reference=reference,
435
+ table=table,
436
+ )
437
+ return {"ok": False, "reference": reference, "error": "invalid_table"}
438
+
439
+ row = provenance.fetch_reference(reference)
440
+ if not row:
441
+ emit_event(
442
+ store.state_store.report_log_path(),
443
+ "store_memory_postprocess_skipped",
444
+ status="error",
445
+ reason="missing_memory",
446
+ reference=reference,
447
+ )
448
+ return {"ok": False, "reference": reference, "error": "missing_memory"}
449
+
450
+ content = str(row.get("content") or "")
451
+ memory_id = int(identifier)
452
+ if run_embedding:
453
+ try:
454
+ from . import vector_index
455
+
456
+ vector_index.insert_memory(
457
+ memory_id,
458
+ content,
459
+ float(row.get("confidence") or 1.0),
460
+ source_type=table,
461
+ skip_provider=skip_embedding_provider,
462
+ )
463
+ except Exception as exc:
464
+ emit_event(
465
+ store.state_store.report_log_path(),
466
+ "store_memory_index_failed",
467
+ status="error",
468
+ error=str(exc),
469
+ reference=reference,
470
+ )
471
+ return {"ok": False, "reference": reference, "error": str(exc)}
472
+
473
+ if run_governance:
474
+ try:
475
+ _auto_attach_governance_candidates(reference, use_model=_auto_attach_model_hints_enabled())
476
+ except Exception as exc:
477
+ emit_event(
478
+ store.state_store.report_log_path(),
479
+ "store_memory_postprocess_governance_failed",
480
+ status="error",
481
+ error=str(exc),
482
+ reference=reference,
483
+ )
484
+ return {"ok": False, "reference": reference, "error": str(exc)}
485
+
486
+ emit_event(
487
+ store.state_store.report_log_path(),
488
+ "store_memory_postprocess_complete",
489
+ status="ok",
490
+ reference=reference,
491
+ )
492
+ return {"ok": True, "reference": reference}
493
+
494
+
495
+ def record_reinforcement(task_id: str, outcome: str, note: str, *, source_module: str | None = None) -> None:
496
+ outcome = _sanitize(outcome)
497
+ note = _sanitize(note)
498
+ memory_reference = f"reinforcement:{task_id or 'unknown'}:{source_module or 'unspecified'}"
499
+ def _write() -> None:
500
+ conn = store.connect()
501
+ try:
502
+ conn.execute(
503
+ "INSERT INTO experiences (task_id, outcome, reward_score, confidence, memory_reference, experience_type, source_module, schema_version) "
504
+ "VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
505
+ (task_id, outcome, None, 1.0, memory_reference, "reinforcement", source_module, store.SCHEMA_VERSION),
506
+ )
507
+ conn.execute(
508
+ "INSERT INTO memory_events (event_type, source, details_json, schema_version) VALUES (?, ?, ?, ?)",
509
+ ("reinforcement_note", source_module, json.dumps({"task_id": task_id, "note": note, "memory_reference": memory_reference}), store.SCHEMA_VERSION),
510
+ )
511
+ conn.commit()
512
+ finally:
513
+ conn.close()
514
+
515
+ store.submit_write(_write, timeout=30.0)
516
+ _emit("record_reinforcement")
517
+
518
+
519
+ def _tokenize(text: str) -> List[str]:
520
+ return [token for token in "".join(ch.lower() if ch.isalnum() else " " for ch in (text or "")).split() if token]
521
+
522
+
523
+ def _similarity(left: str, right: str) -> float:
524
+ left_tokens = set(_tokenize(left))
525
+ right_tokens = set(_tokenize(right))
526
+ if not left_tokens or not right_tokens:
527
+ return 0.0
528
+ overlap = len(left_tokens & right_tokens)
529
+ union = len(left_tokens | right_tokens)
530
+ return round(overlap / max(1, union), 3)
531
+
532
+
533
+ def _extract_literals(text: str) -> List[str]:
534
+ import re
535
+ patterns = [
536
+ r"\b\d{2,6}\b",
537
+ r"\b\d{1,3}(?:\.\d{1,3}){3}\b",
538
+ r"\b\+?1?\d{10,11}\b",
539
+ r"\b[a-zA-Z][a-zA-Z0-9_.-]*:[0-9]{2,5}\b",
540
+ ]
541
+ hits: List[str] = []
542
+ for pattern in patterns:
543
+ for match in re.findall(pattern, text or ""):
544
+ value = str(match).strip()
545
+ if value and value not in hits:
546
+ hits.append(value)
547
+ return hits
548
+
549
+
550
+ def _contradiction_signal(left: str, right: str) -> float:
551
+ left_tokens = set(_tokenize(left))
552
+ right_tokens = set(_tokenize(right))
553
+ literals_left = set(_extract_literals(left))
554
+ literals_right = set(_extract_literals(right))
555
+ shared_context = len((left_tokens & right_tokens) - literals_left - literals_right)
556
+ different_literals = literals_left.symmetric_difference(literals_right)
557
+ lexical_similarity = _similarity(left, right)
558
+ if not literals_left and not literals_right:
559
+ return 0.0
560
+ if literals_left == literals_right:
561
+ return 0.0
562
+ if shared_context < 2 and lexical_similarity < 0.45:
563
+ return 0.0
564
+ base = min(1.0, 0.35 * lexical_similarity + 0.12 * shared_context + 0.3 * min(2, len(different_literals)))
565
+ return round(base, 3)
566
+
567
+
568
+ def _model_contradiction_hint(left: str, right: str) -> Optional[Dict[str, Any]]:
569
+ prompt = (
570
+ "You are checking whether two short memory statements likely contradict each other.\n"
571
+ "Return strict JSON with keys: contradiction (true/false), confidence (0..1), rationale (string).\n"
572
+ f"Statement A: {left}\n"
573
+ f"Statement B: {right}\n"
574
+ )
575
+ result = inference.infer(
576
+ prompt,
577
+ provider_name=os.environ.get("OCMEMOG_PONDER_MODEL", "local-openai:qwen2.5-7b-instruct"),
578
+ )
579
+ if result.get("status") != "ok":
580
+ return None
581
+ try:
582
+ parsed = json.loads(result.get("output") or "{}")
583
+ except Exception:
584
+ return None
585
+ if not isinstance(parsed, dict):
586
+ return None
587
+ return {
588
+ "contradiction": bool(parsed.get("contradiction")),
589
+ "confidence": float(parsed.get("confidence") or 0.0),
590
+ "rationale": str(parsed.get("rationale") or "").strip(),
591
+ }
592
+
593
+
594
+ def find_duplicate_candidates(
595
+ reference: str,
596
+ *,
597
+ limit: int = 5,
598
+ min_similarity: float = 0.72,
599
+ ) -> List[Dict[str, Any]]:
600
+ payload = provenance.fetch_reference(reference) or {}
601
+ table = str(payload.get("table") or payload.get("type") or "")
602
+ content = str(payload.get("content") or "")
603
+ if table not in set(store.MEMORY_TABLES):
604
+ return []
605
+ if not _governance_candidates_significant(content):
606
+ return []
607
+ row_id = payload.get("id")
608
+ content_tokens = set(_tokenize(content))
609
+ conn = store.connect()
610
+ try:
611
+ rows = conn.execute(
612
+ f"SELECT id, content, metadata_json, timestamp FROM {table} WHERE id != ? ORDER BY id DESC LIMIT ?",
613
+ (int(row_id), max(limit * 10, 50)),
614
+ ).fetchall()
615
+ finally:
616
+ conn.close()
617
+
618
+ candidates: List[Dict[str, Any]] = []
619
+ for row in rows:
620
+ candidate_ref = f"{table}:{row['id'] if isinstance(row, dict) else row[0]}"
621
+ candidate_content = row["content"] if isinstance(row, dict) else row[1]
622
+ candidate_tokens = set(_tokenize(candidate_content))
623
+ if not candidate_tokens:
624
+ continue
625
+ overlap_tokens = content_tokens & candidate_tokens
626
+ if not overlap_tokens:
627
+ continue
628
+ overlap = len(overlap_tokens)
629
+ union = len(content_tokens | candidate_tokens)
630
+ if union <= 0:
631
+ continue
632
+ score = round(overlap / union, 3)
633
+ if score < min_similarity:
634
+ continue
635
+ meta_raw = row["metadata_json"] if isinstance(row, dict) else row[2]
636
+ try:
637
+ metadata = json.loads(meta_raw or "{}")
638
+ except Exception:
639
+ metadata = {}
640
+ preview = provenance.preview_from_metadata(metadata)
641
+ candidates.append({
642
+ "reference": candidate_ref,
643
+ "content": candidate_content,
644
+ "similarity": score,
645
+ "timestamp": row["timestamp"] if isinstance(row, dict) else row[3],
646
+ "provenance_preview": preview,
647
+ })
648
+
649
+ candidates.sort(key=lambda item: item["similarity"], reverse=True)
650
+ top = candidates[:limit]
651
+ if top:
652
+ provenance.force_update_memory_metadata(reference, {"duplicate_candidates": [item["reference"] for item in top]})
653
+ _emit("find_duplicate_candidates")
654
+ return top
655
+
656
+
657
+ def find_contradiction_candidates(
658
+ reference: str,
659
+ *,
660
+ limit: int = 5,
661
+ min_signal: float = 0.55,
662
+ use_model: bool = True,
663
+ max_model_hints: int | None = None,
664
+ ) -> List[Dict[str, Any]]:
665
+ payload = provenance.fetch_reference(reference) or {}
666
+ table = str(payload.get("table") or payload.get("type") or "")
667
+ content = str(payload.get("content") or "")
668
+ if table not in set(store.MEMORY_TABLES):
669
+ return []
670
+ if not _governance_candidates_significant(content):
671
+ return []
672
+ row_id = payload.get("id")
673
+ content_tokens = set(_tokenize(content))
674
+ content_literals = set(_extract_literals(content))
675
+ conn = store.connect()
676
+ try:
677
+ rows = conn.execute(
678
+ f"SELECT id, content, metadata_json, timestamp FROM {table} WHERE id != ? ORDER BY id DESC LIMIT ?",
679
+ (int(row_id), max(limit * 12, 60)),
680
+ ).fetchall()
681
+ finally:
682
+ conn.close()
683
+
684
+ candidates: List[Dict[str, Any]] = []
685
+ for row in rows:
686
+ candidate_ref = f"{table}:{row['id'] if isinstance(row, dict) else row[0]}"
687
+ candidate_content = row["content"] if isinstance(row, dict) else row[1]
688
+ candidate_tokens = set(_tokenize(candidate_content))
689
+ if not candidate_tokens:
690
+ continue
691
+ candidate_literal_values = _extract_literals(candidate_content)
692
+ candidate_literals = set(candidate_literal_values)
693
+ overlap_tokens = content_tokens & candidate_tokens
694
+ if not overlap_tokens:
695
+ continue
696
+ shared_context = len(overlap_tokens - content_literals - candidate_literals)
697
+ union = len(content_tokens | candidate_tokens)
698
+ lexical_similarity = round(len(overlap_tokens) / max(1, union), 3)
699
+ if not content_literals and not candidate_literals:
700
+ signal = 0.0
701
+ elif content_literals == candidate_literals:
702
+ signal = 0.0
703
+ elif shared_context < 2 and lexical_similarity < 0.45:
704
+ signal = 0.0
705
+ else:
706
+ signal = round(
707
+ min(1.0, 0.35 * lexical_similarity + 0.12 * shared_context + 0.3 * min(2, len(content_literals.symmetric_difference(candidate_literals)))),
708
+ 3,
709
+ )
710
+ if signal < min_signal:
711
+ continue
712
+ meta_raw = row["metadata_json"] if isinstance(row, dict) else row[2]
713
+ try:
714
+ metadata = json.loads(meta_raw or "{}")
715
+ except Exception:
716
+ metadata = {}
717
+ preview = provenance.preview_from_metadata(metadata)
718
+ item: Dict[str, Any] = {
719
+ "reference": candidate_ref,
720
+ "content": candidate_content,
721
+ "signal": signal,
722
+ "timestamp": row["timestamp"] if isinstance(row, dict) else row[3],
723
+ "provenance_preview": preview,
724
+ "literals": candidate_literal_values,
725
+ }
726
+ candidates.append(item)
727
+
728
+ candidates.sort(key=lambda item: item["signal"], reverse=True)
729
+
730
+ if use_model:
731
+ hint_budget = len(candidates) if max_model_hints is None else max(0, int(max_model_hints))
732
+ for item in candidates[:hint_budget]:
733
+ candidate_content = item.get("content")
734
+ hint = _model_contradiction_hint(content, str(candidate_content))
735
+ if not hint:
736
+ continue
737
+ item["model_hint"] = hint
738
+ if not hint.get("contradiction") and float(item.get("signal") or 0.0) < 0.8:
739
+ continue
740
+ item["signal"] = round(max(float(item.get("signal") or 0.0), float(hint.get("confidence") or 0.0)), 3)
741
+
742
+ candidates = [
743
+ item
744
+ for item in candidates
745
+ if not (item.get("model_hint") is not None and not item["model_hint"].get("contradiction") and float(item.get("signal") or 0.0) < 0.8)
746
+ ]
747
+ candidates.sort(key=lambda item: item["signal"], reverse=True)
748
+
749
+ top = candidates[:limit]
750
+ if top:
751
+ provenance.force_update_memory_metadata(reference, {"contradicts": [item["reference"] for item in top], "contradiction_status": "candidate", "contradiction_candidates": [item["reference"] for item in top]})
752
+ _emit("find_contradiction_candidates")
753
+ return top
754
+
755
+
756
+ def mark_memory_relationship(
757
+ reference: str,
758
+ *,
759
+ relationship: str,
760
+ target_reference: str,
761
+ status: str | None = None,
762
+ ) -> Dict[str, Any] | None:
763
+ relationship = (relationship or "").strip().lower()
764
+ updates: Dict[str, Any] = {}
765
+ if relationship == "supersedes":
766
+ updates = {
767
+ "supersedes": target_reference,
768
+ "memory_status": status or "active",
769
+ "canonical_reference": reference,
770
+ }
771
+ provenance.force_update_memory_metadata(target_reference, {
772
+ "superseded_by": reference,
773
+ "memory_status": "superseded",
774
+ "canonical_reference": reference,
775
+ })
776
+ elif relationship == "duplicate_of":
777
+ updates = {
778
+ "duplicate_of": target_reference,
779
+ "memory_status": status or "duplicate",
780
+ "canonical_reference": target_reference,
781
+ }
782
+ elif relationship == "contradicts":
783
+ updates = {
784
+ "contradicts": [target_reference],
785
+ "contradiction_status": status or "contested",
786
+ "memory_status": "contested",
787
+ }
788
+ provenance.force_update_memory_metadata(target_reference, {
789
+ "contradicts": [reference],
790
+ "contradiction_status": status or "contested",
791
+ "memory_status": "contested",
792
+ })
793
+ else:
794
+ return None
795
+ merged = provenance.force_update_memory_metadata(reference, updates)
796
+ _emit(f"mark_memory_relationship_{relationship}")
797
+ return merged
798
+
799
+
800
+ def list_governance_candidates(
801
+ *,
802
+ categories: Optional[List[str]] = None,
803
+ limit: int = 50,
804
+ ) -> List[Dict[str, Any]]:
805
+ allowed = set(store.MEMORY_TABLES)
806
+ tables = [table for table in (categories or list(allowed)) if table in allowed]
807
+ conn = store.connect()
808
+ try:
809
+ items: List[Dict[str, Any]] = []
810
+ for table in tables:
811
+ rows = conn.execute(
812
+ f"SELECT id, timestamp, content, metadata_json FROM {table} ORDER BY id DESC LIMIT ?",
813
+ (max(limit, 20),),
814
+ ).fetchall()
815
+ for row in rows:
816
+ metadata = json.loads((row["metadata_json"] if isinstance(row, dict) else row[3]) or "{}")
817
+ prov = metadata.get("provenance") if isinstance(metadata.get("provenance"), dict) else {}
818
+ duplicate_candidates = prov.get("duplicate_candidates") or []
819
+ contradiction_candidates = prov.get("contradiction_candidates") or []
820
+ supersession_recommendation = prov.get("supersession_recommendation") or {}
821
+ if not duplicate_candidates and not contradiction_candidates and not supersession_recommendation:
822
+ continue
823
+ items.append({
824
+ "reference": f"{table}:{row['id'] if isinstance(row, dict) else row[0]}",
825
+ "bucket": table,
826
+ "timestamp": row["timestamp"] if isinstance(row, dict) else row[1],
827
+ "content": row["content"] if isinstance(row, dict) else row[2],
828
+ "memory_status": prov.get("memory_status") or metadata.get("memory_status") or "active",
829
+ "duplicate_candidates": duplicate_candidates,
830
+ "contradiction_candidates": contradiction_candidates,
831
+ "supersession_recommendation": supersession_recommendation,
832
+ })
833
+ items.sort(key=lambda item: str(item.get("timestamp") or ""), reverse=True)
834
+ return items[:limit]
835
+ finally:
836
+ conn.close()
837
+
838
+
839
+ def _remove_from_list(values: Any, target: str) -> List[str]:
840
+ return [str(item) for item in (values or []) if str(item) and str(item) != target]
841
+
842
+
843
+ def _review_item_context(reference: str, *, depth: int = 1) -> Dict[str, Any]:
844
+ payload = provenance.hydrate_reference(reference, depth=depth) or {"reference": reference}
845
+ metadata = payload.get("metadata") if isinstance(payload.get("metadata"), dict) else {}
846
+ prov = metadata.get("provenance") if isinstance(metadata.get("provenance"), dict) else {}
847
+ return {
848
+ "reference": reference,
849
+ "bucket": payload.get("table"),
850
+ "id": payload.get("id"),
851
+ "timestamp": payload.get("timestamp"),
852
+ "content": payload.get("content"),
853
+ "memory_status": prov.get("memory_status") or metadata.get("memory_status") or "active",
854
+ "provenance_preview": payload.get("provenance_preview") or provenance.preview_from_metadata(metadata),
855
+ "metadata": metadata,
856
+ "links": payload.get("links") or [],
857
+ "backlinks": payload.get("backlinks") or [],
858
+ }
859
+
860
+
861
+ def _review_item_summary(kind: str, reference: str, target_reference: str) -> str:
862
+ if kind == "duplicate_candidate":
863
+ return f"{reference} may duplicate {target_reference}"
864
+ if kind == "contradiction_candidate":
865
+ return f"{reference} may contradict {target_reference}"
866
+ if kind == "supersession_recommendation":
867
+ return f"{reference} may supersede {target_reference}"
868
+ return f"{reference} requires review against {target_reference}"
869
+
870
+
871
+ def _review_actions(kind: str, relationship: str) -> List[Dict[str, Any]]:
872
+ meta = _REVIEW_KIND_METADATA.get(kind, {})
873
+ return [
874
+ {
875
+ "decision": "approve",
876
+ "approved": True,
877
+ "relationship": relationship,
878
+ "label": meta.get("approve_label") or "Approve",
879
+ },
880
+ {
881
+ "decision": "reject",
882
+ "approved": False,
883
+ "relationship": relationship,
884
+ "label": meta.get("reject_label") or "Reject",
885
+ },
886
+ ]
887
+
888
+
889
+ def _relationship_for_review(kind: str | None = None, relationship: str | None = None) -> str:
890
+ resolved = (relationship or "").strip().lower()
891
+ if resolved:
892
+ return resolved
893
+ kind_key = (kind or "").strip().lower()
894
+ return _REVIEW_KIND_METADATA.get(kind_key, {}).get("relationship", "")
895
+
896
+
897
+ def list_governance_review_items(
898
+ *,
899
+ categories: Optional[List[str]] = None,
900
+ limit: int = 100,
901
+ context_depth: int = 1,
902
+ ) -> List[Dict[str, Any]]:
903
+ items = governance_queue(categories=categories, limit=limit)
904
+ review_items: List[Dict[str, Any]] = []
905
+ for item in items:
906
+ kind = str(item.get("kind") or "")
907
+ relationship = _relationship_for_review(kind=kind)
908
+ reference = str(item.get("reference") or "")
909
+ target_reference = str(item.get("target_reference") or "")
910
+ if not reference or not target_reference or not relationship:
911
+ continue
912
+ review_items.append({
913
+ "review_id": f"{kind}:{reference}->{target_reference}",
914
+ "kind": kind,
915
+ "kind_label": _REVIEW_KIND_METADATA.get(kind, {}).get("label") or kind.replace("_", " "),
916
+ "relationship": relationship,
917
+ "priority": int(item.get("priority") or 0),
918
+ "timestamp": item.get("timestamp"),
919
+ "bucket": item.get("bucket"),
920
+ "signal": float(item.get("signal") or 0.0),
921
+ "reason": item.get("reason"),
922
+ "reference": reference,
923
+ "target_reference": target_reference,
924
+ "summary": _review_item_summary(kind, reference, target_reference),
925
+ "actions": _review_actions(kind, relationship),
926
+ "source": _review_item_context(reference, depth=context_depth),
927
+ "target": _review_item_context(target_reference, depth=context_depth),
928
+ })
929
+ return review_items
930
+
931
+
932
+ def apply_governance_decision(
933
+ reference: str,
934
+ *,
935
+ relationship: str,
936
+ target_reference: str,
937
+ approved: bool = True,
938
+ ) -> Dict[str, Any] | None:
939
+ relationship = (relationship or "").strip().lower()
940
+ if approved:
941
+ merged = mark_memory_relationship(reference, relationship=relationship, target_reference=target_reference)
942
+ if merged is None:
943
+ return None
944
+ updates: Dict[str, Any] = {}
945
+ if relationship == "duplicate_of":
946
+ current = provenance.fetch_reference(reference) or {}
947
+ metadata = current.get("metadata") or {}
948
+ prov = metadata.get("provenance") if isinstance(metadata.get("provenance"), dict) else {}
949
+ updates["duplicate_candidates"] = _remove_from_list(prov.get("duplicate_candidates"), target_reference)
950
+ elif relationship == "contradicts":
951
+ current = provenance.fetch_reference(reference) or {}
952
+ metadata = current.get("metadata") or {}
953
+ prov = metadata.get("provenance") if isinstance(metadata.get("provenance"), dict) else {}
954
+ updates["contradiction_candidates"] = _remove_from_list(prov.get("contradiction_candidates"), target_reference)
955
+ elif relationship == "supersedes":
956
+ updates["supersession_recommendation"] = None
957
+ if updates:
958
+ merged = provenance.force_update_memory_metadata(reference, updates) or merged
959
+ _emit(f"apply_governance_decision_{relationship}_approved")
960
+ return merged
961
+
962
+ current = provenance.fetch_reference(reference) or {}
963
+ metadata = current.get("metadata") or {}
964
+ prov = metadata.get("provenance") if isinstance(metadata.get("provenance"), dict) else {}
965
+ updates: Dict[str, Any] = {}
966
+ if relationship == "duplicate_of":
967
+ updates["duplicate_candidates"] = _remove_from_list(prov.get("duplicate_candidates"), target_reference)
968
+ elif relationship == "contradicts":
969
+ updates["contradiction_candidates"] = _remove_from_list(prov.get("contradiction_candidates"), target_reference)
970
+ elif relationship == "supersedes":
971
+ recommendation = prov.get("supersession_recommendation") if isinstance(prov.get("supersession_recommendation"), dict) else {}
972
+ if not recommendation or str(recommendation.get("target_reference") or "") == target_reference:
973
+ updates["supersession_recommendation"] = None
974
+ updates["supersedes"] = None
975
+ else:
976
+ return None
977
+ merged = provenance.force_update_memory_metadata(reference, updates)
978
+ _emit(f"apply_governance_decision_{relationship}_{'approved' if approved else 'rejected'}")
979
+ return merged
980
+
981
+
982
+ def apply_governance_review_decision(
983
+ reference: str,
984
+ *,
985
+ target_reference: str,
986
+ approved: bool = True,
987
+ kind: str | None = None,
988
+ relationship: str | None = None,
989
+ context_depth: int = 1,
990
+ ) -> Dict[str, Any] | None:
991
+ resolved_relationship = _relationship_for_review(kind=kind, relationship=relationship)
992
+ if not resolved_relationship:
993
+ return None
994
+ result = apply_governance_decision(
995
+ reference,
996
+ relationship=resolved_relationship,
997
+ target_reference=target_reference,
998
+ approved=approved,
999
+ )
1000
+ if result is None:
1001
+ return None
1002
+ resolved_kind = (kind or "").strip().lower()
1003
+ if not resolved_kind:
1004
+ for candidate_kind, meta in _REVIEW_KIND_METADATA.items():
1005
+ if meta.get("relationship") == resolved_relationship:
1006
+ resolved_kind = candidate_kind
1007
+ break
1008
+ return {
1009
+ "reference": reference,
1010
+ "target_reference": target_reference,
1011
+ "approved": bool(approved),
1012
+ "kind": resolved_kind or None,
1013
+ "relationship": resolved_relationship,
1014
+ "result": result,
1015
+ "source": _review_item_context(reference, depth=context_depth),
1016
+ "target": _review_item_context(target_reference, depth=context_depth),
1017
+ }
1018
+
1019
+
1020
+ def rollback_governance_decision(
1021
+ reference: str,
1022
+ *,
1023
+ relationship: str,
1024
+ target_reference: str,
1025
+ ) -> Dict[str, Any] | None:
1026
+ relationship = (relationship or "").strip().lower()
1027
+ if relationship not in {"duplicate_of", "supersedes", "contradicts"}:
1028
+ return None
1029
+
1030
+ reference_payload = provenance.fetch_reference(reference) or {}
1031
+ ref_meta = reference_payload.get("metadata") or {}
1032
+ ref_prov = ref_meta.get("provenance") if isinstance(ref_meta.get("provenance"), dict) else {}
1033
+
1034
+ if relationship == "duplicate_of":
1035
+ updates = {
1036
+ "duplicate_of": None,
1037
+ "memory_status": "active",
1038
+ "canonical_reference": None,
1039
+ }
1040
+ merged = provenance.force_update_memory_metadata(reference, updates)
1041
+ _emit("rollback_governance_duplicate_of")
1042
+ return merged
1043
+
1044
+ if relationship == "supersedes":
1045
+ provenance.force_update_memory_metadata(reference, {"supersedes": None})
1046
+ target_updates = {
1047
+ "superseded_by": None,
1048
+ "memory_status": "active",
1049
+ }
1050
+ merged = provenance.force_update_memory_metadata(target_reference, target_updates)
1051
+ _emit("rollback_governance_supersedes")
1052
+ return merged
1053
+
1054
+ if relationship == "contradicts":
1055
+ new_list = _remove_from_list(ref_prov.get("contradicts"), target_reference)
1056
+ merged = provenance.force_update_memory_metadata(reference, {
1057
+ "contradicts": new_list,
1058
+ "contradiction_status": None,
1059
+ "memory_status": "active",
1060
+ })
1061
+ target_payload = provenance.fetch_reference(target_reference) or {}
1062
+ target_meta = target_payload.get("metadata") or {}
1063
+ target_prov = target_meta.get("provenance") if isinstance(target_meta.get("provenance"), dict) else {}
1064
+ target_updates = {
1065
+ "contradicts": _remove_from_list(target_prov.get("contradicts"), reference),
1066
+ "contradiction_status": None,
1067
+ "memory_status": "active",
1068
+ }
1069
+ provenance.force_update_memory_metadata(target_reference, target_updates)
1070
+ _emit("rollback_governance_contradicts")
1071
+ return merged
1072
+
1073
+ return None
1074
+
1075
+
1076
+ def governance_queue(*, categories: Optional[List[str]] = None, limit: int = 100) -> List[Dict[str, Any]]:
1077
+ allowed = set(store.MEMORY_TABLES)
1078
+ tables = [table for table in (categories or list(allowed)) if table in allowed]
1079
+ conn = store.connect()
1080
+ try:
1081
+ items: List[Dict[str, Any]] = []
1082
+ for table in tables:
1083
+ rows = conn.execute(
1084
+ f"SELECT id, timestamp, content, metadata_json FROM {table} ORDER BY id DESC LIMIT 3000"
1085
+ ).fetchall()
1086
+ for row in rows:
1087
+ reference = f"{table}:{row['id'] if isinstance(row, dict) else row[0]}"
1088
+ timestamp = row["timestamp"] if isinstance(row, dict) else row[1]
1089
+ content = row["content"] if isinstance(row, dict) else row[2]
1090
+ try:
1091
+ metadata = json.loads((row["metadata_json"] if isinstance(row, dict) else row[3]) or "{}")
1092
+ except Exception:
1093
+ metadata = {}
1094
+ prov = metadata.get("provenance") if isinstance(metadata.get("provenance"), dict) else {}
1095
+ duplicate_candidates = [str(x) for x in (prov.get("duplicate_candidates") or []) if x]
1096
+ contradiction_candidates = [str(x) for x in (prov.get("contradiction_candidates") or []) if x]
1097
+ supersession_recommendation = prov.get("supersession_recommendation") or {}
1098
+
1099
+ for target in duplicate_candidates:
1100
+ items.append({
1101
+ "reference": reference,
1102
+ "target_reference": target,
1103
+ "kind": "duplicate_candidate",
1104
+ "priority": 40,
1105
+ "timestamp": timestamp,
1106
+ "bucket": table,
1107
+ "content": content,
1108
+ })
1109
+ for target in contradiction_candidates:
1110
+ items.append({
1111
+ "reference": reference,
1112
+ "target_reference": target,
1113
+ "kind": "contradiction_candidate",
1114
+ "priority": 70,
1115
+ "timestamp": timestamp,
1116
+ "bucket": table,
1117
+ "content": content,
1118
+ })
1119
+ if isinstance(supersession_recommendation, dict) and supersession_recommendation.get("recommended"):
1120
+ items.append({
1121
+ "reference": reference,
1122
+ "target_reference": supersession_recommendation.get("target_reference"),
1123
+ "kind": "supersession_recommendation",
1124
+ "priority": 90,
1125
+ "timestamp": timestamp,
1126
+ "bucket": table,
1127
+ "signal": float(supersession_recommendation.get("signal") or 0.0),
1128
+ "reason": supersession_recommendation.get("reason"),
1129
+ "content": content,
1130
+ })
1131
+ items.sort(key=lambda item: (int(item.get("priority") or 0), str(item.get("timestamp") or "")), reverse=True)
1132
+ return items[:limit]
1133
+ finally:
1134
+ conn.close()
1135
+
1136
+
1137
+ def _resolve_auto_resolve_policy(profile: str | None = None) -> Dict[str, Any]:
1138
+ preset = (profile or os.environ.get("OCMEMOG_GOVERNANCE_AUTORESOLVE_PROFILE", "conservative") or "conservative").strip().lower()
1139
+ presets = {
1140
+ "conservative": {
1141
+ "max_apply": 5,
1142
+ "allowed_kinds": {"duplicate_candidate", "supersession_recommendation"},
1143
+ "min_supersession_signal": 0.95,
1144
+ "allowed_buckets": set(),
1145
+ },
1146
+ "balanced": {
1147
+ "max_apply": 10,
1148
+ "allowed_kinds": {"duplicate_candidate", "supersession_recommendation"},
1149
+ "min_supersession_signal": 0.9,
1150
+ "allowed_buckets": set(),
1151
+ },
1152
+ "aggressive": {
1153
+ "max_apply": 20,
1154
+ "allowed_kinds": {"duplicate_candidate", "supersession_recommendation"},
1155
+ "min_supersession_signal": 0.85,
1156
+ "allowed_buckets": set(),
1157
+ },
1158
+ }
1159
+ policy = presets.get(preset, presets["conservative"]).copy()
1160
+
1161
+ max_apply = os.environ.get("OCMEMOG_GOVERNANCE_AUTORESOLVE_MAX_APPLY")
1162
+ if max_apply:
1163
+ policy["max_apply"] = int(float(max_apply) or policy["max_apply"])
1164
+ allowed_kinds_raw = os.environ.get("OCMEMOG_GOVERNANCE_AUTORESOLVE_ALLOW_KINDS")
1165
+ if allowed_kinds_raw:
1166
+ policy["allowed_kinds"] = {k.strip() for k in allowed_kinds_raw.split(",") if k.strip()}
1167
+ min_supersession_signal = os.environ.get("OCMEMOG_GOVERNANCE_AUTORESOLVE_MIN_SUPERSESSION_SIGNAL")
1168
+ if min_supersession_signal:
1169
+ policy["min_supersession_signal"] = float(min_supersession_signal or policy["min_supersession_signal"])
1170
+ allowed_buckets_raw = os.environ.get("OCMEMOG_GOVERNANCE_AUTORESOLVE_ALLOW_BUCKETS")
1171
+ if allowed_buckets_raw is not None and allowed_buckets_raw != "":
1172
+ policy["allowed_buckets"] = {k.strip() for k in allowed_buckets_raw.split(",") if k.strip()}
1173
+
1174
+ policy["profile"] = preset
1175
+ return policy
1176
+
1177
+
1178
+ def governance_auto_resolve(
1179
+ *,
1180
+ categories: Optional[List[str]] = None,
1181
+ limit: int = 20,
1182
+ dry_run: bool = True,
1183
+ profile: str | None = None,
1184
+ ) -> Dict[str, Any]:
1185
+ queue = governance_queue(categories=categories, limit=limit)
1186
+ actions: List[Dict[str, Any]] = []
1187
+ applied = 0
1188
+ skipped = 0
1189
+
1190
+ policy = _resolve_auto_resolve_policy(profile)
1191
+ max_apply = int(policy["max_apply"])
1192
+ allowed_kinds = set(policy["allowed_kinds"])
1193
+ min_supersession_signal = float(policy["min_supersession_signal"])
1194
+ allowed_buckets = set(policy["allowed_buckets"]) if policy["allowed_buckets"] else set()
1195
+
1196
+ for item in queue:
1197
+ kind = str(item.get("kind") or "")
1198
+ bucket = str(item.get("bucket") or "")
1199
+ reference = str(item.get("reference") or "")
1200
+ target = str(item.get("target_reference") or "")
1201
+ if not reference or not target:
1202
+ skipped += 1
1203
+ actions.append({"reference": reference, "target_reference": target, "kind": kind, "applied": False, "dry_run": bool(dry_run), "reason": "missing_reference"})
1204
+ continue
1205
+
1206
+ if kind not in allowed_kinds:
1207
+ skipped += 1
1208
+ actions.append({"reference": reference, "target_reference": target, "kind": kind, "applied": False, "dry_run": bool(dry_run), "reason": "kind_not_allowed"})
1209
+ continue
1210
+
1211
+ if allowed_buckets and bucket not in allowed_buckets:
1212
+ skipped += 1
1213
+ actions.append({"reference": reference, "target_reference": target, "kind": kind, "applied": False, "dry_run": bool(dry_run), "reason": "bucket_not_allowed"})
1214
+ continue
1215
+
1216
+ relationship = None
1217
+ if kind == "supersession_recommendation":
1218
+ signal = float(item.get("signal") or 0.0)
1219
+ if signal < min_supersession_signal:
1220
+ skipped += 1
1221
+ actions.append({"reference": reference, "target_reference": target, "kind": kind, "applied": False, "dry_run": bool(dry_run), "reason": "signal_below_min"})
1222
+ continue
1223
+ relationship = "supersedes"
1224
+ elif kind == "duplicate_candidate":
1225
+ relationship = "duplicate_of"
1226
+ else:
1227
+ skipped += 1
1228
+ actions.append({"reference": reference, "target_reference": target, "kind": kind, "applied": False, "dry_run": bool(dry_run), "reason": "unsupported_kind"})
1229
+ continue
1230
+
1231
+ if not dry_run and applied >= max_apply:
1232
+ skipped += 1
1233
+ actions.append({"reference": reference, "target_reference": target, "kind": kind, "relationship": relationship, "applied": False, "dry_run": False, "reason": "max_apply_reached"})
1234
+ continue
1235
+
1236
+ if dry_run:
1237
+ actions.append({
1238
+ "reference": reference,
1239
+ "target_reference": target,
1240
+ "kind": kind,
1241
+ "relationship": relationship,
1242
+ "applied": False,
1243
+ "dry_run": True,
1244
+ "reason": "dry_run",
1245
+ })
1246
+ continue
1247
+
1248
+ result = apply_governance_decision(
1249
+ reference,
1250
+ relationship=relationship,
1251
+ target_reference=target,
1252
+ approved=True,
1253
+ )
1254
+ ok = result is not None
1255
+ if ok:
1256
+ applied += 1
1257
+ else:
1258
+ skipped += 1
1259
+ actions.append({
1260
+ "reference": reference,
1261
+ "target_reference": target,
1262
+ "kind": kind,
1263
+ "relationship": relationship,
1264
+ "applied": ok,
1265
+ "dry_run": False,
1266
+ "reason": "applied" if ok else "apply_failed",
1267
+ })
1268
+
1269
+ emit_event(
1270
+ store.state_store.report_log_path(),
1271
+ "governance_auto_resolve",
1272
+ status="ok",
1273
+ dry_run=bool(dry_run),
1274
+ considered=len(queue),
1275
+ applied=applied,
1276
+ skipped=skipped,
1277
+ max_apply=max_apply,
1278
+ allowed_kinds=",".join(sorted(allowed_kinds)),
1279
+ min_supersession_signal=min_supersession_signal,
1280
+ allowed_buckets=",".join(sorted(allowed_buckets)) if allowed_buckets else "*",
1281
+ profile=str(policy.get("profile") or "conservative"),
1282
+ )
1283
+ return {
1284
+ "considered": len(queue),
1285
+ "applied": applied,
1286
+ "skipped": skipped,
1287
+ "dry_run": bool(dry_run),
1288
+ "policy": {
1289
+ "profile": policy.get("profile") or "conservative",
1290
+ "max_apply": max_apply,
1291
+ "allowed_kinds": sorted(allowed_kinds),
1292
+ "min_supersession_signal": min_supersession_signal,
1293
+ "allowed_buckets": sorted(allowed_buckets) if allowed_buckets else ["*"],
1294
+ },
1295
+ "actions": actions,
1296
+ }
1297
+
1298
+
1299
+ def governance_audit(*, limit: int = 100, kinds: Optional[List[str]] = None) -> List[Dict[str, Any]]:
1300
+ logfile = store.state_store.report_log_path()
1301
+ if not logfile.exists():
1302
+ return []
1303
+ wanted = {k.strip() for k in (kinds or []) if k.strip()}
1304
+ if not wanted:
1305
+ wanted = {
1306
+ "store_memory_governance_candidates",
1307
+ "governance_auto_resolve",
1308
+ "mark_memory_relationship_supersedes",
1309
+ "mark_memory_relationship_duplicate_of",
1310
+ "mark_memory_relationship_contradicts",
1311
+ "apply_governance_decision_duplicate_of_approved",
1312
+ "apply_governance_decision_contradicts_approved",
1313
+ "apply_governance_decision_supersedes_approved",
1314
+ "apply_governance_decision_duplicate_of_rejected",
1315
+ "apply_governance_decision_contradicts_rejected",
1316
+ "apply_governance_decision_supersedes_rejected",
1317
+ }
1318
+ entries: List[Dict[str, Any]] = []
1319
+ try:
1320
+ with logfile.open("r", encoding="utf-8", errors="ignore") as handle:
1321
+ lines = handle.readlines()[-max(limit * 5, 200):]
1322
+ except Exception:
1323
+ return []
1324
+ for line in reversed(lines):
1325
+ line = line.strip()
1326
+ if not line:
1327
+ continue
1328
+ try:
1329
+ payload = json.loads(line)
1330
+ except Exception:
1331
+ continue
1332
+ event = str(payload.get("event") or payload.get("name") or "").strip()
1333
+ if event not in wanted:
1334
+ continue
1335
+ payload["event"] = event
1336
+ entries.append(payload)
1337
+ if len(entries) >= limit:
1338
+ break
1339
+ return list(reversed(entries))
1340
+
1341
+
1342
+ def governance_summary(*, categories: Optional[List[str]] = None) -> Dict[str, Any]:
1343
+ allowed = set(store.MEMORY_TABLES)
1344
+ tables = [table for table in (categories or list(allowed)) if table in allowed]
1345
+ conn = store.connect()
1346
+ try:
1347
+ summary: Dict[str, Any] = {
1348
+ "tables": {},
1349
+ "totals": {
1350
+ "rows": 0,
1351
+ "pending_duplicates": 0,
1352
+ "pending_contradictions": 0,
1353
+ "recommended_supersessions": 0,
1354
+ "status_active": 0,
1355
+ "status_duplicate": 0,
1356
+ "status_superseded": 0,
1357
+ "status_contested": 0,
1358
+ },
1359
+ }
1360
+ for table in tables:
1361
+ rows = conn.execute(
1362
+ f"SELECT id, metadata_json FROM {table} ORDER BY id DESC LIMIT 5000"
1363
+ ).fetchall()
1364
+ table_stats = {
1365
+ "rows": 0,
1366
+ "pending_duplicates": 0,
1367
+ "pending_contradictions": 0,
1368
+ "recommended_supersessions": 0,
1369
+ "status_active": 0,
1370
+ "status_duplicate": 0,
1371
+ "status_superseded": 0,
1372
+ "status_contested": 0,
1373
+ }
1374
+ for row in rows:
1375
+ table_stats["rows"] += 1
1376
+ try:
1377
+ metadata = json.loads((row["metadata_json"] if isinstance(row, dict) else row[1]) or "{}")
1378
+ except Exception:
1379
+ metadata = {}
1380
+ prov = metadata.get("provenance") if isinstance(metadata.get("provenance"), dict) else {}
1381
+ status = str(prov.get("memory_status") or metadata.get("memory_status") or "active").strip().lower()
1382
+ if status not in {"active", "duplicate", "superseded", "contested"}:
1383
+ status = "active"
1384
+ table_stats[f"status_{status}"] += 1
1385
+
1386
+ dup = prov.get("duplicate_candidates") or []
1387
+ contra = prov.get("contradiction_candidates") or []
1388
+ if dup:
1389
+ table_stats["pending_duplicates"] += 1
1390
+ if contra:
1391
+ table_stats["pending_contradictions"] += 1
1392
+ rec = prov.get("supersession_recommendation") or {}
1393
+ if isinstance(rec, dict) and rec.get("recommended"):
1394
+ table_stats["recommended_supersessions"] += 1
1395
+
1396
+ summary["tables"][table] = table_stats
1397
+ for key in summary["totals"].keys():
1398
+ summary["totals"][key] += int(table_stats.get(key, 0) or 0)
1399
+ return summary
1400
+ finally:
1401
+ conn.close()
1402
+
1403
+
1404
+ def get_recent_events(limit: int = 10) -> List[Dict[str, Any]]:
1405
+ conn = store.connect()
1406
+ rows = conn.execute(
1407
+ "SELECT id, timestamp, event_type, source, details_json FROM memory_events ORDER BY id DESC LIMIT ?",
1408
+ (limit,),
1409
+ ).fetchall()
1410
+ conn.close()
1411
+ return [dict(row) for row in rows]
1412
+
1413
+
1414
+ def get_recent_tasks(limit: int = 10) -> List[Dict[str, Any]]:
1415
+ conn = store.connect()
1416
+ rows = conn.execute(
1417
+ "SELECT id, timestamp, source, confidence, metadata_json, content FROM tasks ORDER BY id DESC LIMIT ?",
1418
+ (limit,),
1419
+ ).fetchall()
1420
+ conn.close()
1421
+ return [dict(row) for row in rows]
1422
+
1423
+
1424
+ def get_memories(limit: int = 10) -> List[Dict[str, Any]]:
1425
+ conn = store.connect()
1426
+ rows = conn.execute(
1427
+ "SELECT id, timestamp, source, confidence, metadata_json, content FROM knowledge ORDER BY id DESC LIMIT ?",
1428
+ (limit,),
1429
+ ).fetchall()
1430
+ conn.close()
1431
+ return [dict(row) for row in rows]