@simbimbo/memory-ocmemog 0.1.11 → 0.1.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +83 -18
  3. package/brain/runtime/__init__.py +2 -12
  4. package/brain/runtime/config.py +1 -24
  5. package/brain/runtime/inference.py +1 -151
  6. package/brain/runtime/instrumentation.py +1 -15
  7. package/brain/runtime/memory/__init__.py +3 -13
  8. package/brain/runtime/memory/api.py +1 -1219
  9. package/brain/runtime/memory/candidate.py +1 -185
  10. package/brain/runtime/memory/conversation_state.py +1 -1823
  11. package/brain/runtime/memory/distill.py +1 -344
  12. package/brain/runtime/memory/embedding_engine.py +1 -92
  13. package/brain/runtime/memory/freshness.py +1 -112
  14. package/brain/runtime/memory/health.py +1 -40
  15. package/brain/runtime/memory/integrity.py +1 -186
  16. package/brain/runtime/memory/memory_consolidation.py +1 -58
  17. package/brain/runtime/memory/memory_links.py +1 -107
  18. package/brain/runtime/memory/memory_salience.py +1 -233
  19. package/brain/runtime/memory/memory_synthesis.py +1 -31
  20. package/brain/runtime/memory/memory_taxonomy.py +1 -33
  21. package/brain/runtime/memory/pondering_engine.py +1 -654
  22. package/brain/runtime/memory/promote.py +1 -277
  23. package/brain/runtime/memory/provenance.py +1 -406
  24. package/brain/runtime/memory/reinforcement.py +1 -71
  25. package/brain/runtime/memory/retrieval.py +1 -210
  26. package/brain/runtime/memory/semantic_search.py +1 -64
  27. package/brain/runtime/memory/store.py +1 -429
  28. package/brain/runtime/memory/unresolved_state.py +1 -91
  29. package/brain/runtime/memory/vector_index.py +1 -323
  30. package/brain/runtime/model_roles.py +1 -9
  31. package/brain/runtime/model_router.py +1 -22
  32. package/brain/runtime/providers.py +1 -66
  33. package/brain/runtime/security/redaction.py +1 -12
  34. package/brain/runtime/state_store.py +1 -23
  35. package/brain/runtime/storage_paths.py +1 -39
  36. package/docs/architecture/memory.md +20 -24
  37. package/docs/release-checklist.md +19 -6
  38. package/docs/usage.md +33 -17
  39. package/index.ts +8 -1
  40. package/ocmemog/__init__.py +11 -0
  41. package/ocmemog/doctor.py +1255 -0
  42. package/ocmemog/runtime/__init__.py +18 -0
  43. package/ocmemog/runtime/_compat_bridge.py +28 -0
  44. package/ocmemog/runtime/config.py +35 -0
  45. package/ocmemog/runtime/identity.py +115 -0
  46. package/ocmemog/runtime/inference.py +164 -0
  47. package/ocmemog/runtime/instrumentation.py +20 -0
  48. package/ocmemog/runtime/memory/__init__.py +91 -0
  49. package/ocmemog/runtime/memory/api.py +1431 -0
  50. package/ocmemog/runtime/memory/candidate.py +192 -0
  51. package/ocmemog/runtime/memory/conversation_state.py +1831 -0
  52. package/ocmemog/runtime/memory/distill.py +282 -0
  53. package/ocmemog/runtime/memory/embedding_engine.py +151 -0
  54. package/ocmemog/runtime/memory/freshness.py +114 -0
  55. package/ocmemog/runtime/memory/health.py +57 -0
  56. package/ocmemog/runtime/memory/integrity.py +208 -0
  57. package/ocmemog/runtime/memory/memory_consolidation.py +60 -0
  58. package/ocmemog/runtime/memory/memory_links.py +109 -0
  59. package/ocmemog/runtime/memory/memory_salience.py +235 -0
  60. package/ocmemog/runtime/memory/memory_synthesis.py +33 -0
  61. package/ocmemog/runtime/memory/memory_taxonomy.py +35 -0
  62. package/ocmemog/runtime/memory/pondering_engine.py +681 -0
  63. package/ocmemog/runtime/memory/promote.py +279 -0
  64. package/ocmemog/runtime/memory/provenance.py +408 -0
  65. package/ocmemog/runtime/memory/reinforcement.py +73 -0
  66. package/ocmemog/runtime/memory/retrieval.py +224 -0
  67. package/ocmemog/runtime/memory/semantic_search.py +66 -0
  68. package/ocmemog/runtime/memory/store.py +433 -0
  69. package/ocmemog/runtime/memory/unresolved_state.py +93 -0
  70. package/ocmemog/runtime/memory/vector_index.py +411 -0
  71. package/ocmemog/runtime/model_roles.py +16 -0
  72. package/ocmemog/runtime/model_router.py +29 -0
  73. package/ocmemog/runtime/providers.py +79 -0
  74. package/ocmemog/runtime/roles.py +92 -0
  75. package/ocmemog/runtime/security/__init__.py +8 -0
  76. package/ocmemog/runtime/security/redaction.py +17 -0
  77. package/ocmemog/runtime/state_store.py +34 -0
  78. package/ocmemog/runtime/storage_paths.py +70 -0
  79. package/ocmemog/sidecar/app.py +310 -23
  80. package/ocmemog/sidecar/compat.py +50 -13
  81. package/ocmemog/sidecar/transcript_watcher.py +318 -240
  82. package/openclaw.plugin.json +4 -0
  83. package/package.json +1 -1
  84. package/scripts/ocmemog-backfill-vectors.py +5 -3
  85. package/scripts/ocmemog-continuity-benchmark.py +1 -1
  86. package/scripts/ocmemog-demo.py +1 -1
  87. package/scripts/ocmemog-doctor.py +15 -0
  88. package/scripts/ocmemog-install.sh +29 -7
  89. package/scripts/ocmemog-integrated-proof.py +373 -0
  90. package/scripts/ocmemog-reindex-vectors.py +5 -3
  91. package/scripts/ocmemog-release-check.sh +330 -0
  92. package/scripts/ocmemog-sidecar.sh +4 -2
  93. package/scripts/ocmemog-test-rig.py +5 -3
  94. package/brain/runtime/memory/artifacts.py +0 -33
  95. package/brain/runtime/memory/context_builder.py +0 -112
  96. package/brain/runtime/memory/interaction_memory.py +0 -57
  97. package/brain/runtime/memory/memory_gate.py +0 -38
  98. package/brain/runtime/memory/memory_graph.py +0 -54
  99. package/brain/runtime/memory/person_identity.py +0 -83
  100. package/brain/runtime/memory/person_memory.py +0 -138
  101. package/brain/runtime/memory/sentiment_memory.py +0 -67
  102. package/brain/runtime/memory/tool_catalog.py +0 -68
