@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
package/openclaw.plugin.json
CHANGED
|
@@ -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
|
@@ -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("
|
|
18
|
-
os.environ.setdefault("
|
|
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
|
|
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
|
|
15
|
+
from ocmemog.runtime.memory import store # noqa: E402
|
|
16
16
|
from ocmemog.sidecar import app # noqa: E402
|
|
17
17
|
|
|
18
18
|
|
package/scripts/ocmemog-demo.py
CHANGED
|
@@ -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
|
-
|
|
59
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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("
|
|
17
|
-
os.environ.setdefault("
|
|
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
|
|
22
|
+
from ocmemog.runtime.memory import vector_index
|
|
21
23
|
|
|
22
24
|
if __name__ == "__main__":
|
|
23
25
|
count = vector_index.rebuild_vector_index()
|