@simbimbo/memory-ocmemog 0.1.11 → 0.1.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/CHANGELOG.md +30 -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 +34 -0
  45. package/ocmemog/runtime/identity.py +115 -0
  46. package/ocmemog/runtime/inference.py +163 -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 +1594 -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 +93 -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 +15 -0
  72. package/ocmemog/runtime/model_router.py +28 -0
  73. package/ocmemog/runtime/providers.py +78 -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 +32 -0
  78. package/ocmemog/runtime/storage_paths.py +70 -0
  79. package/ocmemog/sidecar/app.py +421 -60
  80. package/ocmemog/sidecar/compat.py +50 -13
  81. package/ocmemog/sidecar/transcript_watcher.py +327 -242
  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 +374 -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
@@ -1,279 +1,3 @@
1
1
  from __future__ import annotations
2
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
- _PREFERENCE_PATTERNS = (
15
- "prefer",
16
- "preference",
17
- "favorite",
18
- "favourite",
19
- "likes",
20
- "like ",
21
- "loves",
22
- "enjoys",
23
- "dislikes",
24
- "hate ",
25
- "avoids",
26
- )
27
- _IDENTITY_PATTERNS = (
28
- "my name is",
29
- "i am ",
30
- "i'm ",
31
- "my pronouns are",
32
- "i live in",
33
- "we live in",
34
- "i work at",
35
- "we work at",
36
- "i study at",
37
- "we study at",
38
- "my timezone is",
39
- "my time zone is",
40
- "my email is",
41
- "my phone number is",
42
- "my birthday is",
43
- "allergic to",
44
- )
45
-
46
-
47
- def _should_promote(confidence: float, threshold: float | None = None) -> bool:
48
- threshold = config.OCMEMOG_PROMOTION_THRESHOLD if threshold is None else threshold
49
- return confidence >= float(threshold)
50
-
51
-
52
- def _destination_table(summary: str) -> str:
53
- lowered = summary.lower()
54
- if "runbook" in lowered or "procedure" in lowered or "steps" in lowered:
55
- return "runbooks"
56
- if "lesson" in lowered or "postmortem" in lowered or "learned" in lowered:
57
- return "lessons"
58
- if any(pattern in lowered for pattern in _PREFERENCE_PATTERNS):
59
- return "preferences"
60
- if any(pattern in lowered for pattern in _IDENTITY_PATTERNS):
61
- return "identity"
62
- return "knowledge"
63
-
64
-
65
- def promote_candidate(candidate: Dict[str, Any]) -> Dict[str, Any]:
66
- emit_event(LOGFILE, "brain_memory_promote_start", status="ok")
67
- confidence = float(candidate.get("confidence_score", 0.0))
68
- decision = "promote" if _should_promote(confidence) else "reject"
69
- candidate_id = str(candidate.get("candidate_id") or "")
70
-
71
- candidate_metadata = provenance.normalize_metadata(candidate.get("metadata", {}), source="promote")
72
- candidate_metadata["candidate_id"] = candidate_id
73
- candidate_metadata["derived_from_candidate_id"] = candidate_id
74
- candidate_metadata["derived_via"] = "promotion"
75
-
76
- conn = store.connect()
77
- promotion_id = None
78
- destination = _destination_table(str(candidate.get("distilled_summary", "")))
79
- if decision == "promote":
80
- row = conn.execute(
81
- "SELECT id FROM promotions WHERE source=? AND content=?",
82
- (str(candidate.get("source_event_id")), candidate.get("distilled_summary", "")),
83
- ).fetchone()
84
- if not row:
85
- cur = conn.execute(
86
- """
87
- INSERT INTO promotions (
88
- candidate_id, source, confidence, status, decision_reason,
89
- metadata_json, content, schema_version
90
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
91
- """,
92
- (
93
- candidate_id,
94
- str(candidate.get("source_event_id")),
95
- confidence,
96
- "promoted",
97
- "confidence_threshold",
98
- json.dumps(candidate_metadata, ensure_ascii=False),
99
- candidate.get("distilled_summary", ""),
100
- store.SCHEMA_VERSION,
101
- ),
102
- )
103
- memory_rec = conn.execute(
104
- f"INSERT INTO {destination} (source, confidence, metadata_json, content, schema_version) VALUES (?, ?, ?, ?, ?)",
105
- (
106
- str(candidate.get("source_event_id")),
107
- confidence,
108
- json.dumps(candidate_metadata, ensure_ascii=False),
109
- candidate.get("distilled_summary", ""),
110
- store.SCHEMA_VERSION,
111
- ),
112
- )
113
- conn.execute(
114
- "UPDATE candidates SET status='promoted', updated_at=datetime('now') WHERE candidate_id=?",
115
- (candidate_id,),
116
- )
117
- conn.execute(
118
- "INSERT INTO memory_events (event_type, source, details_json, schema_version) VALUES (?, ?, ?, ?)",
119
- (
120
- "candidate_promoted",
121
- str(candidate.get("source_event_id")),
122
- json.dumps({"candidate_id": candidate_id, "promotion_table": destination}),
123
- store.SCHEMA_VERSION,
124
- ),
125
- )
126
- conn.commit()
127
- promotion_id = cur.lastrowid
128
- memory_id = memory_rec.lastrowid
129
- else:
130
- promotion_id = row[0]
131
- memory_id = None
132
- emit_event(LOGFILE, "brain_memory_promote_success", status="ok", destination=destination)
133
- else:
134
- conn.execute(
135
- "UPDATE candidates SET status='rejected', updated_at=datetime('now') WHERE candidate_id=?",
136
- (candidate_id,),
137
- )
138
- conn.execute(
139
- "INSERT INTO promotions (candidate_id, source, confidence, status, decision_reason, metadata_json, content, schema_version) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
140
- (
141
- candidate_id,
142
- str(candidate.get("source_event_id")),
143
- confidence,
144
- "rejected",
145
- "below_threshold",
146
- json.dumps(candidate_metadata, ensure_ascii=False),
147
- candidate.get("distilled_summary", ""),
148
- store.SCHEMA_VERSION,
149
- ),
150
- )
151
- conn.commit()
152
- emit_event(LOGFILE, "brain_memory_promote_rejected", status="ok")
153
- memory_id = None
154
- conn.close()
155
-
156
- if decision == "promote" and promotion_id is not None:
157
- from brain.runtime.memory import reinforcement, vector_index
158
-
159
- promoted_reference = f"{destination}:{memory_id}" if memory_id else ""
160
- promotion_updates = {
161
- **candidate_metadata,
162
- "promotion_id": promotion_id,
163
- "derived_from_promotion_id": promotion_id,
164
- }
165
- if promoted_reference:
166
- provenance.update_memory_metadata(promoted_reference, promotion_updates)
167
- reinforcement.log_experience(
168
- task_id=str(candidate.get("candidate_id") or candidate.get("source_event_id") or ""),
169
- outcome="promoted",
170
- confidence=confidence,
171
- reward_score=confidence,
172
- memory_reference=promoted_reference or f"promotions:{promotion_id}",
173
- experience_type="promotion",
174
- source_module="memory_promote",
175
- )
176
- emit_event(LOGFILE, "brain_memory_reinforcement_created", status="ok")
177
- if memory_id:
178
- vector_index.insert_memory(memory_id, candidate.get("distilled_summary", ""), confidence, source_type=destination)
179
- try:
180
- from brain.runtime.memory import api as memory_api
181
-
182
- memory_api._auto_attach_governance_candidates(promoted_reference)
183
- except Exception as exc:
184
- emit_event(LOGFILE, "brain_memory_promotion_governance_failed", status="error", error=str(exc), reference=promoted_reference)
185
-
186
- return {"decision": decision, "confidence": confidence, "promotion_id": promotion_id, "destination": destination}
187
-
188
-
189
- def promote_candidate_by_id(candidate_id: str) -> Dict[str, Any]:
190
- conn = store.connect()
191
- row = conn.execute(
192
- """
193
- SELECT candidate_id, source_event_id, distilled_summary, verification_points,
194
- confidence_score, metadata_json
195
- FROM candidates WHERE candidate_id=?
196
- """,
197
- (candidate_id,),
198
- ).fetchone()
199
- conn.close()
200
- if not row:
201
- emit_event(LOGFILE, "brain_memory_promote_error", status="error")
202
- return {"decision": "error", "reason": "candidate_not_found"}
203
- payload = dict(row)
204
- try:
205
- payload["metadata"] = json.loads(payload.get("metadata_json") or "{}")
206
- except Exception:
207
- payload["metadata"] = {}
208
- return promote_candidate(payload)
209
-
210
-
211
- def demote_memory(reference: str, reason: str = "low_confidence", new_confidence: float = 0.1) -> Dict[str, Any]:
212
- table, sep, raw_id = reference.partition(":")
213
- if not sep or not raw_id.isdigit():
214
- return {"ok": False, "error": "invalid_reference"}
215
- allowed = set(store.MEMORY_TABLES)
216
- if table not in allowed:
217
- return {"ok": False, "error": "unsupported_table"}
218
- conn = store.connect()
219
- row = conn.execute(
220
- f"SELECT confidence, content, metadata_json FROM {table} WHERE id=?",
221
- (int(raw_id),),
222
- ).fetchone()
223
- if not row:
224
- conn.close()
225
- return {"ok": False, "error": "not_found"}
226
- previous = float(row[0] or 0.0)
227
- content = str(row[1] or "")
228
- metadata_json = row[2] or "{}"
229
-
230
- # archive into cold storage, then remove from hot table
231
- conn.execute(
232
- "INSERT INTO cold_storage (source_table, source_id, content, metadata_json, reason, schema_version) VALUES (?, ?, ?, ?, ?, ?)",
233
- (table, int(raw_id), content, metadata_json, reason, store.SCHEMA_VERSION),
234
- )
235
- conn.execute(
236
- f"DELETE FROM {table} WHERE id=?",
237
- (int(raw_id),),
238
- )
239
- conn.execute(
240
- "INSERT INTO demotions (memory_reference, previous_confidence, new_confidence, reason, schema_version) VALUES (?, ?, ?, ?, ?)",
241
- (reference, previous, float(new_confidence), reason, store.SCHEMA_VERSION),
242
- )
243
- conn.commit()
244
- conn.close()
245
- emit_event(LOGFILE, "brain_memory_demoted", status="ok", reference=reference)
246
- return {"ok": True, "reference": reference, "previous": previous, "new": float(new_confidence), "archived": True}
247
-
248
-
249
- def demote_by_confidence(limit: int = 20, threshold: float | None = None, force: bool = False) -> Dict[str, Any]:
250
- threshold = config.OCMEMOG_DEMOTION_THRESHOLD if threshold is None else threshold
251
- tables = tuple(store.MEMORY_TABLES)
252
- conn = store.connect()
253
- rows = []
254
- for table in tables:
255
- try:
256
- rows.extend(
257
- conn.execute(
258
- f"SELECT '{table}' AS table_name, id, confidence FROM {table} ORDER BY confidence ASC LIMIT ?",
259
- (limit,),
260
- ).fetchall()
261
- )
262
- except Exception:
263
- continue
264
- conn.close()
265
- # sort by confidence and demote below threshold
266
- ranked = sorted(rows, key=lambda r: float(r[2] or 0.0))
267
- demoted = []
268
- for row in ranked:
269
- table = row[0]
270
- memory_id = int(row[1])
271
- confidence = float(row[2] or 0.0)
272
- if confidence >= float(threshold) and not force:
273
- continue
274
- result = demote_memory(f"{table}:{memory_id}", reason="low_confidence", new_confidence=confidence * 0.5)
275
- if result.get("ok"):
276
- demoted.append(result)
277
- if len(demoted) >= limit:
278
- break
279
- return {"ok": True, "threshold": float(threshold), "demoted": demoted, "count": len(demoted)}
3
+ from ocmemog.runtime.memory.promote import * # noqa: F401,F403
@@ -1,408 +1,3 @@
1
1
  from __future__ import annotations