@@ -0,0 +1,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,14 +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:
217
+ global _WATCHER_THREAD
105
218
  _load_queue_stats()
106
- enabled = os.environ.get("OCMEMOG_TRANSCRIPT_WATCHER", "").lower() in {"1", "true", "yes"}
219
+ enabled = _parse_bool_env("OCMEMOG_TRANSCRIPT_WATCHER")
107
220
  if not enabled:
108
221
  return
109
- thread = threading.Thread(target=watch_forever, daemon=True)
110
- 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()
111
233
 
112
234
 
113
235
  def _queue_path() -> Path:
@@ -186,6 +308,24 @@ def _enqueue_payload(payload: Dict[str, Any]) -> int:
186
308
  return _queue_depth()
187
309
 
188
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
+
189
329
 
190
330
  def _process_queue(limit: Optional[int] = None) -> Dict[str, Any]:
191
331
  processed = 0
@@ -208,8 +348,11 @@ def _process_queue(limit: Optional[int] = None) -> Dict[str, Any]:
208
348
  acknowledged = 0
209
349
  for line_no, payload in batch:
210
350
  try:
211
- req = IngestRequest(**payload)
212
- _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)
213
356
  processed += 1
214
357
  batch_processed += 1
215
358
  acknowledged = line_no
@@ -240,15 +383,16 @@ def _process_queue(limit: Optional[int] = None) -> Dict[str, Any]:
240
383
 
241
384
 
242
385
  def _ingest_worker() -> None:
243
- 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)
244
387
  if not enabled:
245
388
  return
246
- poll_seconds = float(os.environ.get("OCMEMOG_INGEST_ASYNC_POLL_SECONDS", "5"))
247
- 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)
248
391
 
249
- while True:
392
+ while not _INGEST_WORKER_STOP.is_set():
250
393
  _process_queue(batch_max)
