@simbimbo/memory-ocmemog 0.1.11 → 0.1.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +83 -18
  3. package/brain/runtime/__init__.py +2 -12
  4. package/brain/runtime/config.py +1 -24
  5. package/brain/runtime/inference.py +1 -151
  6. package/brain/runtime/instrumentation.py +1 -15
  7. package/brain/runtime/memory/__init__.py +3 -13
  8. package/brain/runtime/memory/api.py +1 -1219
  9. package/brain/runtime/memory/candidate.py +1 -185
  10. package/brain/runtime/memory/conversation_state.py +1 -1823
  11. package/brain/runtime/memory/distill.py +1 -344
  12. package/brain/runtime/memory/embedding_engine.py +1 -92
  13. package/brain/runtime/memory/freshness.py +1 -112
  14. package/brain/runtime/memory/health.py +1 -40
  15. package/brain/runtime/memory/integrity.py +1 -186
  16. package/brain/runtime/memory/memory_consolidation.py +1 -58
  17. package/brain/runtime/memory/memory_links.py +1 -107
  18. package/brain/runtime/memory/memory_salience.py +1 -233
  19. package/brain/runtime/memory/memory_synthesis.py +1 -31
  20. package/brain/runtime/memory/memory_taxonomy.py +1 -33
  21. package/brain/runtime/memory/pondering_engine.py +1 -654
  22. package/brain/runtime/memory/promote.py +1 -277
  23. package/brain/runtime/memory/provenance.py +1 -406
  24. package/brain/runtime/memory/reinforcement.py +1 -71
  25. package/brain/runtime/memory/retrieval.py +1 -210
  26. package/brain/runtime/memory/semantic_search.py +1 -64
  27. package/brain/runtime/memory/store.py +1 -429
  28. package/brain/runtime/memory/unresolved_state.py +1 -91
  29. package/brain/runtime/memory/vector_index.py +1 -323
  30. package/brain/runtime/model_roles.py +1 -9
  31. package/brain/runtime/model_router.py +1 -22
  32. package/brain/runtime/providers.py +1 -66
  33. package/brain/runtime/security/redaction.py +1 -12
  34. package/brain/runtime/state_store.py +1 -23
  35. package/brain/runtime/storage_paths.py +1 -39
  36. package/docs/architecture/memory.md +20 -24
  37. package/docs/release-checklist.md +19 -6
  38. package/docs/usage.md +33 -17
  39. package/index.ts +8 -1
  40. package/ocmemog/__init__.py +11 -0
  41. package/ocmemog/doctor.py +1255 -0
  42. package/ocmemog/runtime/__init__.py +18 -0
  43. package/ocmemog/runtime/_compat_bridge.py +28 -0
  44. package/ocmemog/runtime/config.py +35 -0
  45. package/ocmemog/runtime/identity.py +115 -0
  46. package/ocmemog/runtime/inference.py +164 -0
  47. package/ocmemog/runtime/instrumentation.py +20 -0
  48. package/ocmemog/runtime/memory/__init__.py +91 -0
  49. package/ocmemog/runtime/memory/api.py +1431 -0
  50. package/ocmemog/runtime/memory/candidate.py +192 -0
  51. package/ocmemog/runtime/memory/conversation_state.py +1831 -0
  52. package/ocmemog/runtime/memory/distill.py +282 -0
  53. package/ocmemog/runtime/memory/embedding_engine.py +151 -0
  54. package/ocmemog/runtime/memory/freshness.py +114 -0
  55. package/ocmemog/runtime/memory/health.py +57 -0
  56. package/ocmemog/runtime/memory/integrity.py +208 -0
  57. package/ocmemog/runtime/memory/memory_consolidation.py +60 -0
  58. package/ocmemog/runtime/memory/memory_links.py +109 -0
  59. package/ocmemog/runtime/memory/memory_salience.py +235 -0
  60. package/ocmemog/runtime/memory/memory_synthesis.py +33 -0
  61. package/ocmemog/runtime/memory/memory_taxonomy.py +35 -0
  62. package/ocmemog/runtime/memory/pondering_engine.py +681 -0
  63. package/ocmemog/runtime/memory/promote.py +279 -0
  64. package/ocmemog/runtime/memory/provenance.py +408 -0
  65. package/ocmemog/runtime/memory/reinforcement.py +73 -0
  66. package/ocmemog/runtime/memory/retrieval.py +224 -0
  67. package/ocmemog/runtime/memory/semantic_search.py +66 -0
  68. package/ocmemog/runtime/memory/store.py +433 -0
  69. package/ocmemog/runtime/memory/unresolved_state.py +93 -0
  70. package/ocmemog/runtime/memory/vector_index.py +411 -0
  71. package/ocmemog/runtime/model_roles.py +16 -0
  72. package/ocmemog/runtime/model_router.py +29 -0
  73. package/ocmemog/runtime/providers.py +79 -0
  74. package/ocmemog/runtime/roles.py +92 -0
  75. package/ocmemog/runtime/security/__init__.py +8 -0
  76. package/ocmemog/runtime/security/redaction.py +17 -0
  77. package/ocmemog/runtime/state_store.py +34 -0
  78. package/ocmemog/runtime/storage_paths.py +70 -0
  79. package/ocmemog/sidecar/app.py +310 -23
  80. package/ocmemog/sidecar/compat.py +50 -13
  81. package/ocmemog/sidecar/transcript_watcher.py +318 -240
  82. package/openclaw.plugin.json +4 -0
  83. package/package.json +1 -1
  84. package/scripts/ocmemog-backfill-vectors.py +5 -3
  85. package/scripts/ocmemog-continuity-benchmark.py +1 -1
  86. package/scripts/ocmemog-demo.py +1 -1
  87. package/scripts/ocmemog-doctor.py +15 -0
  88. package/scripts/ocmemog-install.sh +29 -7
  89. package/scripts/ocmemog-integrated-proof.py +373 -0
  90. package/scripts/ocmemog-reindex-vectors.py +5 -3
  91. package/scripts/ocmemog-release-check.sh +330 -0
  92. package/scripts/ocmemog-sidecar.sh +4 -2
  93. package/scripts/ocmemog-test-rig.py +5 -3
  94. package/brain/runtime/memory/artifacts.py +0 -33
  95. package/brain/runtime/memory/context_builder.py +0 -112
  96. package/brain/runtime/memory/interaction_memory.py +0 -57
  97. package/brain/runtime/memory/memory_gate.py +0 -38
  98. package/brain/runtime/memory/memory_graph.py +0 -54
  99. package/brain/runtime/memory/person_identity.py +0 -83
  100. package/brain/runtime/memory/person_memory.py +0 -138
  101. package/brain/runtime/memory/sentiment_memory.py +0 -67
  102. package/brain/runtime/memory/tool_catalog.py +0 -68
@@ -12,6 +12,10 @@
12
12
  "timeoutMs": {
13
13
  "type": "number",
14
14
  "description": "Request timeout in milliseconds."
15
+ },
16
+ "token": {
17
+ "type": "string",
18
+ "description": "Optional auth token for protected ocmemog sidecar endpoints; sent as x-ocmemog-token and should match OCMEMOG_API_TOKEN."
15
19
  }
16
20
  }
17
21
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@simbimbo/memory-ocmemog",
3
- "version": "0.1.11",
3
+ "version": "0.1.12",
4
4
  "description": "Advanced OpenClaw memory plugin with durable recall, transcript-backed continuity, and sidecar APIs",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -14,11 +14,13 @@ os.environ.setdefault("OCMEMOG_LOCAL_LLM_BASE_URL", "http://127.0.0.1:18080/v1")
14
14
  os.environ.setdefault("OCMEMOG_LOCAL_LLM_MODEL", "qwen2.5-7b-instruct")
15
15
  os.environ.setdefault("OCMEMOG_LOCAL_EMBED_BASE_URL", "http://127.0.0.1:18081/v1")
16
16
  os.environ.setdefault("OCMEMOG_LOCAL_EMBED_MODEL", "nomic-embed-text-v1.5")
17
- os.environ.setdefault("BRAIN_EMBED_MODEL_PROVIDER", "local-openai")
18
- os.environ.setdefault("BRAIN_EMBED_MODEL_LOCAL", "")
17
+ os.environ.setdefault("OCMEMOG_EMBED_MODEL_PROVIDER", "local-openai")
18
+ os.environ.setdefault("OCMEMOG_EMBED_MODEL_LOCAL", "")
19
+ os.environ.setdefault("BRAIN_EMBED_MODEL_PROVIDER", os.environ["OCMEMOG_EMBED_MODEL_PROVIDER"])
20
+ os.environ.setdefault("BRAIN_EMBED_MODEL_LOCAL", os.environ["OCMEMOG_EMBED_MODEL_LOCAL"])
19
21
  os.environ.setdefault("OCMEMOG_STATE_DIR", str(REPO_ROOT / ".ocmemog-state"))
20
22
 
21
- from brain.runtime.memory import vector_index
23
+ from ocmemog.runtime.memory import vector_index
22
24
 
23
25
 
24
26
  def main() -> int:
@@ -12,7 +12,7 @@ REPO_ROOT = Path(__file__).resolve().parents[1]
12
12
  import sys
13
13
  sys.path.insert(0, str(REPO_ROOT))
14
14
 
15
- from brain.runtime.memory import store # noqa: E402
15
+ from ocmemog.runtime.memory import store # noqa: E402
16
16
  from ocmemog.sidecar import app # noqa: E402
17
17
 
18
18
 
@@ -13,7 +13,7 @@ import sys
13
13
  REPO_ROOT = Path(__file__).resolve().parents[1]
14
14
  sys.path.insert(0, str(REPO_ROOT))
15
15
 
16
- from brain.runtime.memory import store
16
+ from ocmemog.runtime.memory import store
17
17
 
18
18
  ENDPOINT = "http://127.0.0.1:17891"
19
19
 
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env python3
2
+ from __future__ import annotations
3
+
4
+ import os
5
+ import sys
6
+
7
+ ROOT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
8
+ if ROOT_DIR not in sys.path:
9
+ sys.path.insert(0, ROOT_DIR)
10
+
11
+ from ocmemog.doctor import main
12
+
13
+
14
+ if __name__ == "__main__":
15
+ raise SystemExit(main())
@@ -34,6 +34,10 @@ wait_for_label_unloaded() {
34
34
  return 1
35
35
  }
36
36
 
37
+ is_label_loaded() {
38
+ launchctl print "gui/$UID/$1" >/dev/null 2>&1
39
+ }
40
+
37
41
  bootstrap_label() {
38
42
  local label="$1"
39
43
  local rendered="$2"
@@ -55,14 +59,32 @@ bootstrap_label() {
55
59
 
56
60
  for plist in "$ROOT_DIR"/scripts/launchagents/com.openclaw.ocmemog.{sidecar,ponder,guard}.plist; do
57
61
  rendered="$LA_DIR/$(basename "$plist")"
58
- render_plist "$plist" "$rendered"
59
- plutil -lint "$rendered" >/dev/null
62
+ rendered_tmp="${rendered}.tmp"
63
+ render_plist "$plist" "$rendered_tmp"
64
+ plutil -lint "$rendered_tmp" >/dev/null
60
65
  label=$(basename "$plist" .plist)
61
- launchctl bootout "gui/$UID/$label" 2>/dev/null || true
62
- wait_for_label_unloaded "$label" || true
63
- bootstrap_label "$label" "$rendered"
64
- launchctl enable "gui/$UID/$label" 2>/dev/null || true
65
- launchctl kickstart -kp "gui/$UID/$label"
66
+ plist_changed="false"
67
+ if [[ -f "$rendered" ]] && cmp -s "$rendered" "$rendered_tmp"; then
68
+ plist_changed="false"
69
+ else
70
+ mv "$rendered_tmp" "$rendered"
71
+ plist_changed="true"
72
+ fi
73
+ rm -f "$rendered_tmp"
74
+
75
+ if is_label_loaded "$label"; then
76
+ if [[ "$plist_changed" == "true" ]]; then
77
+ launchctl bootout "gui/$UID/$label" 2>/dev/null || true
78
+ wait_for_label_unloaded "$label" || true
79
+ bootstrap_label "$label" "$rendered"
80
+ launchctl enable "gui/$UID/$label" 2>/dev/null || true
81
+ launchctl kickstart -kp "gui/$UID/$label"
82
+ fi
83
+ else
84
+ bootstrap_label "$label" "$rendered"
85
+ launchctl enable "gui/$UID/$label" 2>/dev/null || true
86
+ launchctl kickstart -kp "gui/$UID/$label"
87
+ fi
66
88
  echo "Loaded $label"
67
89
  done
68
90
 
@@ -0,0 +1,373 @@
1
+ #!/usr/bin/env python3
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import json
6
+ import os
7
+ import socket
8
+ import subprocess
9
+ import sys
10
+ import tempfile
11
+ import time
12
+ import uuid
13
+ from dataclasses import dataclass
14
+ from pathlib import Path
15
+ from typing import Any, IO, Optional
16
+ from urllib import error, request
17
+
18
+ ROOT_DIR = Path(__file__).resolve().parent.parent
19
+ DEFAULT_ENDPOINT = "http://127.0.0.1:17891"
20
+ DEFAULT_HOST = "127.0.0.1"
21
+ PYTHON_BIN = sys.executable
22
+
23
+
24
+ @dataclass
25
+ class SidecarSession:
26
+ process: subprocess.Popen[bytes]
27
+ endpoint: str
28
+ log_path: Path
29
+ log_handle: Optional[IO[bytes]] = None
30
+
31
+ def stop(self) -> None:
32
+ if self.process.poll() is not None:
33
+ if self.log_handle is not None:
34
+ self.log_handle.close()
35
+ return
36
+ self.process.terminate()
37
+ try:
38
+ self.process.wait(timeout=3)
39
+ except subprocess.TimeoutExpired:
40
+ self.process.kill()
41
+ finally:
42
+ if self.log_handle is not None:
43
+ self.log_handle.close()
44
+
45
+
46
+ def _free_port() -> int:
47
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
48
+ sock.bind((DEFAULT_HOST, 0))
49
+ return sock.getsockname()[1]
50
+
51
+
52
+ def _read_json(response: bytes) -> dict[str, Any]:
53
+ try:
54
+ return json.loads(response.decode("utf-8"))
55
+ except Exception:
56
+ return {}
57
+
58
+
59
+ def _post_json(endpoint: str, path: str, payload: dict[str, Any], *, timeout: int = 10) -> dict[str, Any]:
60
+ data = json.dumps(payload).encode("utf-8")
61
+ req = request.Request(f"{endpoint.rstrip('/')}{path}", data=data, method="POST")
62
+ req.add_header("Content-Type", "application/json")
63
+ with request.urlopen(req, timeout=timeout) as resp:
64
+ body = resp.read()
65
+ return _read_json(body)
66
+
67
+
68
+ def _get_json(endpoint: str, path: str, *, timeout: int = 10) -> dict[str, Any]:
69
+ req = request.Request(f"{endpoint.rstrip('/')}{path}", method="GET")
70
+ with request.urlopen(req, timeout=timeout) as resp:
71
+ return _read_json(resp.read())
72
+
73
+
74
+ def _wait_for_health(
75
+ endpoint: str,
76
+ *,
77
+ timeout: int = 20,
78
+ require_ready: bool = False,
79
+ sidecar_process: subprocess.Popen[bytes] | None = None,
80
+ ) -> dict[str, Any]:
81
+ deadline = time.time() + timeout
82
+ last_error = ""
83
+ last_payload: dict[str, Any] = {}
84
+ while time.time() < deadline:
85
+ try:
86
+ payload = _get_json(endpoint, "/healthz")
87
+ if sidecar_process is not None and sidecar_process.poll() is not None:
88
+ raise RuntimeError(
89
+ f"sidecar process exited during health check (code={sidecar_process.returncode})"
90
+ )
91
+ if isinstance(payload, dict):
92
+ last_payload = payload
93
+ if not isinstance(payload, dict) or not payload.get("ok"):
94
+ last_error = "health not ok"
95
+ time.sleep(0.2)
96
+ continue
97
+ if not require_ready or payload.get("ready") is not False:
98
+ return payload
99
+ last_error = f"health not ready (mode={payload.get('mode')})"
100
+ except Exception as exc:
101
+ last_error = str(exc)
102
+ time.sleep(0.2)
103
+ if require_ready and last_payload:
104
+ raise RuntimeError(f"health check did not reach ready: mode={last_payload.get('mode')} warnings={last_payload.get('warnings')}")
105
+ if not last_payload:
106
+ raise RuntimeError(f"health check failed: {last_error}")
107
+ return last_payload
108
+
109
+
110
+ def _assert_contract_payload(payload: dict[str, Any], *, require_ready: bool = False) -> None:
111
+ if not isinstance(payload, dict):
112
+ raise RuntimeError("invalid health payload format")
113
+ if not payload.get("ok"):
114
+ raise RuntimeError("health check returned ok=false")
115
+ if require_ready and payload.get("ready") is False:
116
+ raise RuntimeError(f"health check not ready (mode={payload.get('mode')})")
117
+ if payload.get("mode") == "degraded":
118
+ return
119
+
120
+
121
+ def _validate_proof_payload(payload: dict[str, Any]) -> None:
122
+ required = ("reference", "search_count", "linked_count")
123
+ missing = [key for key in required if payload.get(key) in (None, "") and key not in payload]
124
+ if missing:
125
+ raise RuntimeError(f"proof payload missing required fields: {', '.join(missing)}")
126
+ if not payload.get("ingest_ok"):
127
+ raise RuntimeError("proof ingest contract failed")
128
+ if not payload.get("search_ok"):
129
+ raise RuntimeError("proof search contract failed")
130
+ if not payload.get("get_ok"):
131
+ raise RuntimeError("proof get contract failed")
132
+ if not payload.get("hydrate_ok"):
133
+ raise RuntimeError("proof hydrate contract failed")
134
+ if int(payload.get("search_count") or 0) > 2:
135
+ raise RuntimeError("proof search returned unbounded results")
136
+ if int(payload.get("linked_count") or 0) > 2:
137
+ raise RuntimeError("proof hydrate returned unbounded linked memories")
138
+
139
+
140
+ def _run_sidecar_probe(endpoint: str, *, require_ready: bool = False, timeout: int = 180) -> dict[str, Any]:
141
+ _wait_for_health(endpoint, timeout=timeout, require_ready=require_ready)
142
+ health = _get_json(endpoint, "/healthz")
143
+ _assert_contract_payload(health, require_ready=require_ready)
144
+ result = run_probe(endpoint)
145
+ _validate_proof_payload(result)
146
+ return result
147
+
148
+
149
+ def _run_sidecar_probe_with_process(
150
+ endpoint: str,
151
+ sidecar_process: subprocess.Popen[bytes],
152
+ *,
153
+ require_ready: bool = False,
154
+ timeout: int = 180,
155
+ ) -> dict[str, Any]:
156
+ _wait_for_health(
157
+ endpoint,
158
+ timeout=timeout,
159
+ require_ready=require_ready,
160
+ sidecar_process=sidecar_process,
161
+ )
162
+ health = _get_json(endpoint, "/healthz")
163
+ _assert_contract_payload(health, require_ready=require_ready)
164
+ result = run_probe(endpoint)
165
+ _validate_proof_payload(result)
166
+ return result
167
+
168
+
169
+ def _start_sidecar(port: int, state_dir: Path) -> SidecarSession:
170
+ env = os.environ.copy()
171
+ env["PYTHONPATH"] = str(ROOT_DIR) + (os.pathsep + env["PYTHONPATH"] if env.get("PYTHONPATH") else "")
172
+ env["OCMEMOG_STATE_DIR"] = str(state_dir)
173
+ env["OCMEMOG_TRANSCRIPT_WATCHER"] = "false"
174
+ env["OCMEMOG_INGEST_ASYNC_WORKER"] = "true"
175
+ env["OCMEMOG_AUTO_HYDRATION"] = "false"
176
+ env["OCMEMOG_SEARCH_SKIP_EMBEDDING_PROVIDER"] = "true"
177
+ endpoint = f"http://{DEFAULT_HOST}:{port}"
178
+ env["OCMEMOG_HOST"] = DEFAULT_HOST
179
+ env["OCMEMOG_PORT"] = str(port)
180
+
181
+ log_path = state_dir / "sidecar-proof.log"
182
+ log_handle = log_path.open("ab")
183
+ proc = subprocess.Popen(
184
+ [PYTHON_BIN, "-m", "uvicorn", "ocmemog.sidecar.app:app", "--host", DEFAULT_HOST, "--port", str(port)],
185
+ cwd=str(ROOT_DIR),
186
+ env=env,
187
+ stdout=log_handle,
188
+ stderr=subprocess.STDOUT,
189
+ )
190
+ return SidecarSession(process=proc, endpoint=endpoint, log_path=log_path, log_handle=log_handle)
191
+
192
+
193
+ def _drain_ingest_queue(endpoint: str) -> None:
194
+ deadline = time.time() + 20
195
+ while time.time() < deadline:
196
+ status = _get_json(endpoint, "/memory/ingest_status")
197
+ if int(status.get("queueDepth", 0) or 0) <= 0:
198
+ return
199
+ time.sleep(0.25)
200
+ flush = _post_json(endpoint, "/memory/ingest_flush", {"limit": 0}, timeout=20)
201
+ if int(flush.get("queueDepth", 0) or 0) <= 0:
202
+ return
203
+ raise RuntimeError("ingest post-process queue did not drain within timeout")
204
+
205
+
206
+ def _derive_port(endpoint: str) -> int | None:
207
+ if "://" not in endpoint:
208
+ endpoint = f"{DEFAULT_HOST}:{endpoint}"
209
+ parsed = endpoint.rsplit(":", 1)[-1]
210
+ if parsed.isdigit():
211
+ value = int(parsed)
212
+ if value > 0:
213
+ return value
214
+ return None
215
+
216
+
217
+ def run_probe(endpoint: str) -> dict[str, Any]:
218
+ token = "proof-token-demo-12345"
219
+ conversation = "proof-conv-demo"
220
+ session = "proof-sess-demo"
221
+ thread = "proof-thread-demo"
222
+
223
+ ingest_payload = {
224
+ "content": f"I learned that the {token} is the canonical token for this verification.",
225
+ "kind": "memory",
226
+ "memory_type": "knowledge",
227
+ "source": "ocmemog-proof",
228
+ "conversation_id": conversation,
229
+ "session_id": session,
230
+ "thread_id": thread,
231
+ }
232
+ ingest_response = _post_json(endpoint, "/memory/ingest", ingest_payload, timeout=20)
233
+ if not ingest_response.get("ok"):
234
+ raise RuntimeError("/memory/ingest did not return ok")
235
+ reference = str(ingest_response.get("reference") or "")
236
+ if not reference:
237
+ raise RuntimeError("/memory/ingest response missing reference")
238
+
239
+ # /memory/ingest is synchronous for the core write; post-process queue may settle later.
240
+ search_response = _post_json(endpoint, "/memory/search", {"query": token, "limit": 2}, timeout=20)
241
+ if not search_response.get("ok"):
242
+ raise RuntimeError("/memory/search did not return ok")
243
+ results = list(search_response.get("results") or [])
244
+ if len(results) > 2:
245
+ raise RuntimeError("/memory/search exceeded limit and did not compact results")
246
+ if not results:
247
+ raise RuntimeError("/memory/search returned no results for a distinctive memory")
248
+ matched_reference = str(results[0].get("reference") or "")
249
+ if reference and reference != matched_reference:
250
+ # allow reordering when other memories are present, but require recalled memory present
251
+ if not any(str(item.get("reference") or "") == reference for item in results):
252
+ raise RuntimeError("/memory/search did not return the ingested memory reference")
253
+
254
+ get_response = _post_json(endpoint, "/memory/get", {"reference": reference}, timeout=20)
255
+ if not get_response.get("ok"):
256
+ raise RuntimeError("/memory/get for ingested reference failed")
257
+ if token not in str(get_response.get("memory", {}).get("content") or ""):
258
+ raise RuntimeError("/memory/get content did not match ingested distinctive token")
259
+
260
+ hydrate_response = _post_json(
261
+ endpoint,
262
+ "/conversation/hydrate",
263
+ {
264
+ "conversation_id": conversation,
265
+ "session_id": session,
266
+ "thread_id": thread,
267
+ "turns_limit": 2,
268
+ "memory_limit": 2,
269
+ },
270
+ timeout=20,
271
+ )
272
+ if not hydrate_response.get("ok"):
273
+ raise RuntimeError("/conversation/hydrate failed")
274
+ linked_memories = hydrate_response.get("linked_memories") or []
275
+ if len(linked_memories) > 2:
276
+ raise RuntimeError("/conversation/hydrate did not compact linked memories")
277
+
278
+ return {
279
+ "reference": reference,
280
+ "ingest_ok": True,
281
+ "search_ok": True,
282
+ "search_count": len(results),
283
+ "get_ok": True,
284
+ "hydrate_ok": True,
285
+ "linked_count": len(linked_memories),
286
+ "endpoint": endpoint,
287
+ }
288
+
289
+
290
+ def run_legacy_probe(endpoint: str) -> dict[str, Any]:
291
+ # Legacy check keeps state untouched and verifies a focused path read against existing sidecar.
292
+ probe_token = f"legacy-{uuid.uuid4().hex}"
293
+ payload = {
294
+ "content": f"Legacy probe content: {probe_token}",
295
+ "kind": "memory",
296
+ "memory_type": "knowledge",
297
+ }
298
+ ingest = _post_json(endpoint, "/memory/ingest", payload, timeout=20)
299
+ if not ingest.get("ok"):
300
+ raise RuntimeError("legacy /memory/ingest failed")
301
+ search = _post_json(endpoint, "/memory/search", {"query": probe_token, "limit": 1}, timeout=20)
302
+ results = list(search.get("results") or [])
303
+ if not results:
304
+ raise RuntimeError("legacy /memory/search did not return ingested probe")
305
+ return {
306
+ "endpoint": endpoint,
307
+ "token": probe_token,
308
+ "legacy_ok": True,
309
+ "search_count": len(results),
310
+ }
311
+
312
+
313
+ def main() -> int:
314
+ parser = argparse.ArgumentParser()
315
+ parser.add_argument("--endpoint", default=DEFAULT_ENDPOINT)
316
+ parser.add_argument("--timeout", type=int, default=180)
317
+ parser.add_argument("--start-sidecar", action="store_true")
318
+ parser.add_argument("--legacy-endpoint", default="")
319
+ parser.add_argument("--state-dir", default="")
320
+ args = parser.parse_args()
321
+
322
+ start = bool(args.start_sidecar)
323
+ endpoint = args.endpoint.rstrip("/")
324
+ state_dir: Path | None = Path(args.state_dir) if args.state_dir else None
325
+ if start:
326
+ if state_dir is None:
327
+ state_dir_ctx = tempfile.TemporaryDirectory(prefix="ocmemog-proof-")
328
+ state_dir = Path(state_dir_ctx.name)
329
+ else:
330
+ state_dir_ctx = None
331
+ state_dir.mkdir(parents=True, exist_ok=True)
332
+
333
+ port = _derive_port(endpoint) or _free_port()
334
+ session = None
335
+ try:
336
+ session = _start_sidecar(port, state_dir)
337
+ result = _run_sidecar_probe_with_process(
338
+ session.endpoint,
339
+ session.process,
340
+ require_ready=False,
341
+ timeout=args.timeout,
342
+ )
343
+ endpoint = session.endpoint
344
+ if args.legacy_endpoint:
345
+ legacy = run_legacy_probe(args.legacy_endpoint.rstrip("/"))
346
+ result.update({"legacy": legacy})
347
+ print(json.dumps(result, indent=2, sort_keys=True))
348
+ return 0
349
+ except Exception as exc:
350
+ print(f"ERROR: contract proof failed to start or probe sidecar: {exc}", file=sys.stderr)
351
+ return 1
352
+ finally:
353
+ if session is not None:
354
+ session.stop()
355
+ if state_dir_ctx is not None:
356
+ state_dir_ctx.cleanup()
357
+ else:
358
+ try:
359
+ result = _run_sidecar_probe(endpoint, require_ready=False, timeout=args.timeout)
360
+ if args.legacy_endpoint:
361
+ result.update({"legacy": run_legacy_probe(args.legacy_endpoint.rstrip("/"))})
362
+ print(json.dumps(result, indent=2, sort_keys=True))
363
+ return 0
364
+ except error.URLError as exc:
365
+ print(f"ERROR: sidecar not reachable at {endpoint}: {exc}", file=sys.stderr)
366
+ return 1
367
+ except Exception as exc:
368
+ print(f"ERROR: contract proof failed: {exc}", file=sys.stderr)
369
+ return 1
370
+
371
+
372
+ if __name__ == "__main__":
373
+ raise SystemExit(main())
@@ -13,11 +13,13 @@ os.environ.setdefault("OCMEMOG_LOCAL_LLM_BASE_URL", "http://127.0.0.1:18080/v1")
13
13
  os.environ.setdefault("OCMEMOG_LOCAL_LLM_MODEL", "qwen2.5-7b-instruct")
14
14
  os.environ.setdefault("OCMEMOG_LOCAL_EMBED_BASE_URL", "http://127.0.0.1:18081/v1")
15
15
  os.environ.setdefault("OCMEMOG_LOCAL_EMBED_MODEL", "nomic-embed-text-v1.5")
16
- os.environ.setdefault("BRAIN_EMBED_MODEL_PROVIDER", "local-openai")
17
- os.environ.setdefault("BRAIN_EMBED_MODEL_LOCAL", "")
16
+ os.environ.setdefault("OCMEMOG_EMBED_MODEL_PROVIDER", "local-openai")
17
+ os.environ.setdefault("OCMEMOG_EMBED_MODEL_LOCAL", "")
18
+ os.environ.setdefault("BRAIN_EMBED_MODEL_PROVIDER", os.environ["OCMEMOG_EMBED_MODEL_PROVIDER"])
19
+ os.environ.setdefault("BRAIN_EMBED_MODEL_LOCAL", os.environ["OCMEMOG_EMBED_MODEL_LOCAL"])
18
20
  os.environ.setdefault("OCMEMOG_STATE_DIR", str(REPO_ROOT / ".ocmemog-state"))
19
21
 
20
- from brain.runtime.memory import vector_index
22
+ from ocmemog.runtime.memory import vector_index
21
23
 
22
24
  if __name__ == "__main__":
23
25
  count = vector_index.rebuild_vector_index()