@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.
- package/CHANGELOG.md +30 -0
- package/README.md +85 -18
- package/brain/runtime/__init__.py +2 -12
- package/brain/runtime/config.py +1 -24
- package/brain/runtime/inference.py +1 -151
- package/brain/runtime/instrumentation.py +1 -15
- package/brain/runtime/memory/__init__.py +3 -13
- package/brain/runtime/memory/api.py +1 -1219
- package/brain/runtime/memory/candidate.py +1 -185
- package/brain/runtime/memory/conversation_state.py +1 -1823
- package/brain/runtime/memory/distill.py +1 -344
- package/brain/runtime/memory/embedding_engine.py +1 -92
- package/brain/runtime/memory/freshness.py +1 -112
- package/brain/runtime/memory/health.py +1 -40
- package/brain/runtime/memory/integrity.py +1 -186
- package/brain/runtime/memory/memory_consolidation.py +1 -58
- package/brain/runtime/memory/memory_links.py +1 -107
- package/brain/runtime/memory/memory_salience.py +1 -233
- package/brain/runtime/memory/memory_synthesis.py +1 -31
- package/brain/runtime/memory/memory_taxonomy.py +1 -33
- package/brain/runtime/memory/pondering_engine.py +1 -654
- package/brain/runtime/memory/promote.py +1 -277
- package/brain/runtime/memory/provenance.py +1 -406
- package/brain/runtime/memory/reinforcement.py +1 -71
- package/brain/runtime/memory/retrieval.py +1 -210
- package/brain/runtime/memory/semantic_search.py +1 -64
- package/brain/runtime/memory/store.py +1 -429
- package/brain/runtime/memory/unresolved_state.py +1 -91
- package/brain/runtime/memory/vector_index.py +1 -323
- package/brain/runtime/model_roles.py +1 -9
- package/brain/runtime/model_router.py +1 -22
- package/brain/runtime/providers.py +1 -66
- package/brain/runtime/security/redaction.py +1 -12
- package/brain/runtime/state_store.py +1 -23
- package/brain/runtime/storage_paths.py +1 -39
- package/docs/architecture/memory.md +20 -24
- package/docs/release-checklist.md +19 -6
- package/docs/usage.md +33 -17
- package/index.ts +8 -1
- package/ocmemog/__init__.py +11 -0
- package/ocmemog/doctor.py +1255 -0
- package/ocmemog/runtime/__init__.py +18 -0
- package/ocmemog/runtime/_compat_bridge.py +28 -0
- package/ocmemog/runtime/config.py +35 -0
- package/ocmemog/runtime/identity.py +115 -0
- package/ocmemog/runtime/inference.py +164 -0
- package/ocmemog/runtime/instrumentation.py +20 -0
- package/ocmemog/runtime/memory/__init__.py +91 -0
- package/ocmemog/runtime/memory/api.py +1431 -0
- package/ocmemog/runtime/memory/candidate.py +192 -0
- package/ocmemog/runtime/memory/conversation_state.py +1831 -0
- package/ocmemog/runtime/memory/distill.py +282 -0
- package/ocmemog/runtime/memory/embedding_engine.py +151 -0
- package/ocmemog/runtime/memory/freshness.py +114 -0
- package/ocmemog/runtime/memory/health.py +57 -0
- package/ocmemog/runtime/memory/integrity.py +208 -0
- package/ocmemog/runtime/memory/memory_consolidation.py +60 -0
- package/ocmemog/runtime/memory/memory_links.py +109 -0
- package/ocmemog/runtime/memory/memory_salience.py +235 -0
- package/ocmemog/runtime/memory/memory_synthesis.py +33 -0
- package/ocmemog/runtime/memory/memory_taxonomy.py +35 -0
- package/ocmemog/runtime/memory/pondering_engine.py +681 -0
- package/ocmemog/runtime/memory/promote.py +279 -0
- package/ocmemog/runtime/memory/provenance.py +408 -0
- package/ocmemog/runtime/memory/reinforcement.py +73 -0
- package/ocmemog/runtime/memory/retrieval.py +224 -0
- package/ocmemog/runtime/memory/semantic_search.py +66 -0
- package/ocmemog/runtime/memory/store.py +433 -0
- package/ocmemog/runtime/memory/unresolved_state.py +93 -0
- package/ocmemog/runtime/memory/vector_index.py +411 -0
- package/ocmemog/runtime/model_roles.py +16 -0
- package/ocmemog/runtime/model_router.py +29 -0
- package/ocmemog/runtime/providers.py +79 -0
- package/ocmemog/runtime/roles.py +92 -0
- package/ocmemog/runtime/security/__init__.py +8 -0
- package/ocmemog/runtime/security/redaction.py +17 -0
- package/ocmemog/runtime/state_store.py +34 -0
- package/ocmemog/runtime/storage_paths.py +70 -0
- package/ocmemog/sidecar/app.py +311 -23
- package/ocmemog/sidecar/compat.py +50 -13
- package/ocmemog/sidecar/transcript_watcher.py +391 -190
- package/openclaw.plugin.json +4 -0
- package/package.json +1 -1
- package/scripts/ocmemog-backfill-vectors.py +5 -3
- package/scripts/ocmemog-continuity-benchmark.py +1 -1
- package/scripts/ocmemog-demo.py +1 -1
- package/scripts/ocmemog-doctor.py +15 -0
- package/scripts/ocmemog-install.sh +29 -7
- package/scripts/ocmemog-integrated-proof.py +373 -0
- package/scripts/ocmemog-reindex-vectors.py +5 -3
- package/scripts/ocmemog-release-check.sh +330 -0
- package/scripts/ocmemog-sidecar.sh +4 -2
- package/scripts/ocmemog-test-rig.py +5 -3
- package/brain/runtime/memory/artifacts.py +0 -33
- package/brain/runtime/memory/context_builder.py +0 -112
- package/brain/runtime/memory/interaction_memory.py +0 -57
- package/brain/runtime/memory/memory_gate.py +0 -38
- package/brain/runtime/memory/memory_graph.py +0 -54
- package/brain/runtime/memory/person_identity.py +0 -83
- package/brain/runtime/memory/person_memory.py +0 -138
- package/brain/runtime/memory/sentiment_memory.py +0 -67
- 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
|
package/ocmemog/sidecar/app.py
CHANGED
|
@@ -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
|
|
17
|
-
from
|
|
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
|
-
|
|
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
|
-
|
|
109
|
-
|
|
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
|
-
|
|
211
|
-
|
|
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 =
|
|
386
|
+
enabled = _parse_bool_env("OCMEMOG_INGEST_ASYNC_WORKER", default=True)
|
|
243
387
|
if not enabled:
|
|
244
388
|
return
|
|
245
|
-
poll_seconds =
|
|
246
|
-
batch_max =
|
|
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
|
|
392
|
+
while not _INGEST_WORKER_STOP.is_set():
|
|
249
393
|
_process_queue(batch_max)
|
|
250
|
-
|
|
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
|
-
|
|
261
|
-
|
|
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(
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
"
|
|
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 =
|
|
40
|
-
|
|
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
|
|
66
|
+
from ocmemog.runtime import inference, providers
|
|
45
67
|
|
|
46
68
|
if getattr(inference, "__shim__", False):
|
|
47
|
-
missing_deps.append("
|
|
69
|
+
missing_deps.append("ocmemog.runtime.inference (shim only)")
|
|
48
70
|
if getattr(getattr(providers, "provider_execute", None), "__shim__", False):
|
|
49
|
-
missing_deps.append("
|
|
71
|
+
missing_deps.append("ocmemog.runtime.providers.provider_execute (shim only)")
|
|
50
72
|
except Exception as exc:
|
|
51
|
-
missing_deps.append(f"
|
|
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
|
-
|
|
54
|
-
|
|
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]]:
|