251
- time.sleep(poll_seconds)
394
+ if _INGEST_WORKER_STOP.wait(poll_seconds):
395
+ break
252
396
 
253
397
 
254
398
 
@@ -256,10 +400,122 @@ def _drain_queue(limit: Optional[int] = None) -> Dict[str, Any]:
256
400
  return _process_queue(limit)
257
401
 
258
402
 
259
- @app.on_event("startup")
260
403
  def _start_ingest_worker() -> None:
261
- thread = threading.Thread(target=_ingest_worker, daemon=True)
262
- 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)
263
519
 
264
520
 
265
521
  class SearchRequest(BaseModel):
@@ -481,6 +737,8 @@ def _runtime_payload() -> Dict[str, Any]:
481
737
  return {
482
738
  "mode": status.mode,
483
739
  "missingDeps": status.missing_deps,
740
+ "identity": status.identity,
741
+ "capabilities": status.capabilities,
484
742
  "todo": status.todo,
485
743
  "warnings": status.warnings,
486
744
  }
@@ -679,6 +937,7 @@ def _read_transcript_snippet(path: Path, line_start: Optional[int], line_end: Op
679
937
  def healthz() -> dict[str, Any]:
680
938
  payload = _runtime_payload()
681
939
  payload["ok"] = True
940
+ payload["ready"] = payload.get("mode") == "ready"
682
941
  return payload
683
942
 
684
943
 
@@ -686,14 +945,35 @@ def healthz() -> dict[str, Any]:
686
945
  def memory_search(request: SearchRequest) -> dict[str, Any]:
687
946
  categories = _normalize_categories(request.categories)
688
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)
689
951
  try:
690
- 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
+ )
691
958
  flattened = flatten_results(results)
959
+ if len(flattened) > request.limit:
960
+ flattened = flattened[: request.limit]
692
961
  used_fallback = False
693
962
  except Exception as exc:
694
963
  flattened = _fallback_search(request.query, request.limit, categories)
695
964
  used_fallback = True
696
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
+ )
697
977
 
698
978
  return {
699
979
  "ok": True,
@@ -1197,8 +1477,10 @@ def _ingest_request(request: IngestRequest) -> dict[str, Any]:
1197
1477
  source=request.source,
1198
1478
  metadata=metadata,
1199
1479
  timestamp=request.timestamp,
1480
+ post_process=False,
1200
1481
  )
1201
1482
  reference = f"{memory_type}:{memory_id}"
1483
+ _enqueue_postprocess(reference, skip_embedding_provider=_parse_bool_env("OCMEMOG_POSTPROCESS_SKIP_EMBEDDING_PROVIDER", default=True))
1202
1484
  if request.conversation_id:
1203
1485
  memory_links.add_memory_link(reference, "conversation", f"conversation:{request.conversation_id}")
1204
1486
  if request.session_id:
@@ -1295,7 +1577,11 @@ def _ingest_request(request: IngestRequest) -> dict[str, Any]:
1295
1577
 
1296
1578
  @app.post("/memory/ingest")
1297
1579
  def memory_ingest(request: IngestRequest) -> dict[str, Any]:
1298
- 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
1299
1585
 
1300
1586
 
1301
1587
  @app.post("/memory/ingest_async")
@@ -1369,7 +1655,7 @@ def metrics() -> dict[str, Any]:
1369
1655
 
1370
1656
 
1371
1657
  def _event_stream():
1372
- path = state_store.reports_dir() / "brain_memory.log.jsonl"
1658
+ path = state_store.report_log_path()
1373
1659
  path.parent.mkdir(parents=True, exist_ok=True)
1374
1660
  if not path.exists():
1375
1661
  path.write_text("")
@@ -1389,12 +1675,13 @@ def events() -> StreamingResponse:
1389
1675
 
1390
1676
 
1391
1677
  def _tail_events(limit: int = 50) -> str:
1392
- path = state_store.reports_dir() / "brain_memory.log.jsonl"
1678
+ path = state_store.report_log_path()
1393
1679
  if not path.exists():
1394
1680
  return ""
1395
1681
  try:
1396
1682
  lines = path.read_text(encoding="utf-8", errors="ignore").splitlines()
1397
- except Exception:
1683
+ except Exception as exc:
1684
+ print(f"[ocmemog][events] tail_read_failed path={path} error={exc!r}", file=sys.stderr)
1398
1685
  return ""
1399
1686
  return "\n".join(lines[-limit:])
1400
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]]: