@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.
- package/CHANGELOG.md +16 -0
- package/README.md +83 -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 +310 -23
- package/ocmemog/sidecar/compat.py +50 -13
- package/ocmemog/sidecar/transcript_watcher.py +318 -240
- 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,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 =
|
|
219
|
+
enabled = _parse_bool_env("OCMEMOG_TRANSCRIPT_WATCHER")
|
|
107
220
|
if not enabled:
|
|
108
221
|
return
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
|
|
212
|
-
|
|
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 =
|
|
386
|
+
enabled = _parse_bool_env("OCMEMOG_INGEST_ASYNC_WORKER", default=True)
|
|
244
387
|
if not enabled:
|
|
245
388
|
return
|
|
246
|
-
poll_seconds =
|
|
247
|
-
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)
|
|
248
391
|
|
|
249
|
-
while
|
|
392
|
+
while not _INGEST_WORKER_STOP.is_set():
|
|
250
393
|
_process_queue(batch_max)
|
|
251
|
-
|
|
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
|
-
|
|
262
|
-
|
|
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(
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
"
|
|
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]]:
|