@simbimbo/memory-ocmemog 0.1.11 → 0.1.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +30 -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 +34 -0
- package/ocmemog/runtime/identity.py +115 -0
- package/ocmemog/runtime/inference.py +163 -0
- package/ocmemog/runtime/instrumentation.py +20 -0
- package/ocmemog/runtime/memory/__init__.py +91 -0
- package/ocmemog/runtime/memory/api.py +1594 -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 +93 -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 +15 -0
- package/ocmemog/runtime/model_router.py +28 -0
- package/ocmemog/runtime/providers.py +78 -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 +32 -0
- package/ocmemog/runtime/storage_paths.py +70 -0
- package/ocmemog/sidecar/app.py +421 -60
- package/ocmemog/sidecar/compat.py +50 -13
- package/ocmemog/sidecar/transcript_watcher.py +327 -242
- 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 +374 -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
package/ocmemog/sidecar/app.py
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
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
|
|
9
|
+
import tempfile
|
|
7
10
|
import time
|
|
11
|
+
import sys
|
|
12
|
+
from contextlib import asynccontextmanager
|
|
8
13
|
from pathlib import Path
|
|
9
14
|
from typing import Any, Dict, Iterable, List, Optional
|
|
10
15
|
|
|
@@ -13,17 +18,126 @@ from fastapi.responses import HTMLResponse, StreamingResponse, JSONResponse
|
|
|
13
18
|
from pydantic import BaseModel, Field
|
|
14
19
|
from datetime import datetime, timedelta
|
|
15
20
|
|
|
16
|
-
from
|
|
17
|
-
from
|
|
21
|
+
from ocmemog import __version__
|
|
22
|
+
from ocmemog.runtime import state_store
|
|
23
|
+
from ocmemog.runtime.memory import (
|
|
24
|
+
api,
|
|
25
|
+
conversation_state,
|
|
26
|
+
distill,
|
|
27
|
+
health,
|
|
28
|
+
memory_links,
|
|
29
|
+
pondering_engine,
|
|
30
|
+
provenance,
|
|
31
|
+
reinforcement,
|
|
32
|
+
retrieval,
|
|
33
|
+
store,
|
|
34
|
+
)
|
|
18
35
|
from ocmemog.sidecar.compat import flatten_results, probe_runtime
|
|
19
36
|
from ocmemog.sidecar.transcript_watcher import watch_forever
|
|
20
37
|
|
|
21
38
|
DEFAULT_CATEGORIES = tuple(store.MEMORY_TABLES)
|
|
22
39
|
|
|
23
|
-
app = FastAPI(title="ocmemog sidecar", version="0.1.10")
|
|
24
|
-
|
|
25
40
|
API_TOKEN = os.environ.get("OCMEMOG_API_TOKEN")
|
|
41
|
+
_GOVERNANCE_REVIEW_CACHE_TTL_SECONDS = 15.0
|
|
42
|
+
_governance_review_cache: Dict[str, Any] = {"key": None, "expires_at": 0.0, "payload": None}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
_BOOL_TRUE_VALUES = {"1", "true", "yes", "on", "y", "t"}
|
|
46
|
+
_BOOL_FALSE_VALUES = {"0", "false", "no", "off", "n", "f"}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _parse_bool_env_value(raw: Any | None, default: bool = False) -> tuple[bool, bool]:
|
|
50
|
+
"""Return ``(value, valid)``, where ``valid`` indicates parser confidence."""
|
|
51
|
+
if raw is None:
|
|
52
|
+
return default, True
|
|
53
|
+
|
|
54
|
+
raw_value = str(raw).strip().lower()
|
|
55
|
+
if raw_value in _BOOL_TRUE_VALUES:
|
|
56
|
+
return True, True
|
|
57
|
+
if raw_value in _BOOL_FALSE_VALUES:
|
|
58
|
+
return False, True
|
|
59
|
+
if not raw_value:
|
|
60
|
+
return default, False
|
|
61
|
+
return default, False
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _parse_bool_env(name: str, default: bool = False) -> bool:
|
|
65
|
+
raw = os.environ.get(name)
|
|
66
|
+
value, _ = _parse_bool_env_value(raw, default=default)
|
|
67
|
+
return value
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _parse_float_env(name: str, default: float, minimum: float | None = None) -> float:
|
|
71
|
+
raw = os.environ.get(name)
|
|
72
|
+
try:
|
|
73
|
+
value = float(raw if raw is not None else default)
|
|
74
|
+
except Exception:
|
|
75
|
+
print(
|
|
76
|
+
f"[ocmemog][config] invalid float env value: {name}={raw!r}; using default {default}",
|
|
77
|
+
file=sys.stderr,
|
|
78
|
+
)
|
|
79
|
+
return default
|
|
80
|
+
if minimum is not None and value < minimum:
|
|
81
|
+
print(
|
|
82
|
+
f"[ocmemog][config] env value below minimum: {name}={value}; using default {default}",
|
|
83
|
+
file=sys.stderr,
|
|
84
|
+
)
|
|
85
|
+
return default
|
|
86
|
+
return value
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _parse_int_env(name: str, default: int, minimum: int | None = None) -> int:
|
|
90
|
+
raw = os.environ.get(name)
|
|
91
|
+
try:
|
|
92
|
+
value = int(raw if raw is not None else default)
|
|
93
|
+
except Exception:
|
|
94
|
+
print(
|
|
95
|
+
f"[ocmemog][config] invalid int env value: {name}={raw!r}; using default {default}",
|
|
96
|
+
file=sys.stderr,
|
|
97
|
+
)
|
|
98
|
+
return default
|
|
99
|
+
if minimum is not None and value < minimum:
|
|
100
|
+
print(
|
|
101
|
+
f"[ocmemog][config] env value below minimum: {name}={value}; using default {default}",
|
|
102
|
+
file=sys.stderr,
|
|
103
|
+
)
|
|
104
|
+
return default
|
|
105
|
+
return value
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
_SHUTDOWN_TIMING = _parse_bool_env("OCMEMOG_SHUTDOWN_TIMING", default=True)
|
|
109
|
+
|
|
26
110
|
|
|
111
|
+
@asynccontextmanager
|
|
112
|
+
async def _sidecar_lifespan(_: FastAPI):
|
|
113
|
+
_startup_started = time.perf_counter()
|
|
114
|
+
try:
|
|
115
|
+
_start_transcript_watcher()
|
|
116
|
+
_start_ingest_worker()
|
|
117
|
+
if _SHUTDOWN_TIMING:
|
|
118
|
+
print(
|
|
119
|
+
f"[ocmemog][shutdown] lifespan_startup elapsed={time.perf_counter()-_startup_started:.3f}s",
|
|
120
|
+
file=sys.stderr,
|
|
121
|
+
)
|
|
122
|
+
yield
|
|
123
|
+
finally:
|
|
124
|
+
shutdown_started = time.perf_counter()
|
|
125
|
+
_stop_background_workers()
|
|
126
|
+
if _SHUTDOWN_TIMING:
|
|
127
|
+
print(
|
|
128
|
+
f"[ocmemog][shutdown] lifespan_shutdown elapsed={time.perf_counter()-shutdown_started:.3f}s",
|
|
129
|
+
file=sys.stderr,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
app = FastAPI(title="ocmemog sidecar", version=__version__, lifespan=_sidecar_lifespan)
|
|
134
|
+
|
|
135
|
+
_INGEST_WORKER_STOP = threading.Event()
|
|
136
|
+
_INGEST_WORKER_THREAD: threading.Thread | None = None
|
|
137
|
+
_INGEST_WORKER_LOCK = threading.Lock()
|
|
138
|
+
_WATCHER_STOP = threading.Event()
|
|
139
|
+
_WATCHER_THREAD: threading.Thread | None = None
|
|
140
|
+
_WATCHER_LOCK = threading.Lock()
|
|
27
141
|
QUEUE_LOCK = threading.Lock()
|
|
28
142
|
QUEUE_PROCESS_LOCK = threading.Lock()
|
|
29
143
|
QUEUE_STATS = {
|
|
@@ -33,6 +147,8 @@ QUEUE_STATS = {
|
|
|
33
147
|
"last_error": None,
|
|
34
148
|
"last_batch": 0,
|
|
35
149
|
}
|
|
150
|
+
_POSTPROCESS_TASK_KEY = "_ocmemog_task"
|
|
151
|
+
_POSTPROCESS_TASK_VALUE = "postprocess_memory"
|
|
36
152
|
|
|
37
153
|
|
|
38
154
|
_REFLECTION_RECLASSIFY_PREFERENCE_PATTERNS = (
|
|
@@ -85,9 +201,14 @@ def _load_queue_stats() -> None:
|
|
|
85
201
|
|
|
86
202
|
def _save_queue_stats() -> None:
|
|
87
203
|
path = _queue_stats_path()
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
204
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
205
|
+
payload = json.dumps(QUEUE_STATS, indent=2, sort_keys=True)
|
|
206
|
+
with tempfile.NamedTemporaryFile('w', encoding='utf-8', dir=str(path.parent), prefix='queue_stats.', suffix='.tmp', delete=False) as handle:
|
|
207
|
+
handle.write(payload)
|
|
208
|
+
handle.flush()
|
|
209
|
+
os.fsync(handle.fileno())
|
|
210
|
+
tmp_name = handle.name
|
|
211
|
+
Path(tmp_name).replace(path)
|
|
91
212
|
|
|
92
213
|
|
|
93
214
|
@app.middleware("http")
|
|
@@ -100,14 +221,23 @@ async def _auth_middleware(request: Request, call_next):
|
|
|
100
221
|
return await call_next(request)
|
|
101
222
|
|
|
102
223
|
|
|
103
|
-
@app.on_event("startup")
|
|
104
224
|
def _start_transcript_watcher() -> None:
|
|
225
|
+
global _WATCHER_THREAD
|
|
105
226
|
_load_queue_stats()
|
|
106
|
-
enabled =
|
|
227
|
+
enabled = _parse_bool_env("OCMEMOG_TRANSCRIPT_WATCHER")
|
|
107
228
|
if not enabled:
|
|
108
229
|
return
|
|
109
|
-
|
|
110
|
-
|
|
230
|
+
with _WATCHER_LOCK:
|
|
231
|
+
if _WATCHER_THREAD and _WATCHER_THREAD.is_alive():
|
|
232
|
+
return
|
|
233
|
+
_WATCHER_STOP.clear()
|
|
234
|
+
_WATCHER_THREAD = threading.Thread(
|
|
235
|
+
target=watch_forever,
|
|
236
|
+
args=(_WATCHER_STOP,),
|
|
237
|
+
daemon=True,
|
|
238
|
+
name="ocmemog-transcript-watcher",
|
|
239
|
+
)
|
|
240
|
+
_WATCHER_THREAD.start()
|
|
111
241
|
|
|
112
242
|
|
|
113
243
|
def _queue_path() -> Path:
|
|
@@ -186,6 +316,24 @@ def _enqueue_payload(payload: Dict[str, Any]) -> int:
|
|
|
186
316
|
return _queue_depth()
|
|
187
317
|
|
|
188
318
|
|
|
319
|
+
def _enqueue_postprocess(reference: str, *, skip_embedding_provider: bool = True) -> int:
|
|
320
|
+
return _enqueue_payload({
|
|
321
|
+
_POSTPROCESS_TASK_KEY: _POSTPROCESS_TASK_VALUE,
|
|
322
|
+
"reference": reference,
|
|
323
|
+
"skip_embedding_provider": bool(skip_embedding_provider),
|
|
324
|
+
})
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def _run_postprocess_payload(payload: Dict[str, Any]) -> None:
|
|
328
|
+
reference = str(payload.get("reference") or "").strip()
|
|
329
|
+
if not reference:
|
|
330
|
+
raise ValueError("missing_reference")
|
|
331
|
+
skip_embedding_provider = bool(payload.get("skip_embedding_provider", True))
|
|
332
|
+
result = api.postprocess_stored_memory(reference, skip_embedding_provider=skip_embedding_provider)
|
|
333
|
+
if not result.get("ok"):
|
|
334
|
+
raise RuntimeError(str(result.get("error") or "postprocess_failed"))
|
|
335
|
+
|
|
336
|
+
|
|
189
337
|
|
|
190
338
|
def _process_queue(limit: Optional[int] = None) -> Dict[str, Any]:
|
|
191
339
|
processed = 0
|
|
@@ -208,8 +356,11 @@ def _process_queue(limit: Optional[int] = None) -> Dict[str, Any]:
|
|
|
208
356
|
acknowledged = 0
|
|
209
357
|
for line_no, payload in batch:
|
|
210
358
|
try:
|
|
211
|
-
|
|
212
|
-
|
|
359
|
+
if isinstance(payload, dict) and payload.get(_POSTPROCESS_TASK_KEY) == _POSTPROCESS_TASK_VALUE:
|
|
360
|
+
_run_postprocess_payload(payload)
|
|
361
|
+
else:
|
|
362
|
+
req = IngestRequest(**payload)
|
|
363
|
+
_ingest_request(req)
|
|
213
364
|
processed += 1
|
|
214
365
|
batch_processed += 1
|
|
215
366
|
acknowledged = line_no
|
|
@@ -240,15 +391,16 @@ def _process_queue(limit: Optional[int] = None) -> Dict[str, Any]:
|
|
|
240
391
|
|
|
241
392
|
|
|
242
393
|
def _ingest_worker() -> None:
|
|
243
|
-
enabled =
|
|
394
|
+
enabled = _parse_bool_env("OCMEMOG_INGEST_ASYNC_WORKER", default=True)
|
|
244
395
|
if not enabled:
|
|
245
396
|
return
|
|
246
|
-
poll_seconds =
|
|
247
|
-
batch_max =
|
|
397
|
+
poll_seconds = _parse_float_env("OCMEMOG_INGEST_ASYNC_POLL_SECONDS", default=5.0, minimum=0.0)
|
|
398
|
+
batch_max = _parse_int_env("OCMEMOG_INGEST_ASYNC_BATCH_MAX", default=25, minimum=1)
|
|
248
399
|
|
|
249
|
-
while
|
|
400
|
+
while not _INGEST_WORKER_STOP.is_set():
|
|
250
401
|
_process_queue(batch_max)
|
|
251
|
-
|
|
402
|
+
if _INGEST_WORKER_STOP.wait(poll_seconds):
|
|
403
|
+
break
|
|
252
404
|
|
|
253
405
|
|
|
254
406
|
|
|
@@ -256,10 +408,122 @@ def _drain_queue(limit: Optional[int] = None) -> Dict[str, Any]:
|
|
|
256
408
|
return _process_queue(limit)
|
|
257
409
|
|
|
258
410
|
|
|
259
|
-
@app.on_event("startup")
|
|
260
411
|
def _start_ingest_worker() -> None:
|
|
261
|
-
|
|
262
|
-
|
|
412
|
+
global _INGEST_WORKER_THREAD
|
|
413
|
+
with _INGEST_WORKER_LOCK:
|
|
414
|
+
if _INGEST_WORKER_THREAD and _INGEST_WORKER_THREAD.is_alive():
|
|
415
|
+
return
|
|
416
|
+
_INGEST_WORKER_STOP.clear()
|
|
417
|
+
_INGEST_WORKER_THREAD = threading.Thread(
|
|
418
|
+
target=_ingest_worker,
|
|
419
|
+
daemon=True,
|
|
420
|
+
name="ocmemog-ingest-worker",
|
|
421
|
+
)
|
|
422
|
+
_INGEST_WORKER_THREAD.start()
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def _stop_background_workers() -> None:
|
|
426
|
+
global _INGEST_WORKER_THREAD, _WATCHER_THREAD
|
|
427
|
+
shutdown_start = time.perf_counter()
|
|
428
|
+
if _SHUTDOWN_TIMING:
|
|
429
|
+
print(f"[ocmemog][shutdown] shutdown_begin", file=sys.stderr)
|
|
430
|
+
timeout = _parse_float_env(
|
|
431
|
+
"OCMEMOG_WORKER_SHUTDOWN_TIMEOUT_SECONDS",
|
|
432
|
+
default=0.35,
|
|
433
|
+
minimum=0.0,
|
|
434
|
+
)
|
|
435
|
+
if _SHUTDOWN_TIMING:
|
|
436
|
+
print(f"[ocmemog][shutdown] shutdown_config timeout={timeout:.3f}s", file=sys.stderr)
|
|
437
|
+
|
|
438
|
+
queue_drain_requested = _parse_bool_env("OCMEMOG_SHUTDOWN_DRAIN_QUEUE")
|
|
439
|
+
if queue_drain_requested and _queue_depth() > 0:
|
|
440
|
+
_queue_drain_start = time.perf_counter()
|
|
441
|
+
drain_stats = _drain_queue()
|
|
442
|
+
if _SHUTDOWN_TIMING:
|
|
443
|
+
print(
|
|
444
|
+
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)}",
|
|
445
|
+
file=sys.stderr,
|
|
446
|
+
)
|
|
447
|
+
_INGEST_WORKER_STOP.set()
|
|
448
|
+
_WATCHER_STOP.set()
|
|
449
|
+
if _SHUTDOWN_TIMING:
|
|
450
|
+
print(
|
|
451
|
+
f"[ocmemog][shutdown] stop_signals_set elapsed={time.perf_counter()-shutdown_start:.3f}s",
|
|
452
|
+
file=sys.stderr,
|
|
453
|
+
)
|
|
454
|
+
|
|
455
|
+
if _parse_bool_env("OCMEMOG_SHUTDOWN_DUMP_THREADS"):
|
|
456
|
+
_dump_thread_dump("post-stop requested")
|
|
457
|
+
|
|
458
|
+
with _INGEST_WORKER_LOCK:
|
|
459
|
+
ingest_worker = _INGEST_WORKER_THREAD
|
|
460
|
+
if ingest_worker is not None and ingest_worker.is_alive():
|
|
461
|
+
ingest_join_start = time.perf_counter()
|
|
462
|
+
ingest_worker.join(timeout=timeout)
|
|
463
|
+
if _SHUTDOWN_TIMING:
|
|
464
|
+
print(
|
|
465
|
+
f"[ocmemog][shutdown] ingest_worker_join elapsed={time.perf_counter()-ingest_join_start:.3f}s alive={ingest_worker.is_alive()}",
|
|
466
|
+
file=sys.stderr,
|
|
467
|
+
)
|
|
468
|
+
if _parse_bool_env("OCMEMOG_SHUTDOWN_DUMP_THREADS"):
|
|
469
|
+
_dump_join_result("ingest-worker", ingest_worker, timeout)
|
|
470
|
+
if not ingest_worker.is_alive():
|
|
471
|
+
with _INGEST_WORKER_LOCK:
|
|
472
|
+
if _INGEST_WORKER_THREAD is ingest_worker:
|
|
473
|
+
_INGEST_WORKER_THREAD = None
|
|
474
|
+
|
|
475
|
+
with _WATCHER_LOCK:
|
|
476
|
+
watcher_thread = _WATCHER_THREAD
|
|
477
|
+
if watcher_thread is not None and watcher_thread.is_alive():
|
|
478
|
+
watcher_join_start = time.perf_counter()
|
|
479
|
+
watcher_thread.join(timeout=timeout)
|
|
480
|
+
if _SHUTDOWN_TIMING:
|
|
481
|
+
print(
|
|
482
|
+
f"[ocmemog][shutdown] transcript_watcher_join elapsed={time.perf_counter()-watcher_join_start:.3f}s alive={watcher_thread.is_alive()}",
|
|
483
|
+
file=sys.stderr,
|
|
484
|
+
)
|
|
485
|
+
if _parse_bool_env("OCMEMOG_SHUTDOWN_DUMP_THREADS"):
|
|
486
|
+
_dump_join_result("transcript-watcher", watcher_thread, timeout)
|
|
487
|
+
if not watcher_thread.is_alive():
|
|
488
|
+
with _WATCHER_LOCK:
|
|
489
|
+
if _WATCHER_THREAD is watcher_thread:
|
|
490
|
+
_WATCHER_THREAD = None
|
|
491
|
+
if _SHUTDOWN_TIMING:
|
|
492
|
+
print(
|
|
493
|
+
f"[ocmemog][shutdown] shutdown_complete elapsed={time.perf_counter()-shutdown_start:.3f}s",
|
|
494
|
+
file=sys.stderr,
|
|
495
|
+
)
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
def _dump_thread_dump(context: str) -> None:
|
|
499
|
+
print(f"[ocmemog][thread-dump:{context}]", file=sys.stderr)
|
|
500
|
+
_dump_thread_states()
|
|
501
|
+
faulthandler.dump_traceback(file=sys.stderr, all_threads=True)
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
def _dump_join_result(thread_label: str, thread: threading.Thread, timeout: float) -> None:
|
|
505
|
+
if thread.is_alive():
|
|
506
|
+
print(
|
|
507
|
+
f"[ocmemog][shutdown] {thread_label} still alive after join timeout={timeout:.3f}s",
|
|
508
|
+
file=sys.stderr,
|
|
509
|
+
)
|
|
510
|
+
_dump_thread_dump(thread_label)
|
|
511
|
+
else:
|
|
512
|
+
print(
|
|
513
|
+
f"[ocmemog][shutdown] {thread_label} joined cleanly",
|
|
514
|
+
file=sys.stderr,
|
|
515
|
+
)
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
def _dump_thread_states() -> None:
|
|
519
|
+
for thread in threading.enumerate():
|
|
520
|
+
print(
|
|
521
|
+
f"[ocmemog][thread-state] name={thread.name} alive={thread.is_alive()} daemon={thread.daemon} ident={thread.ident}",
|
|
522
|
+
file=sys.stderr,
|
|
523
|
+
)
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
atexit.register(_stop_background_workers)
|
|
263
527
|
|
|
264
528
|
|
|
265
529
|
class SearchRequest(BaseModel):
|
|
@@ -290,6 +554,7 @@ class GovernanceReviewRequest(BaseModel):
|
|
|
290
554
|
categories: Optional[List[str]] = None
|
|
291
555
|
limit: int = Field(default=100, ge=1, le=500)
|
|
292
556
|
context_depth: int = Field(default=1, ge=0, le=2)
|
|
557
|
+
scan_limit: int = Field(default=3000, ge=1, le=10000)
|
|
293
558
|
|
|
294
559
|
|
|
295
560
|
class GovernanceDecisionRequest(BaseModel):
|
|
@@ -481,6 +746,8 @@ def _runtime_payload() -> Dict[str, Any]:
|
|
|
481
746
|
return {
|
|
482
747
|
"mode": status.mode,
|
|
483
748
|
"missingDeps": status.missing_deps,
|
|
749
|
+
"identity": status.identity,
|
|
750
|
+
"capabilities": status.capabilities,
|
|
484
751
|
"todo": status.todo,
|
|
485
752
|
"warnings": status.warnings,
|
|
486
753
|
}
|
|
@@ -679,6 +946,7 @@ def _read_transcript_snippet(path: Path, line_start: Optional[int], line_end: Op
|
|
|
679
946
|
def healthz() -> dict[str, Any]:
|
|
680
947
|
payload = _runtime_payload()
|
|
681
948
|
payload["ok"] = True
|
|
949
|
+
payload["ready"] = payload.get("mode") == "ready"
|
|
682
950
|
return payload
|
|
683
951
|
|
|
684
952
|
|
|
@@ -686,14 +954,35 @@ def healthz() -> dict[str, Any]:
|
|
|
686
954
|
def memory_search(request: SearchRequest) -> dict[str, Any]:
|
|
687
955
|
categories = _normalize_categories(request.categories)
|
|
688
956
|
runtime = _runtime_payload()
|
|
957
|
+
started = time.perf_counter()
|
|
958
|
+
query = request.query or ""
|
|
959
|
+
skip_vector_provider = _parse_bool_env("OCMEMOG_SEARCH_SKIP_EMBEDDING_PROVIDER", default=True)
|
|
689
960
|
try:
|
|
690
|
-
results = retrieval.retrieve_for_queries(
|
|
961
|
+
results = retrieval.retrieve_for_queries(
|
|
962
|
+
[query],
|
|
963
|
+
limit=request.limit,
|
|
964
|
+
categories=categories,
|
|
965
|
+
skip_vector_provider=skip_vector_provider,
|
|
966
|
+
)
|
|
691
967
|
flattened = flatten_results(results)
|
|
968
|
+
if len(flattened) > request.limit:
|
|
969
|
+
flattened = flattened[: request.limit]
|
|
692
970
|
used_fallback = False
|
|
693
971
|
except Exception as exc:
|
|
694
972
|
flattened = _fallback_search(request.query, request.limit, categories)
|
|
695
973
|
used_fallback = True
|
|
696
974
|
runtime["warnings"] = [*runtime["warnings"], f"search fallback enabled: {exc}"]
|
|
975
|
+
elapsed_ms = round((time.perf_counter() - started) * 1000, 3)
|
|
976
|
+
if elapsed_ms >= 10:
|
|
977
|
+
print(
|
|
978
|
+
f"[ocmemog][route] memory_search elapsed_ms={elapsed_ms:.3f} limit={request.limit} categories={','.join(categories)} fallback={used_fallback}",
|
|
979
|
+
file=sys.stderr,
|
|
980
|
+
)
|
|
981
|
+
if elapsed_ms >= 200:
|
|
982
|
+
print(
|
|
983
|
+
f"[ocmemog][route] memory_search slow_path query={query[:128]!r} result_count={len(flattened)}",
|
|
984
|
+
file=sys.stderr,
|
|
985
|
+
)
|
|
697
986
|
|
|
698
987
|
return {
|
|
699
988
|
"ok": True,
|
|
@@ -764,17 +1053,59 @@ def memory_governance_review(request: GovernanceReviewRequest) -> dict[str, Any]
|
|
|
764
1053
|
categories=request.categories,
|
|
765
1054
|
limit=request.limit,
|
|
766
1055
|
context_depth=request.context_depth,
|
|
1056
|
+
scan_limit=request.scan_limit,
|
|
767
1057
|
)
|
|
768
1058
|
return {
|
|
769
1059
|
"ok": True,
|
|
770
1060
|
"categories": request.categories,
|
|
771
1061
|
"limit": request.limit,
|
|
772
1062
|
"context_depth": request.context_depth,
|
|
1063
|
+
"scan_limit": request.scan_limit,
|
|
773
1064
|
"items": items,
|
|
774
1065
|
**runtime,
|
|
775
1066
|
}
|
|
776
1067
|
|
|
777
1068
|
|
|
1069
|
+
@app.post("/memory/governance/review/summary")
|
|
1070
|
+
def memory_governance_review_summary(request: GovernanceReviewRequest) -> dict[str, Any]:
|
|
1071
|
+
runtime = _runtime_payload()
|
|
1072
|
+
limit = min(int(request.limit or 25), 50)
|
|
1073
|
+
scan_limit = min(int(request.scan_limit or max(limit * 10, 250)), 500)
|
|
1074
|
+
cache_key = json.dumps(
|
|
1075
|
+
{
|
|
1076
|
+
"categories": sorted(request.categories or []),
|
|
1077
|
+
"limit": limit,
|
|
1078
|
+
"context_depth": 0,
|
|
1079
|
+
"scan_limit": scan_limit,
|
|
1080
|
+
},
|
|
1081
|
+
sort_keys=True,
|
|
1082
|
+
)
|
|
1083
|
+
now = time.time()
|
|
1084
|
+
if _governance_review_cache.get("key") == cache_key and float(_governance_review_cache.get("expires_at") or 0.0) > now:
|
|
1085
|
+
cached_payload = _governance_review_cache.get("payload") or {}
|
|
1086
|
+
return {**cached_payload, **runtime, "cached": True}
|
|
1087
|
+
|
|
1088
|
+
items = api.list_governance_review_items(
|
|
1089
|
+
categories=request.categories,
|
|
1090
|
+
limit=limit,
|
|
1091
|
+
context_depth=0,
|
|
1092
|
+
scan_limit=scan_limit,
|
|
1093
|
+
)
|
|
1094
|
+
payload = {
|
|
1095
|
+
"ok": True,
|
|
1096
|
+
"categories": request.categories,
|
|
1097
|
+
"limit": limit,
|
|
1098
|
+
"context_depth": 0,
|
|
1099
|
+
"scan_limit": scan_limit,
|
|
1100
|
+
"items": items,
|
|
1101
|
+
"cached": False,
|
|
1102
|
+
}
|
|
1103
|
+
_governance_review_cache.update(
|
|
1104
|
+
{"key": cache_key, "expires_at": now + _GOVERNANCE_REVIEW_CACHE_TTL_SECONDS, "payload": payload}
|
|
1105
|
+
)
|
|
1106
|
+
return {**payload, **runtime}
|
|
1107
|
+
|
|
1108
|
+
|
|
778
1109
|
@app.post("/memory/governance/decision")
|
|
779
1110
|
def memory_governance_decision(request: GovernanceDecisionRequest) -> dict[str, Any]:
|
|
780
1111
|
runtime = _runtime_payload()
|
|
@@ -1197,8 +1528,10 @@ def _ingest_request(request: IngestRequest) -> dict[str, Any]:
|
|
|
1197
1528
|
source=request.source,
|
|
1198
1529
|
metadata=metadata,
|
|
1199
1530
|
timestamp=request.timestamp,
|
|
1531
|
+
post_process=False,
|
|
1200
1532
|
)
|
|
1201
1533
|
reference = f"{memory_type}:{memory_id}"
|
|
1534
|
+
_enqueue_postprocess(reference, skip_embedding_provider=_parse_bool_env("OCMEMOG_POSTPROCESS_SKIP_EMBEDDING_PROVIDER", default=True))
|
|
1202
1535
|
if request.conversation_id:
|
|
1203
1536
|
memory_links.add_memory_link(reference, "conversation", f"conversation:{request.conversation_id}")
|
|
1204
1537
|
if request.session_id:
|
|
@@ -1295,7 +1628,11 @@ def _ingest_request(request: IngestRequest) -> dict[str, Any]:
|
|
|
1295
1628
|
|
|
1296
1629
|
@app.post("/memory/ingest")
|
|
1297
1630
|
def memory_ingest(request: IngestRequest) -> dict[str, Any]:
|
|
1298
|
-
|
|
1631
|
+
started = time.perf_counter()
|
|
1632
|
+
payload = _ingest_request(request)
|
|
1633
|
+
elapsed_ms = round((time.perf_counter() - started) * 1000, 3)
|
|
1634
|
+
print(f"[ocmemog][route] memory_ingest elapsed_ms={elapsed_ms:.3f} kind={request.kind} reference={payload.get('reference', '')}", file=sys.stderr)
|
|
1635
|
+
return payload
|
|
1299
1636
|
|
|
1300
1637
|
|
|
1301
1638
|
@app.post("/memory/ingest_async")
|
|
@@ -1344,32 +1681,35 @@ def memory_distill(request: DistillRequest) -> dict[str, Any]:
|
|
|
1344
1681
|
@app.get("/metrics")
|
|
1345
1682
|
def metrics() -> dict[str, Any]:
|
|
1346
1683
|
runtime = _runtime_payload()
|
|
1347
|
-
payload = health.
|
|
1684
|
+
payload = health.get_memory_health_fast()
|
|
1348
1685
|
counts = payload.get("counts", {})
|
|
1349
1686
|
counts["queue_depth"] = _queue_depth()
|
|
1350
1687
|
counts["queue_processed"] = QUEUE_STATS.get("processed", 0)
|
|
1351
1688
|
counts["queue_errors"] = QUEUE_STATS.get("errors", 0)
|
|
1352
1689
|
payload["counts"] = counts
|
|
1690
|
+
|
|
1353
1691
|
coverage_tables = list(store.MEMORY_TABLES)
|
|
1354
1692
|
conn = store.connect()
|
|
1355
1693
|
try:
|
|
1694
|
+
vector_counts: Dict[str, int] = {str(row[0]): int(row[1] or 0) for row in conn.execute("SELECT source_type, COUNT(*) FROM vector_embeddings GROUP BY source_type")}
|
|
1356
1695
|
payload["coverage"] = [
|
|
1357
1696
|
{
|
|
1358
1697
|
"table": table,
|
|
1359
1698
|
"rows": int(counts.get(table, 0) or 0),
|
|
1360
|
-
"vectors": int(
|
|
1361
|
-
"missing": max(int(counts.get(table, 0) or 0) - int(
|
|
1699
|
+
"vectors": int(vector_counts.get(table, 0) or 0),
|
|
1700
|
+
"missing": max(int(counts.get(table, 0) or 0) - int(vector_counts.get(table, 0) or 0), 0),
|
|
1362
1701
|
}
|
|
1363
1702
|
for table in coverage_tables
|
|
1364
1703
|
]
|
|
1365
1704
|
finally:
|
|
1366
1705
|
conn.close()
|
|
1706
|
+
|
|
1367
1707
|
payload["queue"] = QUEUE_STATS
|
|
1368
1708
|
return {"ok": True, "metrics": payload, **runtime}
|
|
1369
1709
|
|
|
1370
1710
|
|
|
1371
1711
|
def _event_stream():
|
|
1372
|
-
path = state_store.
|
|
1712
|
+
path = state_store.report_log_path()
|
|
1373
1713
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
1374
1714
|
if not path.exists():
|
|
1375
1715
|
path.write_text("")
|
|
@@ -1389,33 +1729,55 @@ def events() -> StreamingResponse:
|
|
|
1389
1729
|
|
|
1390
1730
|
|
|
1391
1731
|
def _tail_events(limit: int = 50) -> str:
|
|
1392
|
-
path = state_store.
|
|
1732
|
+
path = state_store.report_log_path()
|
|
1393
1733
|
if not path.exists():
|
|
1394
1734
|
return ""
|
|
1395
1735
|
try:
|
|
1396
|
-
|
|
1397
|
-
|
|
1736
|
+
size = path.stat().st_size
|
|
1737
|
+
# Read only the trailing chunk to avoid loading very large logs.
|
|
1738
|
+
# This bounds dashboard latency even when the report log grows huge.
|
|
1739
|
+
max_bytes = 256 * 1024
|
|
1740
|
+
with path.open("rb") as handle:
|
|
1741
|
+
if size > max_bytes:
|
|
1742
|
+
handle.seek(-max_bytes, 2)
|
|
1743
|
+
data = handle.read()
|
|
1744
|
+
text = data.decode("utf-8", errors="ignore")
|
|
1745
|
+
lines = text.splitlines()
|
|
1746
|
+
except Exception as exc:
|
|
1747
|
+
print(f"[ocmemog][events] tail_read_failed path={path} error={exc!r}", file=sys.stderr)
|
|
1398
1748
|
return ""
|
|
1399
1749
|
return "\n".join(lines[-limit:])
|
|
1400
1750
|
|
|
1401
1751
|
|
|
1402
1752
|
@app.get("/dashboard")
|
|
1403
1753
|
def dashboard() -> HTMLResponse:
|
|
1404
|
-
metrics_payload = health.
|
|
1754
|
+
metrics_payload = health.get_memory_health_fast()
|
|
1405
1755
|
counts = metrics_payload.get("counts", {})
|
|
1406
1756
|
coverage_tables = list(store.MEMORY_TABLES)
|
|
1407
1757
|
conn = store.connect()
|
|
1408
1758
|
try:
|
|
1759
|
+
cursor = conn.execute("SELECT source_type, COUNT(*) FROM vector_embeddings GROUP BY source_type")
|
|
1760
|
+
try:
|
|
1761
|
+
vector_rows = list(cursor)
|
|
1762
|
+
except TypeError:
|
|
1763
|
+
fetchall = getattr(cursor, "fetchall", None)
|
|
1764
|
+
if callable(fetchall):
|
|
1765
|
+
vector_rows = fetchall()
|
|
1766
|
+
else:
|
|
1767
|
+
fetchone = getattr(cursor, "fetchone", None)
|
|
1768
|
+
row = fetchone() if callable(fetchone) else None
|
|
1769
|
+
vector_rows = [row] if row is not None else []
|
|
1770
|
+
vector_counts: Dict[str, int] = {}
|
|
1771
|
+
for row in vector_rows:
|
|
1772
|
+
if not isinstance(row, (list, tuple)) or len(row) < 2:
|
|
1773
|
+
continue
|
|
1774
|
+
vector_counts[str(row[0])] = int(row[1] or 0)
|
|
1775
|
+
if hasattr(cursor, "close"):
|
|
1776
|
+
cursor.close()
|
|
1409
1777
|
coverage_rows = []
|
|
1410
1778
|
for table in coverage_tables:
|
|
1411
1779
|
total = int(counts.get(table, 0) or 0)
|
|
1412
|
-
vectors = int(
|
|
1413
|
-
conn.execute(
|
|
1414
|
-
"SELECT COUNT(*) FROM vector_embeddings WHERE source_type=?",
|
|
1415
|
-
(table,),
|
|
1416
|
-
).fetchone()[0]
|
|
1417
|
-
or 0
|
|
1418
|
-
)
|
|
1780
|
+
vectors = int(vector_counts.get(table, 0) or 0)
|
|
1419
1781
|
missing = max(total - vectors, 0)
|
|
1420
1782
|
coverage_rows.append({"table": table, "rows": total, "vectors": vectors, "missing": missing})
|
|
1421
1783
|
finally:
|
|
@@ -1517,15 +1879,12 @@ def dashboard() -> HTMLResponse:
|
|
|
1517
1879
|
<thead>
|
|
1518
1880
|
<tr>
|
|
1519
1881
|
<th>Priority</th>
|
|
1520
|
-
<th>
|
|
1521
|
-
<th>Source</th>
|
|
1522
|
-
<th>Target</th>
|
|
1523
|
-
<th>Summary</th>
|
|
1882
|
+
<th>Review</th>
|
|
1524
1883
|
<th>Actions</th>
|
|
1525
1884
|
</tr>
|
|
1526
1885
|
</thead>
|
|
1527
1886
|
<tbody id="review-table-body">
|
|
1528
|
-
<tr><td colspan="
|
|
1887
|
+
<tr><td colspan="3" class="muted">Loading...</td></tr>
|
|
1529
1888
|
</tbody>
|
|
1530
1889
|
</table>
|
|
1531
1890
|
</div>
|
|
@@ -1591,6 +1950,17 @@ def dashboard() -> HTMLResponse:
|
|
|
1591
1950
|
return Number.isNaN(parsed.getTime()) ? String(value) : parsed.toLocaleString();
|
|
1592
1951
|
}}
|
|
1593
1952
|
|
|
1953
|
+
function summarizeReviewItem(item) {{
|
|
1954
|
+
const sourceRef = item.source?.reference || item.reference || 'source memory';
|
|
1955
|
+
const targetRef = item.target?.reference || item.target_reference || 'target memory';
|
|
1956
|
+
const sourceText = item.source?.content || sourceRef;
|
|
1957
|
+
const targetText = item.target?.content || targetRef;
|
|
1958
|
+
const relation = item.relationship || (item.kind_label || item.kind || 'relationship').toLowerCase();
|
|
1959
|
+
const when = item.timestamp ? ` Reviewed signal from ${{formatTimestamp(item.timestamp)}}.` : '';
|
|
1960
|
+
const signal = item.signal ? ` Signal score: ${{item.signal}}.` : '';
|
|
1961
|
+
return `${{sourceRef}} may ${{relation.replaceAll('_', ' ')}} ${{targetRef}}. Source: “${{sourceText}}” Target: “${{targetText}}”.${{signal}}${{when}}`;
|
|
1962
|
+
}}
|
|
1963
|
+
|
|
1594
1964
|
function renderReviewTable() {{
|
|
1595
1965
|
const kindFilter = reviewKindFilterEl.value;
|
|
1596
1966
|
const priorityFilter = reviewPriorityFilterEl.value;
|
|
@@ -1608,29 +1978,20 @@ def dashboard() -> HTMLResponse:
|
|
|
1608
1978
|
reviewNoteEl.textContent = `${{filtered.length}} items shown${{reviewItems.length !== filtered.length ? ` of ${{reviewItems.length}}` : ''}} • Last refresh: ${{reviewLastRefresh ? formatTimestamp(reviewLastRefresh) : 'n/a'}}`;
|
|
1609
1979
|
|
|
1610
1980
|
if (!filtered.length) {{
|
|
1611
|
-
reviewTableBodyEl.innerHTML = '<tr><td colspan="
|
|
1981
|
+
reviewTableBodyEl.innerHTML = '<tr><td colspan="3" class="muted">No review items match the current filters.</td></tr>';
|
|
1612
1982
|
return;
|
|
1613
1983
|
}}
|
|
1614
1984
|
|
|
1615
1985
|
reviewTableBodyEl.innerHTML = filtered.map((item) => {{
|
|
1616
1986
|
const disabled = pendingReviewIds.has(item.review_id) ? 'disabled' : '';
|
|
1617
|
-
const
|
|
1618
|
-
const
|
|
1987
|
+
const reviewText = summarizeReviewItem(item);
|
|
1988
|
+
const summaryBits = [item.kind_label || item.kind, item.summary].filter(Boolean).join(' • ');
|
|
1619
1989
|
return `
|
|
1620
1990
|
<tr>
|
|
1621
1991
|
<td>${{escapeHtml(item.priority)}}</td>
|
|
1622
|
-
<td>${{escapeHtml(item.kind_label || item.kind)}}</td>
|
|
1623
|
-
<td>
|
|
1624
|
-
<strong>${{escapeHtml(item.reference)}}</strong><br/>
|
|
1625
|
-
<span class="muted">${{escapeHtml(sourceContent)}}</span>
|
|
1626
|
-
</td>
|
|
1627
|
-
<td>
|
|
1628
|
-
<strong>${{escapeHtml(item.target_reference)}}</strong><br/>
|
|
1629
|
-
<span class="muted">${{escapeHtml(targetContent)}}</span>
|
|
1630
|
-
</td>
|
|
1631
1992
|
<td>
|
|
1632
|
-
<strong>${{escapeHtml(
|
|
1633
|
-
<span class="muted">${{escapeHtml(
|
|
1993
|
+
<strong>${{escapeHtml(summaryBits || 'Governance review item')}}</strong><br/>
|
|
1994
|
+
<span class="muted">${{escapeHtml(reviewText)}}</span>
|
|
1634
1995
|
</td>
|
|
1635
1996
|
<td>
|
|
1636
1997
|
<button type="button" data-review-id="${{escapeHtml(item.review_id)}}" data-approved="true" ${{disabled}}>Approve</button>
|
|
@@ -1675,10 +2036,10 @@ def dashboard() -> HTMLResponse:
|
|
|
1675
2036
|
async function refreshGovernanceReview() {{
|
|
1676
2037
|
reviewErrorEl.textContent = '';
|
|
1677
2038
|
try {{
|
|
1678
|
-
const res = await fetch('/memory/governance/review', {{
|
|
2039
|
+
const res = await fetch('/memory/governance/review/summary', {{
|
|
1679
2040
|
method: 'POST',
|
|
1680
2041
|
headers: {{ 'Content-Type': 'application/json' }},
|
|
1681
|
-
body: JSON.stringify({{ limit:
|
|
2042
|
+
body: JSON.stringify({{ limit: 20, context_depth: 0, scan_limit: 250 }}),
|
|
1682
2043
|
}});
|
|
1683
2044
|
const data = await res.json();
|
|
1684
2045
|
if (!res.ok || !data.ok) {{
|
|
@@ -1689,7 +2050,7 @@ def dashboard() -> HTMLResponse:
|
|
|
1689
2050
|
renderReviewTable();
|
|
1690
2051
|
}} catch (error) {{
|
|
1691
2052
|
reviewErrorEl.textContent = error instanceof Error ? error.message : String(error);
|
|
1692
|
-
reviewTableBodyEl.innerHTML = '<tr><td colspan="
|
|
2053
|
+
reviewTableBodyEl.innerHTML = '<tr><td colspan="3" class="muted">Unable to load review items.</td></tr>';
|
|
1693
2054
|
}}
|
|
1694
2055
|
}}
|
|
1695
2056
|
|