2
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 = set(store.MEMORY_TABLES)
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
- "memory_status",
110
- "superseded_by",
111
- "supersedes",
112
- "duplicate_of",
113
- "duplicate_candidates",
114
- "contradicts",
115
- "contradiction_candidates",
116
- "contradiction_status",
117
- "canonical_reference",
118
- "supersession_recommendation",
119
- ):
120
- if raw.get(key) is not None and provenance.get(key) is None:
121
- provenance[key] = raw.get(key)
122
-
123
- if provenance:
124
- raw["provenance"] = provenance
125
- if source_references:
126
- raw["source_reference"] = source_references[0]
127
- raw["source_references"] = source_references
128
- if source_labels:
129
- raw["source_labels"] = source_labels
130
- raw.setdefault("source_label", source_labels[0])
131
- return raw
132
-
133
-
134
- def preview_from_metadata(metadata: Optional[Dict[str, Any]]) -> Dict[str, Any]:
135
- normalized = normalize_metadata(metadata)
136
- provenance = normalized.get("provenance") if isinstance(normalized.get("provenance"), dict) else {}
137
- return {
138
- "source_references": provenance.get("source_references") or [],
139
- "source_labels": provenance.get("source_labels") or [],
140
- "conversation": provenance.get("conversation") or {},
141
- "transcript_anchor": provenance.get("transcript_anchor") or None,
142
- "origin_source": provenance.get("origin_source"),
143
- "derived_via": provenance.get("derived_via"),
144
- }
145
-
146
-
147
- def _link_once(source_reference: str, link_type: str, target_reference: str) -> None:
148
- if not source_reference or not target_reference:
149
- return
150
- existing = memory_links.get_memory_links(source_reference)
151
- if any(item.get("link_type") == link_type and item.get("target_reference") == target_reference for item in existing):
152
- return
153
- memory_links.add_memory_link(source_reference, link_type, target_reference)
154
-
155
-
156
- def apply_links(reference: str, metadata: Optional[Dict[str, Any]]) -> None:
157
- normalized = normalize_metadata(metadata)
158
- provenance = normalized.get("provenance") if isinstance(normalized.get("provenance"), dict) else {}
159
- for source_reference in provenance.get("source_references") or []:
160
- _link_once(reference, "derived_from", str(source_reference))
161
- for label in provenance.get("source_labels") or []:
162
- _link_once(reference, "source_label", f"label:{label}")
163
- conversation = provenance.get("conversation") or {}
164
- for key, link_type in (
165
- ("conversation_id", "conversation"),
166
- ("session_id", "session"),
167
- ("thread_id", "thread"),
168
- ("message_id", "message"),
169
- ):
170
- value = str(conversation.get(key) or "").strip()
171
- if value:
172
- _link_once(reference, link_type, f"{link_type}:{value}")
173
- transcript = provenance.get("transcript_anchor") or {}
174
- if transcript.get("path"):
175
- _link_once(
176
- reference,
177
- "transcript",
178
- _transcript_target(
179
- str(transcript.get("path")),
180
- transcript.get("start_line"),
181
- transcript.get("end_line"),
182
- ),
183
- )
184
- if provenance.get("experience_reference"):
185
- _link_once(reference, "experience", str(provenance.get("experience_reference")))
186
- if provenance.get("derived_from_candidate_id"):
187
- _link_once(reference, "candidate", f"candidate:{provenance['derived_from_candidate_id']}")
188
- if provenance.get("derived_from_promotion_id"):
189
- _link_once(reference, "promotion", f"promotions:{provenance['derived_from_promotion_id']}")
190
- if provenance.get("superseded_by"):
191
- _link_once(reference, "superseded_by", str(provenance.get("superseded_by")))
192
- if provenance.get("supersedes"):
193
- _link_once(reference, "supersedes", str(provenance.get("supersedes")))
194
- if provenance.get("duplicate_of"):
195
- _link_once(reference, "duplicate_of", str(provenance.get("duplicate_of")))
196
- for candidate in provenance.get("duplicate_candidates") or []:
197
- _link_once(reference, "duplicate_candidate", str(candidate))
198
- for target in provenance.get("contradicts") or []:
199
- _link_once(reference, "contradicts", str(target))
200
- for target in provenance.get("contradiction_candidates") or []:
201
- _link_once(reference, "contradiction_candidate", str(target))
202
- if provenance.get("canonical_reference"):
203
- _link_once(reference, "canonical", str(provenance.get("canonical_reference")))
204
-
205
-
206
- def update_memory_metadata(reference: str, updates: Dict[str, Any]) -> Optional[Dict[str, Any]]:
207
- table, sep, raw_id = reference.partition(":")
208
- if not sep or table not in _MEMORY_TABLES or not raw_id.isdigit():
209
- return None
210
- conn = store.connect()
211
- try:
212
- row = conn.execute(f"SELECT metadata_json FROM {table} WHERE id = ?", (int(raw_id),)).fetchone()
213
- if not row:
214
- return None
215
- current = _load_json(row["metadata_json"], {})
216
- merged = normalize_metadata({**current, **updates})
217
- conn.execute(
218
- f"UPDATE {table} SET metadata_json = ? WHERE id = ?",
219
- (json.dumps(merged, ensure_ascii=False), int(raw_id)),
220
- )
221
- conn.commit()
222
- finally:
223
- conn.close()
224
- apply_links(reference, merged)
225
- return merged
226
-
227
-
228
- def force_update_memory_metadata(reference: str, updates: Dict[str, Any]) -> Optional[Dict[str, Any]]:
229
- table, sep, raw_id = reference.partition(":")
230
- if not sep or table not in _MEMORY_TABLES or not raw_id.isdigit():
231
- return None
232
- conn = store.connect()
233
- try:
234
- row = conn.execute(f"SELECT metadata_json FROM {table} WHERE id = ?", (int(raw_id),)).fetchone()
235
- if not row:
236
- return None
237
- current = _load_json(row["metadata_json"], {})
238
- provenance_meta = current.get("provenance") if isinstance(current.get("provenance"), dict) else {}
239
- for key, value in updates.items():
240
- if value is None or value == "":
241
- provenance_meta.pop(key, None)
242
- else:
243
- provenance_meta[key] = value
244
- current["provenance"] = provenance_meta
245
- conn.execute(
246
- f"UPDATE {table} SET metadata_json = ? WHERE id = ?",
247
- (json.dumps(current, ensure_ascii=False), int(raw_id)),
248
- )
249
- conn.commit()
250
- finally:
251
- conn.close()
252
- apply_links(reference, current)
253
- return current
254
-
255
-
256
- def fetch_reference(reference: str) -> Optional[Dict[str, Any]]:
257
- prefix, sep, raw_id = reference.partition(":")
258
- if not sep or not prefix:
259
- return None
260
- if prefix in _SYNTHETIC_PREFIXES:
261
- payload: Dict[str, Any] = {"reference": reference, "type": prefix, "value": raw_id}
262
- if prefix == "transcript":
263
- payload["path"] = raw_id
264
- return payload
265
- if prefix == "candidate":
266
- return {"reference": reference, "type": "candidate", "candidate_id": raw_id}
267
- if prefix not in _FETCHABLE_TABLES:
268
- return None
269
-
270
- conn = store.connect()
271
- try:
272
- if prefix == "conversation_turns":
273
- if not raw_id.isdigit():
274
- return None
275
- row = conn.execute("SELECT * FROM conversation_turns WHERE id = ?", (int(raw_id),)).fetchone()
276
- if not row:
277
- return None
278
- payload = dict(row)
279
- payload["reference"] = reference
280
- payload["table"] = prefix
281
- payload["id"] = int(raw_id)
282
- payload["metadata"] = _load_json(payload.pop("metadata_json", "{}"), {})
283
- return payload
284
- if prefix == "conversation_checkpoints":
285
- if not raw_id.isdigit():
286
- return None
287
- row = conn.execute("SELECT * FROM conversation_checkpoints WHERE id = ?", (int(raw_id),)).fetchone()
288
- if not row:
289
- return None
290
- payload = dict(row)
291
- payload["reference"] = reference
292
- payload["table"] = prefix
293
- payload["id"] = int(raw_id)
294
- payload["metadata"] = _load_json(payload.pop("metadata_json", "{}"), {})
295
- payload["open_loops"] = _load_json(payload.pop("open_loops_json", "[]"), [])
296
- payload["pending_actions"] = _load_json(payload.pop("pending_actions_json", "[]"), [])
297
- return payload
298
- if prefix == "experiences":
299
- if not raw_id.isdigit():
300
- return None
301
- row = conn.execute("SELECT * FROM experiences WHERE id = ?", (int(raw_id),)).fetchone()
302
- if not row:
303
- return None
304
- payload = dict(row)
305
- payload["reference"] = reference
306
- payload["table"] = prefix
307
- payload["id"] = int(raw_id)
308
- payload["content"] = payload.get("outcome")
309
- payload["metadata"] = _load_json(payload.pop("metadata_json", "{}"), {})
310
- return payload
311
- if prefix == "promotions":
312
- if not raw_id.isdigit():
313
- return None
314
- row = conn.execute("SELECT * FROM promotions WHERE id = ?", (int(raw_id),)).fetchone()
315
- if not row:
316
- return None
317
- payload = dict(row)
318
- payload["reference"] = reference
319
- payload["table"] = prefix
320
- payload["id"] = int(raw_id)
321
- payload["metadata"] = _load_json(payload.pop("metadata_json", "{}"), {})
322
- return payload
323
- if not raw_id.isdigit():
324
- return None
325
- row = conn.execute(f"SELECT * FROM {prefix} WHERE id = ?", (int(raw_id),)).fetchone()
326
- if not row:
327
- return None
328
- payload = dict(row)
329
- payload["reference"] = reference
330
- payload["table"] = prefix
331
- payload["id"] = int(raw_id)
332
- payload["metadata"] = _load_json(payload.pop("metadata_json", "{}"), {})
333
- return payload
334
- finally:
335
- conn.close()
336
-
337
-
338
- def _hydrate_target(reference: str, depth: int, seen: Set[str]) -> Optional[Dict[str, Any]]:
339
- if reference in seen:
340
- return {"reference": reference, "cycle": True}
341
- return hydrate_reference(reference, depth=depth, _seen=seen)
342
-
343
-
344
- def hydrate_reference(reference: str, *, depth: int = 1, _seen: Optional[Set[str]] = None) -> Optional[Dict[str, Any]]:
345
- seen = set(_seen or set())
346
- if reference in seen:
347
- return {"reference": reference, "cycle": True}
348
- seen.add(reference)
349
- payload = fetch_reference(reference)
350
- if payload is None:
351
- return None
352
-
353
- metadata = payload.get("metadata") if isinstance(payload.get("metadata"), dict) else {}
354
- preview = preview_from_metadata(metadata)
355
- links = memory_links.get_memory_links(reference)
356
- backlinks = memory_links.get_memory_links_for_target(reference)
357
-
358
- payload["provenance_preview"] = preview
359
- payload["links"] = links
360
- payload["backlinks"] = backlinks
361
- if depth <= 0:
362
- return payload
363
-
364
- payload["provenance"] = {
365
- "outbound": [
366
- {
367
- **item,
368
- "target": _hydrate_target(str(item.get("target_reference") or ""), depth - 1, seen),
369
- }
370
- for item in links
371
- ],
372
- "inbound": [
373
- {
374
- **item,
375
- "source": _hydrate_target(str(item.get("source_reference") or ""), depth - 1, seen),
376
- }
377
- for item in backlinks
378
- ],
379
- }
380
- return payload
381
-
382
-
383
- def collect_source_references(reference: str, *, depth: int = 2) -> List[str]:
384
- pending = [reference]
385
- seen: Set[str] = set()
386
- collected: List[str] = []
387
- remaining = max(0, int(depth))
388
- while pending and remaining >= 0:
389
- next_round: List[str] = []
390
- for current in pending:
391
- if current in seen:
392
- continue
393
- seen.add(current)
394
- collected.append(current)
395
- row = fetch_reference(current)
396
- metadata = row.get("metadata") if isinstance((row or {}).get("metadata"), dict) else {}
397
- preview = preview_from_metadata(metadata)
398
- for source_ref in preview.get("source_references") or []:
399
- if source_ref not in seen:
400
- next_round.append(str(source_ref))
401
- pending = next_round
402
- remaining -= 1
403
- return _dedupe(collected)
404
-
405
-
406
- def source_references_only(reference: str, *, depth: int = 2) -> List[str]:
407
- refs = collect_source_references(reference, depth=depth)
408
- return refs[1:] if len(refs) > 1 else []
3
+ from ocmemog.runtime.memory.provenance import * # noqa: F401,F403