@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,237 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from typing import Dict, Any
5
+
6
+ from brain.runtime.instrumentation import emit_event
7
+ from brain.runtime import state_store
8
+ from brain.runtime.memory import provenance, store
9
+ from brain.runtime import config
10
+
11
+
12
+ LOGFILE = state_store.reports_dir() / "brain_memory.log.jsonl"
13
+
14
+
15
+ def _should_promote(confidence: float, threshold: float | None = None) -> bool:
16
+ threshold = config.OCMEMOG_PROMOTION_THRESHOLD if threshold is None else threshold
17
+ return confidence >= float(threshold)
18
+
19
+
20
+ def _destination_table(summary: str) -> str:
21
+ lowered = summary.lower()
22
+ if "runbook" in lowered or "procedure" in lowered or "steps" in lowered:
23
+ return "runbooks"
24
+ if "lesson" in lowered or "postmortem" in lowered or "learned" in lowered:
25
+ return "lessons"
26
+ return "knowledge"
27
+
28
+
29
+ def promote_candidate(candidate: Dict[str, Any]) -> Dict[str, Any]:
30
+ emit_event(LOGFILE, "brain_memory_promote_start", status="ok")
31
+ confidence = float(candidate.get("confidence_score", 0.0))
32
+ decision = "promote" if _should_promote(confidence) else "reject"
33
+ candidate_id = str(candidate.get("candidate_id") or "")
34
+
35
+ candidate_metadata = provenance.normalize_metadata(candidate.get("metadata", {}), source="promote")
36
+ candidate_metadata["candidate_id"] = candidate_id
37
+ candidate_metadata["derived_from_candidate_id"] = candidate_id
38
+ candidate_metadata["derived_via"] = "promotion"
39
+
40
+ conn = store.connect()
41
+ promotion_id = None
42
+ destination = _destination_table(str(candidate.get("distilled_summary", "")))
43
+ if decision == "promote":
44
+ row = conn.execute(
45
+ "SELECT id FROM promotions WHERE source=? AND content=?",
46
+ (str(candidate.get("source_event_id")), candidate.get("distilled_summary", "")),
47
+ ).fetchone()
48
+ if not row:
49
+ cur = conn.execute(
50
+ """
51
+ INSERT INTO promotions (
52
+ candidate_id, source, confidence, status, decision_reason,
53
+ metadata_json, content, schema_version
54
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
55
+ """,
56
+ (
57
+ candidate_id,
58
+ str(candidate.get("source_event_id")),
59
+ confidence,
60
+ "promoted",
61
+ "confidence_threshold",
62
+ json.dumps(candidate_metadata, ensure_ascii=False),
63
+ candidate.get("distilled_summary", ""),
64
+ store.SCHEMA_VERSION,
65
+ ),
66
+ )
67
+ memory_rec = conn.execute(
68
+ f"INSERT INTO {destination} (source, confidence, metadata_json, content, schema_version) VALUES (?, ?, ?, ?, ?)",
69
+ (
70
+ str(candidate.get("source_event_id")),
71
+ confidence,
72
+ json.dumps(candidate_metadata, ensure_ascii=False),
73
+ candidate.get("distilled_summary", ""),
74
+ store.SCHEMA_VERSION,
75
+ ),
76
+ )
77
+ conn.execute(
78
+ "UPDATE candidates SET status='promoted', updated_at=datetime('now') WHERE candidate_id=?",
79
+ (candidate_id,),
80
+ )
81
+ conn.execute(
82
+ "INSERT INTO memory_events (event_type, source, details_json, schema_version) VALUES (?, ?, ?, ?)",
83
+ (
84
+ "candidate_promoted",
85
+ str(candidate.get("source_event_id")),
86
+ json.dumps({"candidate_id": candidate_id, "promotion_table": destination}),
87
+ store.SCHEMA_VERSION,
88
+ ),
89
+ )
90
+ conn.commit()
91
+ promotion_id = cur.lastrowid
92
+ memory_id = memory_rec.lastrowid
93
+ else:
94
+ promotion_id = row[0]
95
+ memory_id = None
96
+ emit_event(LOGFILE, "brain_memory_promote_success", status="ok", destination=destination)
97
+ else:
98
+ conn.execute(
99
+ "UPDATE candidates SET status='rejected', updated_at=datetime('now') WHERE candidate_id=?",
100
+ (candidate_id,),
101
+ )
102
+ conn.execute(
103
+ "INSERT INTO promotions (candidate_id, source, confidence, status, decision_reason, metadata_json, content, schema_version) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
104
+ (
105
+ candidate_id,
106
+ str(candidate.get("source_event_id")),
107
+ confidence,
108
+ "rejected",
109
+ "below_threshold",
110
+ json.dumps(candidate_metadata, ensure_ascii=False),
111
+ candidate.get("distilled_summary", ""),
112
+ store.SCHEMA_VERSION,
113
+ ),
114
+ )
115
+ conn.commit()
116
+ emit_event(LOGFILE, "brain_memory_promote_rejected", status="ok")
117
+ memory_id = None
118
+ conn.close()
119
+
120
+ if decision == "promote" and promotion_id is not None:
121
+ from brain.runtime.memory import reinforcement, vector_index
122
+
123
+ promoted_reference = f"{destination}:{memory_id}" if memory_id else ""
124
+ promotion_updates = {
125
+ **candidate_metadata,
126
+ "promotion_id": promotion_id,
127
+ "derived_from_promotion_id": promotion_id,
128
+ }
129
+ if promoted_reference:
130
+ provenance.update_memory_metadata(promoted_reference, promotion_updates)
131
+ reinforcement.log_experience(
132
+ task_id=str(candidate.get("candidate_id") or candidate.get("source_event_id") or ""),
133
+ outcome="promoted",
134
+ confidence=confidence,
135
+ reward_score=confidence,
136
+ memory_reference=promoted_reference or f"promotions:{promotion_id}",
137
+ experience_type="promotion",
138
+ source_module="memory_promote",
139
+ )
140
+ emit_event(LOGFILE, "brain_memory_reinforcement_created", status="ok")
141
+ if memory_id:
142
+ vector_index.insert_memory(memory_id, candidate.get("distilled_summary", ""), confidence)
143
+
144
+ return {"decision": decision, "confidence": confidence, "promotion_id": promotion_id, "destination": destination}
145
+
146
+
147
+ def promote_candidate_by_id(candidate_id: str) -> Dict[str, Any]:
148
+ conn = store.connect()
149
+ row = conn.execute(
150
+ """
151
+ SELECT candidate_id, source_event_id, distilled_summary, verification_points,
152
+ confidence_score, metadata_json
153
+ FROM candidates WHERE candidate_id=?
154
+ """,
155
+ (candidate_id,),
156
+ ).fetchone()
157
+ conn.close()
158
+ if not row:
159
+ emit_event(LOGFILE, "brain_memory_promote_error", status="error")
160
+ return {"decision": "error", "reason": "candidate_not_found"}
161
+ payload = dict(row)
162
+ try:
163
+ payload["metadata"] = json.loads(payload.get("metadata_json") or "{}")
164
+ except Exception:
165
+ payload["metadata"] = {}
166
+ return promote_candidate(payload)
167
+
168
+
169
+ def demote_memory(reference: str, reason: str = "low_confidence", new_confidence: float = 0.1) -> Dict[str, Any]:
170
+ table, sep, raw_id = reference.partition(":")
171
+ if not sep or not raw_id.isdigit():
172
+ return {"ok": False, "error": "invalid_reference"}
173
+ allowed = {"knowledge", "runbooks", "lessons", "directives", "reflections", "tasks"}
174
+ if table not in allowed:
175
+ return {"ok": False, "error": "unsupported_table"}
176
+ conn = store.connect()
177
+ row = conn.execute(
178
+ f"SELECT confidence, content, metadata_json FROM {table} WHERE id=?",
179
+ (int(raw_id),),
180
+ ).fetchone()
181
+ if not row:
182
+ conn.close()
183
+ return {"ok": False, "error": "not_found"}
184
+ previous = float(row[0] or 0.0)
185
+ content = str(row[1] or "")
186
+ metadata_json = row[2] or "{}"
187
+
188
+ # archive into cold storage, then remove from hot table
189
+ conn.execute(
190
+ "INSERT INTO cold_storage (source_table, source_id, content, metadata_json, reason, schema_version) VALUES (?, ?, ?, ?, ?, ?)",
191
+ (table, int(raw_id), content, metadata_json, reason, store.SCHEMA_VERSION),
192
+ )
193
+ conn.execute(
194
+ f"DELETE FROM {table} WHERE id=?",
195
+ (int(raw_id),),
196
+ )
197
+ conn.execute(
198
+ "INSERT INTO demotions (memory_reference, previous_confidence, new_confidence, reason, schema_version) VALUES (?, ?, ?, ?, ?)",
199
+ (reference, previous, float(new_confidence), reason, store.SCHEMA_VERSION),
200
+ )
201
+ conn.commit()
202
+ conn.close()
203
+ emit_event(LOGFILE, "brain_memory_demoted", status="ok", reference=reference)
204
+ return {"ok": True, "reference": reference, "previous": previous, "new": float(new_confidence), "archived": True}
205
+
206
+
207
+ def demote_by_confidence(limit: int = 20, threshold: float | None = None, force: bool = False) -> Dict[str, Any]:
208
+ threshold = config.OCMEMOG_DEMOTION_THRESHOLD if threshold is None else threshold
209
+ tables = ("knowledge", "runbooks", "lessons", "directives", "reflections", "tasks")
210
+ conn = store.connect()
211
+ rows = []
212
+ for table in tables:
213
+ try:
214
+ rows.extend(
215
+ conn.execute(
216
+ f"SELECT '{table}' AS table_name, id, confidence FROM {table} ORDER BY confidence ASC LIMIT ?",
217
+ (limit,),
218
+ ).fetchall()
219
+ )
220
+ except Exception:
221
+ continue
222
+ conn.close()
223
+ # sort by confidence and demote below threshold
224
+ ranked = sorted(rows, key=lambda r: float(r[2] or 0.0))
225
+ demoted = []
226
+ for row in ranked:
227
+ table = row[0]
228
+ memory_id = int(row[1])
229
+ confidence = float(row[2] or 0.0)
230
+ if confidence >= float(threshold) and not force:
231
+ continue
232
+ result = demote_memory(f"{table}:{memory_id}", reason="low_confidence", new_confidence=confidence * 0.5)
233
+ if result.get("ok"):
234
+ demoted.append(result)
235
+ if len(demoted) >= limit:
236
+ break
237
+ return {"ok": True, "threshold": float(threshold), "demoted": demoted, "count": len(demoted)}
@@ -0,0 +1,356 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from pathlib import Path
5
+ from typing import Any, Dict, Iterable, List, Optional, Set
6
+
7
+ from brain.runtime.memory import memory_links, store
8
+
9
+ _MEMORY_TABLES = {"knowledge", "reflections", "directives", "tasks", "runbooks", "lessons"}
10
+ _FETCHABLE_TABLES = _MEMORY_TABLES | {"promotions", "experiences", "conversation_turns", "conversation_checkpoints"}
11
+ _SYNTHETIC_PREFIXES = {"conversation", "session", "thread", "message", "label", "transcript"}
12
+
13
+
14
+ def _load_json(value: Any, fallback: Any) -> Any:
15
+ try:
16
+ return json.loads(value or "")
17
+ except Exception:
18
+ return fallback
19
+
20
+
21
+ def _dedupe(values: Iterable[str]) -> List[str]:
22
+ seen: Set[str] = set()
23
+ items: List[str] = []
24
+ for value in values:
25
+ item = str(value or "").strip()
26
+ if not item or item in seen:
27
+ continue
28
+ seen.add(item)
29
+ items.append(item)
30
+ return items
31
+
32
+
33
+ def _transcript_target(path: str, start_line: Any = None, end_line: Any = None) -> str:
34
+ suffix = ""
35
+ try:
36
+ start = int(start_line) if start_line is not None else None
37
+ except Exception:
38
+ start = None
39
+ try:
40
+ end = int(end_line) if end_line is not None else None
41
+ except Exception:
42
+ end = None
43
+ if start and end and end >= start:
44
+ suffix = f"#L{start}-L{end}"
45
+ elif start:
46
+ suffix = f"#L{start}"
47
+ return f"transcript:{path}{suffix}"
48
+
49
+
50
+ def normalize_metadata(metadata: Optional[Dict[str, Any]], *, source: Optional[str] = None) -> Dict[str, Any]:
51
+ raw = dict(metadata or {})
52
+ existing = raw.get("provenance") if isinstance(raw.get("provenance"), dict) else {}
53
+
54
+ source_references = _dedupe(
55
+ [
56
+ *(existing.get("source_references") or []),
57
+ *(raw.get("source_references") or []),
58
+ existing.get("source_reference") or "",
59
+ raw.get("source_reference") or "",
60
+ existing.get("experience_reference") or "",
61
+ raw.get("experience_reference") or "",
62
+ ]
63
+ )
64
+ source_labels = _dedupe(
65
+ [
66
+ *(existing.get("source_labels") or []),
67
+ *(raw.get("source_labels") or []),
68
+ existing.get("source_label") or "",
69
+ raw.get("source_label") or "",
70
+ ]
71
+ )
72
+
73
+ conversation = dict(existing.get("conversation") or {})
74
+ for key in ("conversation_id", "session_id", "thread_id", "message_id", "role"):
75
+ if raw.get(key) is not None and conversation.get(key) is None:
76
+ conversation[key] = raw.get(key)
77
+
78
+ transcript_anchor = dict(existing.get("transcript_anchor") or {})
79
+ if raw.get("transcript_path") and not transcript_anchor.get("path"):
80
+ transcript_anchor = {
81
+ "path": raw.get("transcript_path"),
82
+ "start_line": raw.get("transcript_offset"),
83
+ "end_line": raw.get("transcript_end_offset"),
84
+ }
85
+
86
+ provenance: Dict[str, Any] = dict(existing)
87
+ if source_references:
88
+ provenance["source_references"] = source_references
89
+ provenance["source_reference"] = source_references[0]
90
+ if source_labels:
91
+ provenance["source_labels"] = source_labels
92
+ if conversation:
93
+ provenance["conversation"] = conversation
94
+ if transcript_anchor.get("path"):
95
+ provenance["transcript_anchor"] = transcript_anchor
96
+ if source:
97
+ provenance.setdefault("origin_source", source)
98
+
99
+ for key in (
100
+ "source_event_id",
101
+ "task_id",
102
+ "candidate_id",
103
+ "promotion_id",
104
+ "experience_reference",
105
+ "derived_from_candidate_id",
106
+ "derived_from_promotion_id",
107
+ "derived_via",
108
+ "kind",
109
+ ):
110
+ if raw.get(key) is not None and provenance.get(key) is None:
111
+ provenance[key] = raw.get(key)
112
+
113
+ if provenance:
114
+ raw["provenance"] = provenance
115
+ if source_references:
116
+ raw["source_reference"] = source_references[0]
117
+ raw["source_references"] = source_references
118
+ if source_labels:
119
+ raw["source_labels"] = source_labels
120
+ raw.setdefault("source_label", source_labels[0])
121
+ return raw
122
+
123
+
124
+ def preview_from_metadata(metadata: Optional[Dict[str, Any]]) -> Dict[str, Any]:
125
+ normalized = normalize_metadata(metadata)
126
+ provenance = normalized.get("provenance") if isinstance(normalized.get("provenance"), dict) else {}
127
+ return {
128
+ "source_references": provenance.get("source_references") or [],
129
+ "source_labels": provenance.get("source_labels") or [],
130
+ "conversation": provenance.get("conversation") or {},
131
+ "transcript_anchor": provenance.get("transcript_anchor") or None,
132
+ "origin_source": provenance.get("origin_source"),
133
+ "derived_via": provenance.get("derived_via"),
134
+ }
135
+
136
+
137
+ def _link_once(source_reference: str, link_type: str, target_reference: str) -> None:
138
+ if not source_reference or not target_reference:
139
+ return
140
+ existing = memory_links.get_memory_links(source_reference)
141
+ if any(item.get("link_type") == link_type and item.get("target_reference") == target_reference for item in existing):
142
+ return
143
+ memory_links.add_memory_link(source_reference, link_type, target_reference)
144
+
145
+
146
+ def apply_links(reference: str, metadata: Optional[Dict[str, Any]]) -> None:
147
+ normalized = normalize_metadata(metadata)
148
+ provenance = normalized.get("provenance") if isinstance(normalized.get("provenance"), dict) else {}
149
+ for source_reference in provenance.get("source_references") or []:
150
+ _link_once(reference, "derived_from", str(source_reference))
151
+ for label in provenance.get("source_labels") or []:
152
+ _link_once(reference, "source_label", f"label:{label}")
153
+ conversation = provenance.get("conversation") or {}
154
+ for key, link_type in (
155
+ ("conversation_id", "conversation"),
156
+ ("session_id", "session"),
157
+ ("thread_id", "thread"),
158
+ ("message_id", "message"),
159
+ ):
160
+ value = str(conversation.get(key) or "").strip()
161
+ if value:
162
+ _link_once(reference, link_type, f"{link_type}:{value}")
163
+ transcript = provenance.get("transcript_anchor") or {}
164
+ if transcript.get("path"):
165
+ _link_once(
166
+ reference,
167
+ "transcript",
168
+ _transcript_target(
169
+ str(transcript.get("path")),
170
+ transcript.get("start_line"),
171
+ transcript.get("end_line"),
172
+ ),
173
+ )
174
+ if provenance.get("experience_reference"):
175
+ _link_once(reference, "experience", str(provenance.get("experience_reference")))
176
+ if provenance.get("derived_from_candidate_id"):
177
+ _link_once(reference, "candidate", f"candidate:{provenance['derived_from_candidate_id']}")
178
+ if provenance.get("derived_from_promotion_id"):
179
+ _link_once(reference, "promotion", f"promotions:{provenance['derived_from_promotion_id']}")
180
+
181
+
182
+ def update_memory_metadata(reference: str, updates: Dict[str, Any]) -> Optional[Dict[str, Any]]:
183
+ table, sep, raw_id = reference.partition(":")
184
+ if not sep or table not in _MEMORY_TABLES or not raw_id.isdigit():
185
+ return None
186
+ conn = store.connect()
187
+ try:
188
+ row = conn.execute(f"SELECT metadata_json FROM {table} WHERE id = ?", (int(raw_id),)).fetchone()
189
+ if not row:
190
+ return None
191
+ current = _load_json(row["metadata_json"], {})
192
+ merged = normalize_metadata({**current, **updates})
193
+ conn.execute(
194
+ f"UPDATE {table} SET metadata_json = ? WHERE id = ?",
195
+ (json.dumps(merged, ensure_ascii=False), int(raw_id)),
196
+ )
197
+ conn.commit()
198
+ finally:
199
+ conn.close()
200
+ apply_links(reference, merged)
201
+ return merged
202
+
203
+
204
+ def fetch_reference(reference: str) -> Optional[Dict[str, Any]]:
205
+ prefix, sep, raw_id = reference.partition(":")
206
+ if not sep or not prefix:
207
+ return None
208
+ if prefix in _SYNTHETIC_PREFIXES:
209
+ payload: Dict[str, Any] = {"reference": reference, "type": prefix, "value": raw_id}
210
+ if prefix == "transcript":
211
+ payload["path"] = raw_id
212
+ return payload
213
+ if prefix == "candidate":
214
+ return {"reference": reference, "type": "candidate", "candidate_id": raw_id}
215
+ if prefix not in _FETCHABLE_TABLES:
216
+ return None
217
+
218
+ conn = store.connect()
219
+ try:
220
+ if prefix == "conversation_turns":
221
+ if not raw_id.isdigit():
222
+ return None
223
+ row = conn.execute("SELECT * FROM conversation_turns WHERE id = ?", (int(raw_id),)).fetchone()
224
+ if not row:
225
+ return None
226
+ payload = dict(row)
227
+ payload["reference"] = reference
228
+ payload["table"] = prefix
229
+ payload["id"] = int(raw_id)
230
+ payload["metadata"] = _load_json(payload.pop("metadata_json", "{}"), {})
231
+ return payload
232
+ if prefix == "conversation_checkpoints":
233
+ if not raw_id.isdigit():
234
+ return None
235
+ row = conn.execute("SELECT * FROM conversation_checkpoints WHERE id = ?", (int(raw_id),)).fetchone()
236
+ if not row:
237
+ return None
238
+ payload = dict(row)
239
+ payload["reference"] = reference
240
+ payload["table"] = prefix
241
+ payload["id"] = int(raw_id)
242
+ payload["metadata"] = _load_json(payload.pop("metadata_json", "{}"), {})
243
+ payload["open_loops"] = _load_json(payload.pop("open_loops_json", "[]"), [])
244
+ payload["pending_actions"] = _load_json(payload.pop("pending_actions_json", "[]"), [])
245
+ return payload
246
+ if prefix == "experiences":
247
+ if not raw_id.isdigit():
248
+ return None
249
+ row = conn.execute("SELECT * FROM experiences WHERE id = ?", (int(raw_id),)).fetchone()
250
+ if not row:
251
+ return None
252
+ payload = dict(row)
253
+ payload["reference"] = reference
254
+ payload["table"] = prefix
255
+ payload["id"] = int(raw_id)
256
+ payload["content"] = payload.get("outcome")
257
+ payload["metadata"] = _load_json(payload.pop("metadata_json", "{}"), {})
258
+ return payload
259
+ if prefix == "promotions":
260
+ if not raw_id.isdigit():
261
+ return None
262
+ row = conn.execute("SELECT * FROM promotions WHERE id = ?", (int(raw_id),)).fetchone()
263
+ if not row:
264
+ return None
265
+ payload = dict(row)
266
+ payload["reference"] = reference
267
+ payload["table"] = prefix
268
+ payload["id"] = int(raw_id)
269
+ payload["metadata"] = _load_json(payload.pop("metadata_json", "{}"), {})
270
+ return payload
271
+ if not raw_id.isdigit():
272
+ return None
273
+ row = conn.execute(f"SELECT * FROM {prefix} WHERE id = ?", (int(raw_id),)).fetchone()
274
+ if not row:
275
+ return None
276
+ payload = dict(row)
277
+ payload["reference"] = reference
278
+ payload["table"] = prefix
279
+ payload["id"] = int(raw_id)
280
+ payload["metadata"] = _load_json(payload.pop("metadata_json", "{}"), {})
281
+ return payload
282
+ finally:
283
+ conn.close()
284
+
285
+
286
+ def _hydrate_target(reference: str, depth: int, seen: Set[str]) -> Optional[Dict[str, Any]]:
287
+ if reference in seen:
288
+ return {"reference": reference, "cycle": True}
289
+ return hydrate_reference(reference, depth=depth, _seen=seen)
290
+
291
+
292
+ def hydrate_reference(reference: str, *, depth: int = 1, _seen: Optional[Set[str]] = None) -> Optional[Dict[str, Any]]:
293
+ seen = set(_seen or set())
294
+ if reference in seen:
295
+ return {"reference": reference, "cycle": True}
296
+ seen.add(reference)
297
+ payload = fetch_reference(reference)
298
+ if payload is None:
299
+ return None
300
+
301
+ metadata = payload.get("metadata") if isinstance(payload.get("metadata"), dict) else {}
302
+ preview = preview_from_metadata(metadata)
303
+ links = memory_links.get_memory_links(reference)
304
+ backlinks = memory_links.get_memory_links_for_target(reference)
305
+
306
+ payload["provenance_preview"] = preview
307
+ payload["links"] = links
308
+ payload["backlinks"] = backlinks
309
+ if depth <= 0:
310
+ return payload
311
+
312
+ payload["provenance"] = {
313
+ "outbound": [
314
+ {
315
+ **item,
316
+ "target": _hydrate_target(str(item.get("target_reference") or ""), depth - 1, seen),
317
+ }
318
+ for item in links
319
+ ],
320
+ "inbound": [
321
+ {
322
+ **item,
323
+ "source": _hydrate_target(str(item.get("source_reference") or ""), depth - 1, seen),
324
+ }
325
+ for item in backlinks
326
+ ],
327
+ }
328
+ return payload
329
+
330
+
331
+ def collect_source_references(reference: str, *, depth: int = 2) -> List[str]:
332
+ pending = [reference]
333
+ seen: Set[str] = set()
334
+ collected: List[str] = []
335
+ remaining = max(0, int(depth))
336
+ while pending and remaining >= 0:
337
+ next_round: List[str] = []
338
+ for current in pending:
339
+ if current in seen:
340
+ continue
341
+ seen.add(current)
342
+ collected.append(current)
343
+ row = fetch_reference(current)
344
+ metadata = row.get("metadata") if isinstance((row or {}).get("metadata"), dict) else {}
345
+ preview = preview_from_metadata(metadata)
346
+ for source_ref in preview.get("source_references") or []:
347
+ if source_ref not in seen:
348
+ next_round.append(str(source_ref))
349
+ pending = next_round
350
+ remaining -= 1
351
+ return _dedupe(collected)
352
+
353
+
354
+ def source_references_only(reference: str, *, depth: int = 2) -> List[str]:
355
+ refs = collect_source_references(reference, depth=depth)
356
+ return refs[1:] if len(refs) > 1 else []
@@ -0,0 +1,73 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Dict, Any
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
+ def log_experience(
11
+ task_id: str,
12
+ outcome: str,
13
+ confidence: float,
14
+ reward_score: float,
15
+ memory_reference: str,
16
+ experience_type: str,
17
+ source_module: str,
18
+ ) -> Dict[str, Any]:
19
+ conn = store.connect()
20
+ row = conn.execute(
21
+ "SELECT id FROM experiences WHERE task_id=? AND memory_reference=? AND outcome=?",
22
+ (task_id, memory_reference, outcome),
23
+ ).fetchone()
24
+ if row:
25
+ conn.close()
26
+ emit_event(state_store.reports_dir() / "brain_memory.log.jsonl", "brain_memory_experience_duplicate", status="ok")
27
+ return {"experience_id": row[0], "duplicate": True}
28
+
29
+ cur = conn.execute(
30
+ "INSERT INTO experiences (task_id, outcome, reward_score, confidence, memory_reference, experience_type, source_module, schema_version) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
31
+ (task_id, outcome, reward_score, confidence, memory_reference, experience_type, source_module, store.SCHEMA_VERSION),
32
+ )
33
+ conn.commit()
34
+ conn.close()
35
+ emit_event(state_store.reports_dir() / "brain_memory.log.jsonl", "brain_memory_experience_logged", status="ok")
36
+ return {"experience_id": cur.lastrowid, "duplicate": False, "experience_type": experience_type, "source_module": source_module}
37
+
38
+
39
+ def log_task_execution(
40
+ *,
41
+ task_id: str,
42
+ task_type: str,
43
+ agent_id: str,
44
+ tool_used: str,
45
+ success: bool,
46
+ duration_ms: int,
47
+ ) -> Dict[str, Any]:
48
+ outcome_payload = {
49
+ "task_type": task_type,
50
+ "agent_id": agent_id,
51
+ "tool_used": tool_used,
52
+ "success": bool(success),
53
+ "duration_ms": duration_ms,
54
+ }
55
+ return log_experience(
56
+ task_id=task_id,
57
+ outcome=str(outcome_payload),
58
+ confidence=1.0,
59
+ reward_score=1.0 if success else 0.0,
60
+ memory_reference=f"tool:{tool_used}",
61
+ experience_type="task_execution",
62
+ source_module="task_engine",
63
+ )
64
+
65
+
66
+ def list_recent_experiences(limit: int = 20) -> Dict[str, int]:
67
+ conn = store.connect()
68
+ rows = conn.execute(
69
+ "SELECT experience_type, COUNT(*) as count FROM experiences GROUP BY experience_type ORDER BY count DESC LIMIT ?",
70
+ (limit,),
71
+ ).fetchall()
72
+ conn.close()
73
+ return {row[0]: int(row[1]) for row in rows}