@simbimbo/memory-ocmemog 0.1.10 → 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 +30 -0
  2. package/README.md +85 -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 +311 -23
  80. package/ocmemog/sidecar/compat.py +50 -13
  81. package/ocmemog/sidecar/transcript_watcher.py +391 -190
  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,70 @@
1
+ """Runtime storage path helpers owned by ocmemog."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from pathlib import Path
7
+
8
+
9
+ def _env_path(name: str) -> Path | None:
10
+ raw = os.environ.get(name)
11
+ if not raw:
12
+ return None
13
+ trimmed = raw.strip()
14
+ if not trimmed:
15
+ return None
16
+ return Path(trimmed).expanduser().resolve()
17
+
18
+
19
+ def root_dir() -> Path:
20
+ configured = _env_path("OCMEMOG_STATE_DIR") or _env_path("BRAIN_STATE_DIR")
21
+ if configured:
22
+ base = configured
23
+ else:
24
+ base = Path(__file__).resolve().parents[2] / ".ocmemog-state"
25
+ base.mkdir(parents=True, exist_ok=True)
26
+ return base
27
+
28
+
29
+ def data_dir() -> Path:
30
+ path = root_dir() / "data"
31
+ path.mkdir(parents=True, exist_ok=True)
32
+ return path
33
+
34
+
35
+ def memory_dir() -> Path:
36
+ path = root_dir() / "memory"
37
+ path.mkdir(parents=True, exist_ok=True)
38
+ return path
39
+
40
+
41
+ def reports_dir() -> Path:
42
+ path = root_dir() / "reports"
43
+ path.mkdir(parents=True, exist_ok=True)
44
+ return path
45
+
46
+
47
+ def report_log_path() -> Path:
48
+ override = _env_path("OCMEMOG_REPORT_LOG_PATH") or _env_path("BRAIN_REPORT_LOG_PATH")
49
+ if override:
50
+ override.parent.mkdir(parents=True, exist_ok=True)
51
+ return override
52
+ reports = reports_dir()
53
+ native = reports / "ocmemog_memory.log.jsonl"
54
+ legacy = reports / "brain_memory.log.jsonl"
55
+ if native.exists() or not legacy.exists():
56
+ return native
57
+ return legacy
58
+
59
+
60
+ def memory_db_path() -> Path:
61
+ override = _env_path("OCMEMOG_DB_PATH")
62
+ if override:
63
+ override.parent.mkdir(parents=True, exist_ok=True)
64
+ return override
65
+ memory = memory_dir()
66
+ native = memory / "ocmemog_memory.sqlite3"
67
+ legacy = memory / "brain_memory.sqlite3"
68
+ if native.exists() or not legacy.exists():
69
+ return native
70
+ return legacy
@@ -1,10 +1,14 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import json
4
+ import atexit
5
+ import faulthandler
4
6
  import os
5
7
  import re
6
8
  import threading
7
9
  import time
10
+ import sys
11
+ from contextlib import asynccontextmanager
8
12
  from pathlib import Path
9
13
  from typing import Any, Dict, Iterable, List, Optional
10
14
 
@@ -13,17 +17,124 @@ from fastapi.responses import HTMLResponse, StreamingResponse, JSONResponse
13
17
  from pydantic import BaseModel, Field
14
18
  from datetime import datetime, timedelta
15
19
 
16
- from brain.runtime import state_store
17
- from brain.runtime.memory import api, conversation_state, distill, health, memory_links, pondering_engine, provenance, reinforcement, retrieval, store
20
+ from ocmemog import __version__
21
+ from ocmemog.runtime import state_store
22
+ from ocmemog.runtime.memory import (
23
+ api,
24
+ conversation_state,
25
+ distill,
26
+ health,
27
+ memory_links,
28
+ pondering_engine,
29
+ provenance,
30
+ reinforcement,
31
+ retrieval,
32
+ store,
33
+ )
18
34
  from ocmemog.sidecar.compat import flatten_results, probe_runtime
19
35
  from ocmemog.sidecar.transcript_watcher import watch_forever
20
36
 
21
37
  DEFAULT_CATEGORIES = tuple(store.MEMORY_TABLES)
22
38
 
23
- app = FastAPI(title="ocmemog sidecar", version="0.1.10")
24
-
25
39
  API_TOKEN = os.environ.get("OCMEMOG_API_TOKEN")
26
40
 
41
+
42
+ _BOOL_TRUE_VALUES = {"1", "true", "yes", "on", "y", "t"}
43
+ _BOOL_FALSE_VALUES = {"0", "false", "no", "off", "n", "f"}
44
+
45
+
46
+ def _parse_bool_env_value(raw: Any | None, default: bool = False) -> tuple[bool, bool]:
47
+ """Return ``(value, valid)``, where ``valid`` indicates parser confidence."""
48
+ if raw is None:
49
+ return default, True
50
+
51
+ raw_value = str(raw).strip().lower()
52
+ if raw_value in _BOOL_TRUE_VALUES:
53
+ return True, True
54
+ if raw_value in _BOOL_FALSE_VALUES:
55
+ return False, True
56
+ if not raw_value:
57
+ return default, False
58
+ return default, False
59
+
60
+
61
+ def _parse_bool_env(name: str, default: bool = False) -> bool:
62
+ raw = os.environ.get(name)
63
+ value, _ = _parse_bool_env_value(raw, default=default)
64
+ return value
65
+
66
+
67
+ def _parse_float_env(name: str, default: float, minimum: float | None = None) -> float:
68
+ raw = os.environ.get(name)
69
+ try:
70
+ value = float(raw if raw is not None else default)
71
+ except Exception:
72
+ print(
73
+ f"[ocmemog][config] invalid float env value: {name}={raw!r}; using default {default}",
74
+ file=sys.stderr,
75
+ )
76
+ return default
77
+ if minimum is not None and value < minimum:
78
+ print(
79
+ f"[ocmemog][config] env value below minimum: {name}={value}; using default {default}",
80
+ file=sys.stderr,
81
+ )
82
+ return default
83
+ return value
84
+
85
+
86
+ def _parse_int_env(name: str, default: int, minimum: int | None = None) -> int:
87
+ raw = os.environ.get(name)
88
+ try:
89
+ value = int(raw if raw is not None else default)
90
+ except Exception:
91
+ print(
92
+ f"[ocmemog][config] invalid int env value: {name}={raw!r}; using default {default}",
93
+ file=sys.stderr,
94
+ )
95
+ return default
96
+ if minimum is not None and value < minimum:
97
+ print(
98
+ f"[ocmemog][config] env value below minimum: {name}={value}; using default {default}",
99
+ file=sys.stderr,
100
+ )
101
+ return default
102
+ return value
103
+
104
+
105
+ _SHUTDOWN_TIMING = _parse_bool_env("OCMEMOG_SHUTDOWN_TIMING", default=True)
106
+
107
+
108
+ @asynccontextmanager
109
+ async def _sidecar_lifespan(_: FastAPI):
110
+ _startup_started = time.perf_counter()
111
+ try:
112
+ _start_transcript_watcher()
113
+ _start_ingest_worker()
114
+ if _SHUTDOWN_TIMING:
115
+ print(
116
+ f"[ocmemog][shutdown] lifespan_startup elapsed={time.perf_counter()-_startup_started:.3f}s",
117
+ file=sys.stderr,
118
+ )
119
+ yield
120
+ finally:
121
+ shutdown_started = time.perf_counter()
122
+ _stop_background_workers()
123
+ if _SHUTDOWN_TIMING:
124
+ print(
125
+ f"[ocmemog][shutdown] lifespan_shutdown elapsed={time.perf_counter()-shutdown_started:.3f}s",
126
+ file=sys.stderr,
127
+ )
128
+
129
+
130
+ app = FastAPI(title="ocmemog sidecar", version=__version__, lifespan=_sidecar_lifespan)
131
+
132
+ _INGEST_WORKER_STOP = threading.Event()
133
+ _INGEST_WORKER_THREAD: threading.Thread | None = None
134
+ _INGEST_WORKER_LOCK = threading.Lock()
135
+ _WATCHER_STOP = threading.Event()
136
+ _WATCHER_THREAD: threading.Thread | None = None
137
+ _WATCHER_LOCK = threading.Lock()
27
138
  QUEUE_LOCK = threading.Lock()
28
139
  QUEUE_PROCESS_LOCK = threading.Lock()
29
140
  QUEUE_STATS = {
@@ -33,6 +144,8 @@ QUEUE_STATS = {
33
144
  "last_error": None,
34
145
  "last_batch": 0,
35
146
  }
147
+ _POSTPROCESS_TASK_KEY = "_ocmemog_task"
148
+ _POSTPROCESS_TASK_VALUE = "postprocess_memory"
36
149
 
37
150
 
38
151
  _REFLECTION_RECLASSIFY_PREFERENCE_PATTERNS = (
@@ -100,13 +213,23 @@ async def _auth_middleware(request: Request, call_next):
100
213
  return await call_next(request)
101
214
 
102
215
 
103
- @app.on_event("startup")
104
216
  def _start_transcript_watcher() -> None:
105
- enabled = os.environ.get("OCMEMOG_TRANSCRIPT_WATCHER", "").lower() in {"1", "true", "yes"}
217
+ global _WATCHER_THREAD
218
+ _load_queue_stats()
219
+ enabled = _parse_bool_env("OCMEMOG_TRANSCRIPT_WATCHER")
106
220
  if not enabled:
107
221
  return
108
- thread = threading.Thread(target=watch_forever, daemon=True)
109
- thread.start()
222
+ with _WATCHER_LOCK:
223
+ if _WATCHER_THREAD and _WATCHER_THREAD.is_alive():
224
+ return
225
+ _WATCHER_STOP.clear()
226
+ _WATCHER_THREAD = threading.Thread(
227
+ target=watch_forever,
228
+ args=(_WATCHER_STOP,),
229
+ daemon=True,
230
+ name="ocmemog-transcript-watcher",
231
+ )
232
+ _WATCHER_THREAD.start()
110
233
 
111
234
 
112
235
  def _queue_path() -> Path:
@@ -185,6 +308,24 @@ def _enqueue_payload(payload: Dict[str, Any]) -> int:
185
308
  return _queue_depth()
186
309
 
187
310
 
311
+ def _enqueue_postprocess(reference: str, *, skip_embedding_provider: bool = True) -> int:
312
+ return _enqueue_payload({
313
+ _POSTPROCESS_TASK_KEY: _POSTPROCESS_TASK_VALUE,
314
+ "reference": reference,
315
+ "skip_embedding_provider": bool(skip_embedding_provider),
316
+ })
317
+
318
+
319
+ def _run_postprocess_payload(payload: Dict[str, Any]) -> None:
320
+ reference = str(payload.get("reference") or "").strip()
321
+ if not reference:
322
+ raise ValueError("missing_reference")
323
+ skip_embedding_provider = bool(payload.get("skip_embedding_provider", True))
324
+ result = api.postprocess_stored_memory(reference, skip_embedding_provider=skip_embedding_provider)
325
+ if not result.get("ok"):
326
+ raise RuntimeError(str(result.get("error") or "postprocess_failed"))
327
+
328
+
188
329
 
189
330
  def _process_queue(limit: Optional[int] = None) -> Dict[str, Any]:
190
331
  processed = 0
@@ -207,8 +348,11 @@ def _process_queue(limit: Optional[int] = None) -> Dict[str, Any]:
207
348
  acknowledged = 0
208
349
  for line_no, payload in batch:
209
350
  try:
210
- req = IngestRequest(**payload)
211
- _ingest_request(req)
351
+ if isinstance(payload, dict) and payload.get(_POSTPROCESS_TASK_KEY) == _POSTPROCESS_TASK_VALUE:
352
+ _run_postprocess_payload(payload)
353
+ else:
354
+ req = IngestRequest(**payload)
355
+ _ingest_request(req)
212
356
  processed += 1
213
357
  batch_processed += 1
214
358
  acknowledged = line_no
@@ -239,15 +383,16 @@ def _process_queue(limit: Optional[int] = None) -> Dict[str, Any]:
239
383
 
240
384
 
241
385
  def _ingest_worker() -> None:
242
- enabled = os.environ.get("OCMEMOG_INGEST_ASYNC_WORKER", "true").lower() in {"1", "true", "yes"}
386
+ enabled = _parse_bool_env("OCMEMOG_INGEST_ASYNC_WORKER", default=True)
243
387
  if not enabled:
244
388
  return
245
- poll_seconds = float(os.environ.get("OCMEMOG_INGEST_ASYNC_POLL_SECONDS", "5"))
246
- batch_max = int(os.environ.get("OCMEMOG_INGEST_ASYNC_BATCH_MAX", "25"))
389
+ poll_seconds = _parse_float_env("OCMEMOG_INGEST_ASYNC_POLL_SECONDS", default=5.0, minimum=0.0)
390
+ batch_max = _parse_int_env("OCMEMOG_INGEST_ASYNC_BATCH_MAX", default=25, minimum=1)
247
391
 
248
- while True:
392
+ while not _INGEST_WORKER_STOP.is_set():
249
393
  _process_queue(batch_max)
250
- time.sleep(poll_seconds)
394
+ if _INGEST_WORKER_STOP.wait(poll_seconds):
395
+ break
251
396
 
252
397
 
253
398
 
@@ -255,10 +400,122 @@ def _drain_queue(limit: Optional[int] = None) -> Dict[str, Any]:
255
400
  return _process_queue(limit)
256
401
 
257
402
 
258
- @app.on_event("startup")
259
403
  def _start_ingest_worker() -> None:
260
- thread = threading.Thread(target=_ingest_worker, daemon=True)
261
- thread.start()
404
+ global _INGEST_WORKER_THREAD
405
+ with _INGEST_WORKER_LOCK:
406
+ if _INGEST_WORKER_THREAD and _INGEST_WORKER_THREAD.is_alive():
407
+ return
408
+ _INGEST_WORKER_STOP.clear()
409
+ _INGEST_WORKER_THREAD = threading.Thread(
410
+ target=_ingest_worker,
411
+ daemon=True,
412
+ name="ocmemog-ingest-worker",
413
+ )
414
+ _INGEST_WORKER_THREAD.start()
415
+
416
+
417
+ def _stop_background_workers() -> None:
418
+ global _INGEST_WORKER_THREAD, _WATCHER_THREAD
419
+ shutdown_start = time.perf_counter()
420
+ if _SHUTDOWN_TIMING:
421
+ print(f"[ocmemog][shutdown] shutdown_begin", file=sys.stderr)
422
+ timeout = _parse_float_env(
423
+ "OCMEMOG_WORKER_SHUTDOWN_TIMEOUT_SECONDS",
424
+ default=0.35,
425
+ minimum=0.0,
426
+ )
427
+ if _SHUTDOWN_TIMING:
428
+ print(f"[ocmemog][shutdown] shutdown_config timeout={timeout:.3f}s", file=sys.stderr)
429
+
430
+ queue_drain_requested = _parse_bool_env("OCMEMOG_SHUTDOWN_DRAIN_QUEUE")
431
+ if queue_drain_requested and _queue_depth() > 0:
432
+ _queue_drain_start = time.perf_counter()
433
+ drain_stats = _drain_queue()
434
+ if _SHUTDOWN_TIMING:
435
+ print(
436
+ f"[ocmemog][shutdown] queue_drain elapsed={time.perf_counter()-_queue_drain_start:.3f}s processed={drain_stats.get('processed', 0)} errors={drain_stats.get('errors', 0)}",
437
+ file=sys.stderr,
438
+ )
439
+ _INGEST_WORKER_STOP.set()
440
+ _WATCHER_STOP.set()
441
+ if _SHUTDOWN_TIMING:
442
+ print(
443
+ f"[ocmemog][shutdown] stop_signals_set elapsed={time.perf_counter()-shutdown_start:.3f}s",
444
+ file=sys.stderr,
445
+ )
446
+
447
+ if _parse_bool_env("OCMEMOG_SHUTDOWN_DUMP_THREADS"):
448
+ _dump_thread_dump("post-stop requested")
449
+
450
+ with _INGEST_WORKER_LOCK:
451
+ ingest_worker = _INGEST_WORKER_THREAD
452
+ if ingest_worker is not None and ingest_worker.is_alive():
453
+ ingest_join_start = time.perf_counter()
454
+ ingest_worker.join(timeout=timeout)
455
+ if _SHUTDOWN_TIMING:
456
+ print(
457
+ f"[ocmemog][shutdown] ingest_worker_join elapsed={time.perf_counter()-ingest_join_start:.3f}s alive={ingest_worker.is_alive()}",
458
+ file=sys.stderr,
459
+ )
460
+ if _parse_bool_env("OCMEMOG_SHUTDOWN_DUMP_THREADS"):
461
+ _dump_join_result("ingest-worker", ingest_worker, timeout)
462
+ if not ingest_worker.is_alive():
463
+ with _INGEST_WORKER_LOCK:
464
+ if _INGEST_WORKER_THREAD is ingest_worker:
465
+ _INGEST_WORKER_THREAD = None
466
+
467
+ with _WATCHER_LOCK:
468
+ watcher_thread = _WATCHER_THREAD
469
+ if watcher_thread is not None and watcher_thread.is_alive():
470
+ watcher_join_start = time.perf_counter()
471
+ watcher_thread.join(timeout=timeout)
472
+ if _SHUTDOWN_TIMING:
473
+ print(
474
+ f"[ocmemog][shutdown] transcript_watcher_join elapsed={time.perf_counter()-watcher_join_start:.3f}s alive={watcher_thread.is_alive()}",
475
+ file=sys.stderr,
476
+ )
477
+ if _parse_bool_env("OCMEMOG_SHUTDOWN_DUMP_THREADS"):
478
+ _dump_join_result("transcript-watcher", watcher_thread, timeout)
479
+ if not watcher_thread.is_alive():
480
+ with _WATCHER_LOCK:
481
+ if _WATCHER_THREAD is watcher_thread:
482
+ _WATCHER_THREAD = None
483
+ if _SHUTDOWN_TIMING:
484
+ print(
485
+ f"[ocmemog][shutdown] shutdown_complete elapsed={time.perf_counter()-shutdown_start:.3f}s",
486
+ file=sys.stderr,
487
+ )
488
+
489
+
490
+ def _dump_thread_dump(context: str) -> None:
491
+ print(f"[ocmemog][thread-dump:{context}]", file=sys.stderr)
492
+ _dump_thread_states()
493
+ faulthandler.dump_traceback(file=sys.stderr, all_threads=True)
494
+
495
+
496
+ def _dump_join_result(thread_label: str, thread: threading.Thread, timeout: float) -> None:
497
+ if thread.is_alive():
498
+ print(
499
+ f"[ocmemog][shutdown] {thread_label} still alive after join timeout={timeout:.3f}s",
500
+ file=sys.stderr,
501
+ )
502
+ _dump_thread_dump(thread_label)
503
+ else:
504
+ print(
505
+ f"[ocmemog][shutdown] {thread_label} joined cleanly",
506
+ file=sys.stderr,
507
+ )
508
+
509
+
510
+ def _dump_thread_states() -> None:
511
+ for thread in threading.enumerate():
512
+ print(
513
+ f"[ocmemog][thread-state] name={thread.name} alive={thread.is_alive()} daemon={thread.daemon} ident={thread.ident}",
514
+ file=sys.stderr,
515
+ )
516
+
517
+
518
+ atexit.register(_stop_background_workers)
262
519
 
263
520
 
264
521
  class SearchRequest(BaseModel):
@@ -480,6 +737,8 @@ def _runtime_payload() -> Dict[str, Any]:
480
737
  return {
481
738
  "mode": status.mode,
482
739
  "missingDeps": status.missing_deps,
740
+ "identity": status.identity,
741
+ "capabilities": status.capabilities,
483
742
  "todo": status.todo,
484
743
  "warnings": status.warnings,
485
744
  }
@@ -678,6 +937,7 @@ def _read_transcript_snippet(path: Path, line_start: Optional[int], line_end: Op
678
937
  def healthz() -> dict[str, Any]:
679
938
  payload = _runtime_payload()
680
939
  payload["ok"] = True
940
+ payload["ready"] = payload.get("mode") == "ready"
681
941
  return payload
682
942
 
683
943
 
@@ -685,14 +945,35 @@ def healthz() -> dict[str, Any]:
685
945
  def memory_search(request: SearchRequest) -> dict[str, Any]:
686
946
  categories = _normalize_categories(request.categories)
687
947
  runtime = _runtime_payload()
948
+ started = time.perf_counter()
949
+ query = request.query or ""
950
+ skip_vector_provider = _parse_bool_env("OCMEMOG_SEARCH_SKIP_EMBEDDING_PROVIDER", default=True)
688
951
  try:
689
- results = retrieval.retrieve_for_queries([request.query], limit=request.limit, categories=categories)
952
+ results = retrieval.retrieve_for_queries(
953
+ [query],
954
+ limit=request.limit,
955
+ categories=categories,
956
+ skip_vector_provider=skip_vector_provider,
957
+ )
690
958
  flattened = flatten_results(results)
959
+ if len(flattened) > request.limit:
960
+ flattened = flattened[: request.limit]
691
961
  used_fallback = False
692
962
  except Exception as exc:
693
963
  flattened = _fallback_search(request.query, request.limit, categories)
694
964
  used_fallback = True
695
965
  runtime["warnings"] = [*runtime["warnings"], f"search fallback enabled: {exc}"]
966
+ elapsed_ms = round((time.perf_counter() - started) * 1000, 3)
967
+ if elapsed_ms >= 10:
968
+ print(
969
+ f"[ocmemog][route] memory_search elapsed_ms={elapsed_ms:.3f} limit={request.limit} categories={','.join(categories)} fallback={used_fallback}",
970
+ file=sys.stderr,
971
+ )
972
+ if elapsed_ms >= 200:
973
+ print(
974
+ f"[ocmemog][route] memory_search slow_path query={query[:128]!r} result_count={len(flattened)}",
975
+ file=sys.stderr,
976
+ )
696
977
 
697
978
  return {
698
979
  "ok": True,
@@ -1196,8 +1477,10 @@ def _ingest_request(request: IngestRequest) -> dict[str, Any]:
1196
1477
  source=request.source,
1197
1478
  metadata=metadata,
1198
1479
  timestamp=request.timestamp,
1480
+ post_process=False,
1199
1481
  )
1200
1482
  reference = f"{memory_type}:{memory_id}"
1483
+ _enqueue_postprocess(reference, skip_embedding_provider=_parse_bool_env("OCMEMOG_POSTPROCESS_SKIP_EMBEDDING_PROVIDER", default=True))
1201
1484
  if request.conversation_id:
1202
1485
  memory_links.add_memory_link(reference, "conversation", f"conversation:{request.conversation_id}")
1203
1486
  if request.session_id:
@@ -1294,7 +1577,11 @@ def _ingest_request(request: IngestRequest) -> dict[str, Any]:
1294
1577
 
1295
1578
  @app.post("/memory/ingest")
1296
1579
  def memory_ingest(request: IngestRequest) -> dict[str, Any]:
1297
- return _ingest_request(request)
1580
+ started = time.perf_counter()
1581
+ payload = _ingest_request(request)
1582
+ elapsed_ms = round((time.perf_counter() - started) * 1000, 3)
1583
+ print(f"[ocmemog][route] memory_ingest elapsed_ms={elapsed_ms:.3f} kind={request.kind} reference={payload.get('reference', '')}", file=sys.stderr)
1584
+ return payload
1298
1585
 
1299
1586
 
1300
1587
  @app.post("/memory/ingest_async")
@@ -1368,7 +1655,7 @@ def metrics() -> dict[str, Any]:
1368
1655
 
1369
1656
 
1370
1657
  def _event_stream():
1371
- path = state_store.reports_dir() / "brain_memory.log.jsonl"
1658
+ path = state_store.report_log_path()
1372
1659
  path.parent.mkdir(parents=True, exist_ok=True)
1373
1660
  if not path.exists():
1374
1661
  path.write_text("")
@@ -1388,12 +1675,13 @@ def events() -> StreamingResponse:
1388
1675
 
1389
1676
 
1390
1677
  def _tail_events(limit: int = 50) -> str:
1391
- path = state_store.reports_dir() / "brain_memory.log.jsonl"
1678
+ path = state_store.report_log_path()
1392
1679
  if not path.exists():
1393
1680
  return ""
1394
1681
  try:
1395
1682
  lines = path.read_text(encoding="utf-8", errors="ignore").splitlines()
1396
- except Exception:
1683
+ except Exception as exc:
1684
+ print(f"[ocmemog][events] tail_read_failed path={path} error={exc!r}", file=sys.stderr)
1397
1685
  return ""
1398
1686
  return "\n".join(lines[-limit:])
1399
1687
 
@@ -6,6 +6,8 @@ import os
6
6
  from dataclasses import dataclass
7
7
  from typing import Any
8
8
 
9
+ from ocmemog.runtime import identity
10
+
9
11
 
10
12
  @dataclass(frozen=True)
11
13
  class RuntimeStatus:
@@ -13,45 +15,80 @@ class RuntimeStatus:
13
15
  missing_deps: list[str]
14
16
  todo: list[str]
15
17
  warnings: list[str]
18
+ identity: dict[str, Any]
19
+ capabilities: list[dict[str, Any]]
16
20
 
17
21
 
18
22
  TODO_ITEMS = [
19
- "Add a role registry (brain.runtime.roles) if you want role-prioritized context building.",
20
23
  "Add non-OpenAI embedding providers if required.",
21
24
  ]
22
25
 
26
+ _EMBEDDING_PROVIDER_BACKEND_HINTS = {
27
+ "openai",
28
+ "openai_compatible",
29
+ "openai-compatible",
30
+ "local-openai",
31
+ "local_openai",
32
+ "llamacpp",
33
+ "llama.cpp",
34
+ "ollama",
35
+ "local-ollama",
36
+ }
37
+
23
38
 
24
39
  def probe_runtime() -> RuntimeStatus:
40
+ runtime_identity = identity.get_runtime_identity()
41
+ capabilities = runtime_identity.get("capabilities", [])
42
+
25
43
  missing_deps: list[str] = []
26
44
  warnings: list[str] = []
27
45
 
28
46
  for module_name in (
29
- "brain.runtime.memory.store",
30
- "brain.runtime.memory.retrieval",
31
- "brain.runtime.memory.vector_index",
32
- "brain.runtime.memory.memory_links",
47
+ "ocmemog.runtime.memory.store",
48
+ "ocmemog.runtime.memory.retrieval",
49
+ "ocmemog.runtime.memory.vector_index",
50
+ "ocmemog.runtime.memory.memory_links",
33
51
  ):
34
52
  try:
35
53
  importlib.import_module(module_name)
36
54
  except Exception as exc:
37
55
  missing_deps.append(f"{module_name}: {exc}")
38
56
 
39
- provider = os.environ.get("BRAIN_EMBED_MODEL_PROVIDER", "").strip().lower()
40
- if importlib.util.find_spec("sentence_transformers") is None and provider not in {"ollama", "openai", "openai_compatible", "openai-compatible", "local-ollama"}:
57
+ provider = (
58
+ os.environ.get("OCMEMOG_EMBED_MODEL_PROVIDER")
59
+ or os.environ.get("OCMEMOG_EMBED_PROVIDER", "")
60
+ or os.environ.get("BRAIN_EMBED_MODEL_PROVIDER", "")
61
+ ).strip().lower()
62
+ if importlib.util.find_spec("sentence_transformers") is None and provider not in _EMBEDDING_PROVIDER_BACKEND_HINTS:
41
63
  warnings.append("Optional dependency missing: sentence-transformers; using local hash embeddings.")
42
64
 
43
65
  try:
44
- from brain.runtime import inference, providers
66
+ from ocmemog.runtime import inference, providers
45
67
 
46
68
  if getattr(inference, "__shim__", False):
47
- missing_deps.append("brain.runtime.inference (shim only)")
69
+ missing_deps.append("ocmemog.runtime.inference (shim only)")
48
70
  if getattr(getattr(providers, "provider_execute", None), "__shim__", False):
49
- missing_deps.append("brain.runtime.providers.provider_execute (shim only)")
71
+ missing_deps.append("ocmemog.runtime.providers.provider_execute (shim only)")
50
72
  except Exception as exc:
51
- missing_deps.append(f"brain.runtime compatibility probe failed: {exc}")
73
+ missing_deps.append(f"ocmemog.runtime compatibility probe failed: {exc}")
74
+
75
+ shim_count = sum(1 for item in capabilities if item.get("owner") == "brain-runtime-shim")
76
+ if shim_count:
77
+ warnings.append(f"Runtime still relies on {shim_count} legacy compatibility surface(s).")
78
+ mode = "degraded"
79
+ else:
80
+ mode = "ready"
52
81
 
53
- mode = "degraded" if missing_deps else "ready"
54
- return RuntimeStatus(mode=mode, missing_deps=missing_deps, todo=list(TODO_ITEMS), warnings=warnings)
82
+ if missing_deps:
83
+ mode = "degraded"
84
+ return RuntimeStatus(
85
+ mode=mode,
86
+ missing_deps=missing_deps,
87
+ todo=list(TODO_ITEMS),
88
+ warnings=warnings,
89
+ identity=runtime_identity,
90
+ capabilities=capabilities,
91
+ )
55
92
 
56
93
 
57
94
  def flatten_results(results: dict[str, list[dict[str, Any]]]) -> list[dict[str, Any]]: