@simbimbo/memory-ocmemog 0.1.4

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 (81) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/LICENSE +21 -0
  3. package/README.md +223 -0
  4. package/brain/__init__.py +1 -0
  5. package/brain/runtime/__init__.py +13 -0
  6. package/brain/runtime/config.py +21 -0
  7. package/brain/runtime/inference.py +83 -0
  8. package/brain/runtime/instrumentation.py +17 -0
  9. package/brain/runtime/memory/__init__.py +13 -0
  10. package/brain/runtime/memory/api.py +152 -0
  11. package/brain/runtime/memory/artifacts.py +33 -0
  12. package/brain/runtime/memory/candidate.py +89 -0
  13. package/brain/runtime/memory/context_builder.py +87 -0
  14. package/brain/runtime/memory/conversation_state.py +1825 -0
  15. package/brain/runtime/memory/distill.py +198 -0
  16. package/brain/runtime/memory/embedding_engine.py +94 -0
  17. package/brain/runtime/memory/freshness.py +91 -0
  18. package/brain/runtime/memory/health.py +42 -0
  19. package/brain/runtime/memory/integrity.py +170 -0
  20. package/brain/runtime/memory/interaction_memory.py +57 -0
  21. package/brain/runtime/memory/memory_consolidation.py +60 -0
  22. package/brain/runtime/memory/memory_gate.py +38 -0
  23. package/brain/runtime/memory/memory_graph.py +54 -0
  24. package/brain/runtime/memory/memory_links.py +109 -0
  25. package/brain/runtime/memory/memory_salience.py +235 -0
  26. package/brain/runtime/memory/memory_synthesis.py +33 -0
  27. package/brain/runtime/memory/memory_taxonomy.py +35 -0
  28. package/brain/runtime/memory/person_identity.py +83 -0
  29. package/brain/runtime/memory/person_memory.py +138 -0
  30. package/brain/runtime/memory/pondering_engine.py +577 -0
  31. package/brain/runtime/memory/promote.py +237 -0
  32. package/brain/runtime/memory/provenance.py +356 -0
  33. package/brain/runtime/memory/reinforcement.py +73 -0
  34. package/brain/runtime/memory/retrieval.py +153 -0
  35. package/brain/runtime/memory/semantic_search.py +66 -0
  36. package/brain/runtime/memory/sentiment_memory.py +67 -0
  37. package/brain/runtime/memory/store.py +400 -0
  38. package/brain/runtime/memory/tool_catalog.py +68 -0
  39. package/brain/runtime/memory/unresolved_state.py +93 -0
  40. package/brain/runtime/memory/vector_index.py +270 -0
  41. package/brain/runtime/model_roles.py +11 -0
  42. package/brain/runtime/model_router.py +22 -0
  43. package/brain/runtime/providers.py +59 -0
  44. package/brain/runtime/security/__init__.py +3 -0
  45. package/brain/runtime/security/redaction.py +14 -0
  46. package/brain/runtime/state_store.py +25 -0
  47. package/brain/runtime/storage_paths.py +41 -0
  48. package/docs/architecture/memory.md +118 -0
  49. package/docs/release-checklist.md +34 -0
  50. package/docs/reports/ocmemog-code-audit-2026-03-14.md +155 -0
  51. package/docs/usage.md +223 -0
  52. package/index.ts +726 -0
  53. package/ocmemog/__init__.py +1 -0
  54. package/ocmemog/sidecar/__init__.py +1 -0
  55. package/ocmemog/sidecar/app.py +1068 -0
  56. package/ocmemog/sidecar/compat.py +74 -0
  57. package/ocmemog/sidecar/transcript_watcher.py +425 -0
  58. package/openclaw.plugin.json +18 -0
  59. package/package.json +60 -0
  60. package/scripts/install-ocmemog.sh +277 -0
  61. package/scripts/launchagents/com.openclaw.ocmemog.guard.plist +22 -0
  62. package/scripts/launchagents/com.openclaw.ocmemog.ponder.plist +22 -0
  63. package/scripts/launchagents/com.openclaw.ocmemog.sidecar.plist +27 -0
  64. package/scripts/ocmemog-context.sh +15 -0
  65. package/scripts/ocmemog-continuity-benchmark.py +178 -0
  66. package/scripts/ocmemog-demo.py +122 -0
  67. package/scripts/ocmemog-failover-test.sh +17 -0
  68. package/scripts/ocmemog-guard.sh +11 -0
  69. package/scripts/ocmemog-install.sh +93 -0
  70. package/scripts/ocmemog-load-test.py +106 -0
  71. package/scripts/ocmemog-ponder.sh +30 -0
  72. package/scripts/ocmemog-recall-test.py +58 -0
  73. package/scripts/ocmemog-reindex-vectors.py +14 -0
  74. package/scripts/ocmemog-reliability-soak.py +177 -0
  75. package/scripts/ocmemog-sidecar.sh +46 -0
  76. package/scripts/ocmemog-soak-report.py +58 -0
  77. package/scripts/ocmemog-soak-test.py +44 -0
  78. package/scripts/ocmemog-test-rig.py +345 -0
  79. package/scripts/ocmemog-transcript-append.py +45 -0
  80. package/scripts/ocmemog-transcript-watcher.py +8 -0
  81. package/scripts/ocmemog-transcript-watcher.sh +7 -0
@@ -0,0 +1,198 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import re
5
+ from typing import Dict, Any, List
6
+
7
+ from brain.runtime.instrumentation import emit_event
8
+ from brain.runtime import state_store
9
+ from brain.runtime.memory import candidate, provenance, store
10
+ from brain.runtime.security import redaction
11
+ from brain.runtime import inference
12
+ from brain.runtime import model_roles
13
+
14
+
15
+ def _normalize(text: str) -> str:
16
+ return re.sub(r"\s+", " ", text.lower()).strip()
17
+
18
+
19
+ def _heuristic_summary(text: str) -> str:
20
+ lines = [line.strip() for line in text.splitlines() if line.strip()]
21
+ if not lines:
22
+ return ""
23
+ return lines[0][:240]
24
+
25
+
26
+ def _verification_points(text: str) -> List[str]:
27
+ points = []
28
+ if "verify" in text.lower():
29
+ points.append("Verify referenced assumptions")
30
+ if "risk" in text.lower():
31
+ points.append("Validate risk and mitigation")
32
+ if not points:
33
+ points.append("Confirm key facts before promotion")
34
+ return points[:3]
35
+
36
+
37
+ def _candidate_score(summary: str, source: str) -> float:
38
+ if not source:
39
+ return 0.0
40
+ ratio = len(summary) / max(1, len(source))
41
+ score = 1.0 - min(1.0, ratio * 0.5)
42
+ return round(max(0.1, min(1.0, score)), 3)
43
+
44
+
45
+ def _row_value(row: Any, key: str, fallback_index: int | None = None) -> Any:
46
+ if isinstance(row, dict):
47
+ return row.get(key)
48
+ try:
49
+ return row[key]
50
+ except Exception:
51
+ if fallback_index is None:
52
+ return None
53
+ try:
54
+ return row[fallback_index]
55
+ except Exception:
56
+ return None
57
+
58
+
59
+
60
+ def distill_experiences(limit: int = 10) -> List[Dict[str, Any]]:
61
+ emit_event(state_store.reports_dir() / "brain_memory.log.jsonl", "brain_memory_distill_start", status="ok")
62
+ conn = store.connect()
63
+ rows = conn.execute(
64
+ "SELECT id, task_id, outcome, source_module, metadata_json FROM experiences ORDER BY id DESC LIMIT ?",
65
+ (limit,),
66
+ ).fetchall()
67
+ conn.close()
68
+
69
+ distilled: List[Dict[str, Any]] = []
70
+ seen = set()
71
+
72
+ for row in rows:
73
+ source_id = _row_value(row, "id", 0)
74
+ task_id = _row_value(row, "task_id", 1)
75
+ content = _row_value(row, "outcome", 2) or ""
76
+ source_module = _row_value(row, "source_module", 3)
77
+ raw_metadata = _row_value(row, "metadata_json", 4) or "{}"
78
+ try:
79
+ experience_metadata = json.loads(raw_metadata) if isinstance(raw_metadata, str) else dict(raw_metadata or {})
80
+ except Exception:
81
+ experience_metadata = {}
82
+ content, _ = redaction.redact_text(content)
83
+
84
+ summary = ""
85
+ try:
86
+ model = model_roles.get_model_for_role("memory")
87
+ result = inference.infer(
88
+ f"Distill this experience into a concise summary:\n\n{content}".strip(),
89
+ provider_name=model,
90
+ )
91
+ if result.get("status") == "ok":
92
+ summary = str(result.get("output", "")).strip()
93
+ except Exception:
94
+ summary = ""
95
+
96
+ if not summary or len(summary) > len(content):
97
+ summary = _heuristic_summary(content)
98
+
99
+ summary, _ = redaction.redact_text(summary)
100
+ norm = _normalize(summary)
101
+ if not norm or norm in seen:
102
+ emit_event(state_store.reports_dir() / "brain_memory.log.jsonl", "brain_memory_distill_rejected", status="ok")
103
+ continue
104
+
105
+ seen.add(norm)
106
+ verification = _verification_points(content)
107
+ score = _candidate_score(summary, content)
108
+ ratio = len(summary) / max(1, len(content))
109
+
110
+ if score <= 0.1:
111
+ emit_event(state_store.reports_dir() / "brain_memory.log.jsonl", "brain_memory_distill_rejected", status="ok")
112
+ continue
113
+
114
+ candidate_metadata = provenance.normalize_metadata(
115
+ {
116
+ **experience_metadata,
117
+ "compression_ratio": round(ratio, 3),
118
+ "task_id": task_id,
119
+ "source_event_id": source_id,
120
+ "experience_reference": f"experiences:{source_id}",
121
+ "derived_via": "distill",
122
+ "kind": "distilled_candidate",
123
+ "source_labels": [*(experience_metadata.get("source_labels") or []), *( [source_module] if source_module else [])],
124
+ },
125
+ source=source_module,
126
+ )
127
+ candidate_result = candidate.create_candidate(
128
+ source_event_id=source_id,
129
+ distilled_summary=summary,
130
+ verification_points=verification,
131
+ confidence_score=score,
132
+ metadata=candidate_metadata,
133
+ )
134
+
135
+ distilled.append({
136
+ "source_event_id": source_id,
137
+ "distilled_summary": summary,
138
+ "verification_points": verification,
139
+ "confidence_score": score,
140
+ "compression_ratio": round(ratio, 3),
141
+ "candidate_id": candidate_result.get("candidate_id"),
142
+ "duplicate": candidate_result.get("duplicate"),
143
+ "provenance": provenance.preview_from_metadata(candidate_metadata),
144
+ })
145
+ emit_event(state_store.reports_dir() / "brain_memory.log.jsonl", "brain_memory_distill_success", status="ok")
146
+
147
+ return distilled
148
+
149
+
150
+ def distill_artifact(artifact: Dict[str, Any]) -> List[Dict[str, Any]]:
151
+ text = artifact.get("content_text", "")
152
+ if not isinstance(text, str) or not text.strip():
153
+ return []
154
+
155
+ text, _ = redaction.redact_text(text)
156
+ summary = _heuristic_summary(text)
157
+ summary, _ = redaction.redact_text(summary)
158
+ norm = _normalize(summary)
159
+ if not norm:
160
+ emit_event(state_store.reports_dir() / "brain_memory.log.jsonl", "brain_memory_distill_rejected", status="ok")
161
+ return []
162
+
163
+ verification = _verification_points(text)
164
+ score = _candidate_score(summary, text)
165
+ ratio = len(summary) / max(1, len(text))
166
+
167
+ if score <= 0.1:
168
+ emit_event(state_store.reports_dir() / "brain_memory.log.jsonl", "brain_memory_distill_rejected", status="ok")
169
+ return []
170
+
171
+ candidate_metadata = provenance.normalize_metadata(
172
+ {
173
+ "compression_ratio": round(ratio, 3),
174
+ "artifact_id": artifact.get("artifact_id"),
175
+ "derived_via": "artifact_distill",
176
+ "kind": "distilled_candidate",
177
+ "source_labels": ["artifact"],
178
+ }
179
+ )
180
+ candidate_result = candidate.create_candidate(
181
+ source_event_id=0,
182
+ distilled_summary=summary,
183
+ verification_points=verification,
184
+ confidence_score=score,
185
+ metadata=candidate_metadata,
186
+ )
187
+
188
+ emit_event(state_store.reports_dir() / "brain_memory.log.jsonl", "brain_memory_distill_success", status="ok")
189
+ return [{
190
+ "source_event_id": 0,
191
+ "distilled_summary": summary,
192
+ "verification_points": verification,
193
+ "confidence_score": score,
194
+ "compression_ratio": round(ratio, 3),
195
+ "candidate_id": candidate_result.get("candidate_id"),
196
+ "duplicate": candidate_result.get("duplicate"),
197
+ "provenance": provenance.preview_from_metadata(candidate_metadata),
198
+ }]
@@ -0,0 +1,94 @@
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ from typing import List, Any
5
+
6
+ from brain.runtime import config, state_store, model_router
7
+ from brain.runtime.instrumentation import emit_event
8
+ from brain.runtime.providers import provider_execute
9
+
10
+ LOGFILE = state_store.reports_dir() / "brain_memory.log.jsonl"
11
+ _MODEL_CACHE: dict[str, Any] = {}
12
+
13
+
14
+ def _simple_embedding(text: str, dims: int = 8) -> List[float]:
15
+ digest = hashlib.sha256(text.encode("utf-8")).digest()
16
+ values = [digest[i] / 255.0 for i in range(dims)]
17
+ return values
18
+
19
+
20
+ def _load_sentence_transformer(model_name: str) -> Any | None:
21
+ if model_name in _MODEL_CACHE:
22
+ return _MODEL_CACHE[model_name]
23
+ try:
24
+ from sentence_transformers import SentenceTransformer
25
+ except Exception:
26
+ return None
27
+ model = SentenceTransformer(model_name)
28
+ _MODEL_CACHE[model_name] = model
29
+ return model
30
+
31
+
32
+ def _provider_embedding(text: str, model_name: str) -> tuple[List[float] | None, dict[str, str]]:
33
+ selection = model_router.get_provider_for_role("embedding")
34
+ if not selection.provider_id:
35
+ return None, {}
36
+ response = provider_execute.execute_embedding_call(selection, text)
37
+ embedding = response.get("embedding") if isinstance(response, dict) else None
38
+ meta = {
39
+ "provider_id": str(getattr(selection, "provider_id", "") or ""),
40
+ "model": str(model_name or getattr(selection, "model", "") or ""),
41
+ }
42
+ if isinstance(embedding, list):
43
+ return [float(x) for x in embedding], meta
44
+ return None, meta
45
+
46
+
47
+ def generate_embedding(text: str) -> List[float] | None:
48
+ emit_event(LOGFILE, "brain_embedding_start", status="ok")
49
+ if not isinstance(text, str) or not text.strip():
50
+ emit_event(LOGFILE, "brain_embedding_failed", status="error", reason="empty_text")
51
+ return None
52
+ local_model = getattr(config, "BRAIN_EMBED_MODEL_LOCAL", "simple")
53
+ provider_model = getattr(config, "BRAIN_EMBED_MODEL_PROVIDER", "")
54
+
55
+ if provider_model:
56
+ try:
57
+ embedding, provider_meta = _provider_embedding(text, provider_model)
58
+ except Exception:
59
+ embedding, provider_meta = None, {}
60
+ if embedding:
61
+ emit_event(
62
+ LOGFILE,
63
+ "brain_embedding_complete",
64
+ status="ok",
65
+ provider="provider",
66
+ provider_id=provider_meta.get("provider_id", ""),
67
+ model=provider_meta.get("model", ""),
68
+ )
69
+ emit_event(
70
+ LOGFILE,
71
+ "brain_embedding_generated",
72
+ status="ok",
73
+ provider="provider",
74
+ dimensions=len(embedding),
75
+ provider_id=provider_meta.get("provider_id", ""),
76
+ model=provider_meta.get("model", ""),
77
+ )
78
+ return embedding
79
+
80
+ if local_model:
81
+ if local_model in {"simple", "hash"}:
82
+ embedding = _simple_embedding(text)
83
+ emit_event(LOGFILE, "brain_embedding_complete", status="ok", provider="local_simple")
84
+ emit_event(LOGFILE, "brain_embedding_generated", status="ok", provider="local_simple", dimensions=len(embedding))
85
+ return embedding
86
+ model = _load_sentence_transformer(local_model)
87
+ if model is not None:
88
+ embedding = model.encode([text])[0]
89
+ emit_event(LOGFILE, "brain_embedding_complete", status="ok", provider="local_model")
90
+ vector = [float(x) for x in embedding]
91
+ emit_event(LOGFILE, "brain_embedding_generated", status="ok", provider="local_model", dimensions=len(vector))
92
+ return vector
93
+ emit_event(LOGFILE, "brain_embedding_failed", status="error", reason="no_embedding")
94
+ return None
@@ -0,0 +1,91 @@
1
+ from __future__ import annotations
2
+
3
+ import time
4
+ from typing import Any, Dict, List
5
+
6
+ from brain.runtime import state_store
7
+ from brain.runtime.instrumentation import emit_event
8
+ from brain.runtime.memory import store
9
+
10
+ DEFAULT_STALE_DAYS = 30
11
+ DEFAULT_CONFIDENCE_THRESHOLD = 0.6
12
+ DEFAULT_LIMIT = 25
13
+
14
+
15
+ def scan_freshness(
16
+ stale_days: int = DEFAULT_STALE_DAYS,
17
+ confidence_threshold: float = DEFAULT_CONFIDENCE_THRESHOLD,
18
+ limit: int = DEFAULT_LIMIT,
19
+ ) -> Dict[str, Any]:
20
+ emit_event(
21
+ state_store.reports_dir() / "brain_memory.log.jsonl",
22
+ "brain_memory_freshness_scan_start",
23
+ status="ok",
24
+ stale_days=stale_days,
25
+ confidence_threshold=confidence_threshold,
26
+ )
27
+ conn = store.connect()
28
+ stale_rows = conn.execute(
29
+ """
30
+ SELECT 'knowledge' AS memory_type, id, timestamp, confidence, content
31
+ FROM knowledge
32
+ WHERE timestamp <= datetime('now', ?)
33
+ ORDER BY timestamp ASC
34
+ LIMIT ?
35
+ """,
36
+ (f"-{max(1, stale_days)} days", limit),
37
+ ).fetchall()
38
+ low_conf_rows = conn.execute(
39
+ """
40
+ SELECT 'knowledge' AS memory_type, id, timestamp, confidence, content
41
+ FROM knowledge
42
+ WHERE confidence < ?
43
+ ORDER BY confidence ASC, timestamp ASC
44
+ LIMIT ?
45
+ """,
46
+ (confidence_threshold, limit),
47
+ ).fetchall()
48
+ conn.close()
49
+ advisories: List[Dict[str, Any]] = []
50
+ refresh_candidates: List[Dict[str, Any]] = []
51
+ now_ts = time.time()
52
+ for category, rows in (("stale", stale_rows), ("low_confidence", low_conf_rows)):
53
+ for row in rows:
54
+ age_seconds = 0.0
55
+ if row["timestamp"]:
56
+ try:
57
+ age_seconds = max(0.0, now_ts - time.mktime(time.strptime(row["timestamp"], "%Y-%m-%d %H:%M:%S")))
58
+ except Exception:
59
+ age_seconds = 0.0
60
+ confidence = float(row["confidence"] or 0.0)
61
+ freshness_score = max(0.0, 1.0 - min(age_seconds / (stale_days * 86400), 1.0)) * (0.5 + confidence / 2.0)
62
+ refresh_recommended = freshness_score < 0.4 or category == "stale"
63
+ entry = {
64
+ "category": category,
65
+ "memory_type": row["memory_type"],
66
+ "memory_id": row["id"],
67
+ "timestamp": row["timestamp"],
68
+ "confidence": confidence,
69
+ "summary": str(row["content"])[:120],
70
+ "freshness_score": round(freshness_score, 3),
71
+ "refresh_recommended": refresh_recommended,
72
+ }
73
+ advisories.append(entry)
74
+ refresh_candidates.append(entry)
75
+ emit_event(
76
+ state_store.reports_dir() / "brain_memory.log.jsonl",
77
+ "brain_memory_freshness_scan_complete",
78
+ status="ok",
79
+ advisory_count=len(advisories),
80
+ refresh_candidates=len(refresh_candidates),
81
+ )
82
+ return {
83
+ "ok": True,
84
+ "advisory_only": True,
85
+ "advisories": advisories,
86
+ "refresh_candidates": refresh_candidates,
87
+ }
88
+
89
+
90
+ def freshness_weight(score: float) -> float:
91
+ return max(0.0, min(1.0, float(score)))
@@ -0,0 +1,42 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Dict, Any
4
+
5
+ from brain.runtime.memory import store, integrity
6
+
7
+
8
+ EMBED_TABLES = ("knowledge", "runbooks", "lessons", "directives", "reflections", "tasks")
9
+
10
+
11
+ def get_memory_health() -> Dict[str, Any]:
12
+ conn = store.connect()
13
+ counts: Dict[str, int] = {}
14
+ for table in ["experiences", "candidates", "promotions", "memory_index", "knowledge", "runbooks", "lessons", "directives", "reflections", "tasks", "vector_embeddings"]:
15
+ try:
16
+ counts[table] = conn.execute(f"SELECT COUNT(*) FROM {table}").fetchone()[0]
17
+ except Exception:
18
+ counts[table] = 0
19
+
20
+ vector_count = 0
21
+ try:
22
+ vector_count = conn.execute(
23
+ "SELECT COUNT(*) FROM vector_embeddings WHERE source_type IN ('knowledge','runbooks','lessons','directives','reflections','tasks')"
24
+ ).fetchone()[0]
25
+ except Exception:
26
+ vector_count = 0
27
+
28
+ total_embed_sources = sum(counts.get(table, 0) for table in EMBED_TABLES)
29
+ conn.close()
30
+ integrity_result = integrity.run_integrity_check()
31
+
32
+ coverage = 0.0
33
+ if total_embed_sources:
34
+ coverage = round(vector_count / total_embed_sources, 3)
35
+
36
+ return {
37
+ "counts": counts,
38
+ "vector_index_count": vector_count,
39
+ "vector_index_coverage": coverage,
40
+ "vector_index_integrity_status": integrity_result.get("ok"),
41
+ "integrity": integrity_result,
42
+ }
@@ -0,0 +1,170 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Dict, Any, List
4
+
5
+ from brain.runtime.instrumentation import emit_event
6
+ from brain.runtime import state_store
7
+ from brain.runtime.memory import store
8
+
9
+
10
+ EMBED_TABLES = ("knowledge", "runbooks", "lessons", "directives", "reflections", "tasks")
11
+
12
+
13
+ def run_integrity_check() -> Dict[str, Any]:
14
+ emit_event(state_store.reports_dir() / "brain_memory.log.jsonl", "brain_memory_integrity_start", status="ok")
15
+ conn = store.connect()
16
+ issues: List[str] = []
17
+ repairable: List[str] = []
18
+ sqlite_ok = True
19
+
20
+ # required tables
21
+ required = {
22
+ "experiences",
23
+ "knowledge",
24
+ "reflections",
25
+ "tasks",
26
+ "directives",
27
+ "promotions",
28
+ "candidates",
29
+ "memory_index",
30
+ "vector_embeddings",
31
+ "runbooks",
32
+ "lessons",
33
+ }
34
+ tables = {row[0] for row in conn.execute("SELECT name FROM sqlite_master WHERE type='table'").fetchall()}
35
+ missing = required - tables
36
+ if missing:
37
+ issues.append(f"missing_tables:{','.join(sorted(missing))}")
38
+ emit_event(state_store.reports_dir() / "brain_memory.log.jsonl", "brain_memory_integrity_issue", status="warn")
39
+
40
+ # orphan candidates (source_event_id missing in experiences)
41
+ try:
42
+ orphan = conn.execute(
43
+ "SELECT COUNT(*) FROM candidates WHERE source_event_id NOT IN (SELECT id FROM experiences)",
44
+ ).fetchone()[0]
45
+ if orphan:
46
+ issues.append(f"orphan_candidates:{orphan}")
47
+ emit_event(state_store.reports_dir() / "brain_memory.log.jsonl", "brain_memory_integrity_issue", status="warn")
48
+ except Exception:
49
+ pass
50
+
51
+ # duplicate promotions
52
+ try:
53
+ dup = conn.execute(
54
+ "SELECT COUNT(*) FROM promotions GROUP BY source, content HAVING COUNT(*) > 1",
55
+ ).fetchone()
56
+ if dup:
57
+ issues.append("duplicate_promotions")
58
+ emit_event(state_store.reports_dir() / "brain_memory.log.jsonl", "brain_memory_integrity_issue", status="warn")
59
+ except Exception:
60
+ pass
61
+
62
+ # reinforcement references missing
63
+ try:
64
+ missing_ref = conn.execute(
65
+ "SELECT COUNT(*) FROM experiences WHERE memory_reference IS NULL OR memory_reference = ''",
66
+ ).fetchone()[0]
67
+ if missing_ref:
68
+ issues.append(f"missing_memory_reference:{missing_ref}")
69
+ emit_event(state_store.reports_dir() / "brain_memory.log.jsonl", "brain_memory_integrity_issue", status="warn")
70
+ except Exception:
71
+ pass
72
+
73
+ # vector index mismatch (knowledge/runbooks/lessons vs vector_embeddings)
74
+ missing_vectors = 0
75
+ orphan_vectors = 0
76
+ try:
77
+ for table in EMBED_TABLES:
78
+ missing_vectors += conn.execute(
79
+ f"SELECT COUNT(*) FROM {table} WHERE id NOT IN (SELECT CAST(source_id AS INTEGER) FROM vector_embeddings WHERE source_type=?)",
80
+ (table,),
81
+ ).fetchone()[0]
82
+ except Exception:
83
+ pass
84
+
85
+ try:
86
+ for table in EMBED_TABLES:
87
+ orphan_vectors += conn.execute(
88
+ "SELECT COUNT(*) FROM vector_embeddings WHERE source_type=? AND CAST(source_id AS INTEGER) NOT IN (SELECT id FROM %s)"
89
+ % table,
90
+ (table,),
91
+ ).fetchone()[0]
92
+ except Exception:
93
+ pass
94
+
95
+ if missing_vectors:
96
+ issues.append(f"vector_missing:{missing_vectors}")
97
+ emit_event(state_store.reports_dir() / "brain_memory.log.jsonl", "brain_memory_vector_integrity_issue", status="warn")
98
+
99
+ if orphan_vectors:
100
+ issues.append(f"vector_orphan:{orphan_vectors}")
101
+ repairable.append("vector_orphan")
102
+ emit_event(state_store.reports_dir() / "brain_memory.log.jsonl", "brain_memory_vector_integrity_issue", status="warn")
103
+
104
+ try:
105
+ quick_check = conn.execute("PRAGMA quick_check(1)").fetchone()
106
+ quick_value = str((quick_check or ["ok"])[0] or "ok")
107
+ if quick_value.lower() != "ok":
108
+ sqlite_ok = False
109
+ issues.append(f"sqlite_quick_check:{quick_value}")
110
+ emit_event(state_store.reports_dir() / "brain_memory.log.jsonl", "brain_memory_integrity_issue", status="warn")
111
+ except Exception:
112
+ sqlite_ok = False
113
+
114
+ warning_type = ""
115
+ warning_summary = ""
116
+ for issue in issues:
117
+ if issue.startswith("vector_missing"):
118
+ warning_type = "vector_missing"
119
+ warning_summary = "Vector embeddings missing entries"
120
+ break
121
+ if issue.startswith("vector_orphan"):
122
+ warning_type = "vector_orphan"
123
+ warning_summary = "Vector embeddings have orphan entries"
124
+ break
125
+
126
+ conn.close()
127
+ emit_event(state_store.reports_dir() / "brain_memory.log.jsonl", "brain_memory_integrity_complete", status="ok")
128
+ return {
129
+ "issues": issues,
130
+ "ok": len(issues) == 0 and sqlite_ok,
131
+ "warning_type": warning_type,
132
+ "warning_summary": warning_summary,
133
+ "repairable_issues": repairable,
134
+ "sqlite_ok": sqlite_ok,
135
+ }
136
+
137
+
138
+ def repair_integrity() -> Dict[str, Any]:
139
+ repaired: List[str] = []
140
+
141
+ def _write() -> Dict[str, Any]:
142
+ conn = store.connect()
143
+ removed_orphans = 0
144
+ try:
145
+ tables = {row[0] for row in conn.execute("SELECT name FROM sqlite_master WHERE type='table'").fetchall()}
146
+ if "vector_embeddings" in tables:
147
+ for table in EMBED_TABLES:
148
+ if table not in tables:
149
+ continue
150
+ removed_orphans += conn.execute(
151
+ f"""
152
+ DELETE FROM vector_embeddings
153
+ WHERE source_type = ?
154
+ AND NOT EXISTS (
155
+ SELECT 1 FROM {table} source
156
+ WHERE source.id = CAST(vector_embeddings.source_id AS INTEGER)
157
+ )
158
+ """,
159
+ (table,),
160
+ ).rowcount
161
+ conn.commit()
162
+ return {"removed_orphan_vectors": int(removed_orphans)}
163
+ finally:
164
+ conn.close()
165
+
166
+ result = store.submit_write(_write, timeout=30.0)
167
+ if int(result.get("removed_orphan_vectors") or 0) > 0:
168
+ repaired.append(f"vector_orphan:{int(result['removed_orphan_vectors'])}")
169
+ emit_event(state_store.reports_dir() / "brain_memory.log.jsonl", "brain_memory_integrity_repair", status="ok", repaired="vector_orphan", count=int(result["removed_orphan_vectors"]))
170
+ return {"ok": True, "repaired": repaired, **result}
@@ -0,0 +1,57 @@
1
+ from __future__ import annotations
2
+
3
+ import sqlite3
4
+ import time
5
+ from typing import Dict, List
6
+
7
+ from brain.runtime import state_store
8
+ from brain.runtime.instrumentation import emit_event
9
+ from brain.runtime.memory import person_memory
10
+
11
+ LOGFILE = state_store.reports_dir() / "brain_memory.log.jsonl"
12
+
13
+
14
+ def _connect() -> sqlite3.Connection:
15
+ path = state_store.data_dir() / "interaction_memory.db"
16
+ path.parent.mkdir(parents=True, exist_ok=True)
17
+ conn = sqlite3.connect(str(path))
18
+ conn.row_factory = sqlite3.Row
19
+ conn.execute(
20
+ """
21
+ CREATE TABLE IF NOT EXISTS interaction_memory (
22
+ interaction_id INTEGER PRIMARY KEY AUTOINCREMENT,
23
+ person_id TEXT,
24
+ timestamp TEXT,
25
+ channel TEXT,
26
+ thread_id TEXT,
27
+ sentiment TEXT,
28
+ outcome TEXT
29
+ )
30
+ """
31
+ )
32
+ return conn
33
+
34
+
35
+ def record_interaction(person_id: str, channel: str, thread_id: str, sentiment: str, outcome: str) -> None:
36
+ conn = _connect()
37
+ conn.execute(
38
+ "INSERT INTO interaction_memory (person_id, timestamp, channel, thread_id, sentiment, outcome) VALUES (?, ?, ?, ?, ?, ?)",
39
+ (person_id, time.strftime("%Y-%m-%d %H:%M:%S"), channel, thread_id, sentiment, outcome[:80]),
40
+ )
41
+ conn.commit()
42
+ conn.close()
43
+ person_memory.update_person(
44
+ person_id,
45
+ {"interaction_count": (person_memory.get_person(person_id) or {}).get("interaction_count", 0) + 1, "last_seen": time.strftime("%Y-%m-%d %H:%M:%S")},
46
+ )
47
+ emit_event(LOGFILE, "brain_person_interaction_recorded", status="ok", person_id=person_id)
48
+
49
+
50
+ def get_recent_interactions(person_id: str, limit: int = 10) -> List[Dict[str, object]]:
51
+ conn = _connect()
52
+ rows = conn.execute(
53
+ "SELECT timestamp, channel, thread_id, sentiment, outcome FROM interaction_memory WHERE person_id=? ORDER BY timestamp DESC LIMIT ?",
54
+ (person_id, limit),
55
+ ).fetchall()
56
+ conn.close()
57
+ return [dict(row) for row in rows]