@simbimbo/memory-ocmemog 0.1.4
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 +59 -0
- package/LICENSE +21 -0
- package/README.md +223 -0
- package/brain/__init__.py +1 -0
- package/brain/runtime/__init__.py +13 -0
- package/brain/runtime/config.py +21 -0
- package/brain/runtime/inference.py +83 -0
- package/brain/runtime/instrumentation.py +17 -0
- package/brain/runtime/memory/__init__.py +13 -0
- package/brain/runtime/memory/api.py +152 -0
- package/brain/runtime/memory/artifacts.py +33 -0
- package/brain/runtime/memory/candidate.py +89 -0
- package/brain/runtime/memory/context_builder.py +87 -0
- package/brain/runtime/memory/conversation_state.py +1825 -0
- package/brain/runtime/memory/distill.py +198 -0
- package/brain/runtime/memory/embedding_engine.py +94 -0
- package/brain/runtime/memory/freshness.py +91 -0
- package/brain/runtime/memory/health.py +42 -0
- package/brain/runtime/memory/integrity.py +170 -0
- package/brain/runtime/memory/interaction_memory.py +57 -0
- package/brain/runtime/memory/memory_consolidation.py +60 -0
- package/brain/runtime/memory/memory_gate.py +38 -0
- package/brain/runtime/memory/memory_graph.py +54 -0
- package/brain/runtime/memory/memory_links.py +109 -0
- package/brain/runtime/memory/memory_salience.py +235 -0
- package/brain/runtime/memory/memory_synthesis.py +33 -0
- package/brain/runtime/memory/memory_taxonomy.py +35 -0
- package/brain/runtime/memory/person_identity.py +83 -0
- package/brain/runtime/memory/person_memory.py +138 -0
- package/brain/runtime/memory/pondering_engine.py +577 -0
- package/brain/runtime/memory/promote.py +237 -0
- package/brain/runtime/memory/provenance.py +356 -0
- package/brain/runtime/memory/reinforcement.py +73 -0
- package/brain/runtime/memory/retrieval.py +153 -0
- package/brain/runtime/memory/semantic_search.py +66 -0
- package/brain/runtime/memory/sentiment_memory.py +67 -0
- package/brain/runtime/memory/store.py +400 -0
- package/brain/runtime/memory/tool_catalog.py +68 -0
- package/brain/runtime/memory/unresolved_state.py +93 -0
- package/brain/runtime/memory/vector_index.py +270 -0
- package/brain/runtime/model_roles.py +11 -0
- package/brain/runtime/model_router.py +22 -0
- package/brain/runtime/providers.py +59 -0
- package/brain/runtime/security/__init__.py +3 -0
- package/brain/runtime/security/redaction.py +14 -0
- package/brain/runtime/state_store.py +25 -0
- package/brain/runtime/storage_paths.py +41 -0
- package/docs/architecture/memory.md +118 -0
- package/docs/release-checklist.md +34 -0
- package/docs/reports/ocmemog-code-audit-2026-03-14.md +155 -0
- package/docs/usage.md +223 -0
- package/index.ts +726 -0
- package/ocmemog/__init__.py +1 -0
- package/ocmemog/sidecar/__init__.py +1 -0
- package/ocmemog/sidecar/app.py +1068 -0
- package/ocmemog/sidecar/compat.py +74 -0
- package/ocmemog/sidecar/transcript_watcher.py +425 -0
- package/openclaw.plugin.json +18 -0
- package/package.json +60 -0
- package/scripts/install-ocmemog.sh +277 -0
- package/scripts/launchagents/com.openclaw.ocmemog.guard.plist +22 -0
- package/scripts/launchagents/com.openclaw.ocmemog.ponder.plist +22 -0
- package/scripts/launchagents/com.openclaw.ocmemog.sidecar.plist +27 -0
- package/scripts/ocmemog-context.sh +15 -0
- package/scripts/ocmemog-continuity-benchmark.py +178 -0
- package/scripts/ocmemog-demo.py +122 -0
- package/scripts/ocmemog-failover-test.sh +17 -0
- package/scripts/ocmemog-guard.sh +11 -0
- package/scripts/ocmemog-install.sh +93 -0
- package/scripts/ocmemog-load-test.py +106 -0
- package/scripts/ocmemog-ponder.sh +30 -0
- package/scripts/ocmemog-recall-test.py +58 -0
- package/scripts/ocmemog-reindex-vectors.py +14 -0
- package/scripts/ocmemog-reliability-soak.py +177 -0
- package/scripts/ocmemog-sidecar.sh +46 -0
- package/scripts/ocmemog-soak-report.py +58 -0
- package/scripts/ocmemog-soak-test.py +44 -0
- package/scripts/ocmemog-test-rig.py +345 -0
- package/scripts/ocmemog-transcript-append.py +45 -0
- package/scripts/ocmemog-transcript-watcher.py +8 -0
- package/scripts/ocmemog-transcript-watcher.sh +7 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
CONFIG="${OPENCLAW_CONFIG_PATH:-$HOME/.openclaw/openclaw.json}"
|
|
5
|
+
EXPECTED="memory-ocmemog"
|
|
6
|
+
|
|
7
|
+
CURRENT=$(jq -r '.plugins.slots.memory // ""' "$CONFIG" 2>/dev/null || echo "")
|
|
8
|
+
if [[ "$CURRENT" != "$EXPECTED" ]]; then
|
|
9
|
+
osascript -e "display notification \"Memory slot switched to $CURRENT\" with title \"OpenClaw Memory Alert\""
|
|
10
|
+
echo "ALERT: memory slot is $CURRENT" >> /tmp/ocmemog-guard.alerts.log
|
|
11
|
+
fi
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
5
|
+
LA_DIR="$HOME/Library/LaunchAgents"
|
|
6
|
+
export PATH="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:${PATH:-}"
|
|
7
|
+
|
|
8
|
+
mkdir -p "$LA_DIR"
|
|
9
|
+
|
|
10
|
+
render_plist() {
|
|
11
|
+
local src="$1"
|
|
12
|
+
local dest="$2"
|
|
13
|
+
python3 - "$src" "$dest" "$ROOT_DIR" <<'PY'
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
import sys
|
|
16
|
+
|
|
17
|
+
src = Path(sys.argv[1])
|
|
18
|
+
dest = Path(sys.argv[2])
|
|
19
|
+
root = sys.argv[3]
|
|
20
|
+
dest.write_text(src.read_text(encoding="utf-8").replace("__ROOT_DIR__", root), encoding="utf-8")
|
|
21
|
+
PY
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
wait_for_label_unloaded() {
|
|
25
|
+
local label="$1"
|
|
26
|
+
local attempts="${2:-25}"
|
|
27
|
+
local sleep_s="${3:-0.2}"
|
|
28
|
+
for ((i=0; i<attempts; i++)); do
|
|
29
|
+
if ! launchctl print "gui/$UID/$label" >/dev/null 2>&1; then
|
|
30
|
+
return 0
|
|
31
|
+
fi
|
|
32
|
+
sleep "$sleep_s"
|
|
33
|
+
done
|
|
34
|
+
return 1
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
bootstrap_label() {
|
|
38
|
+
local label="$1"
|
|
39
|
+
local rendered="$2"
|
|
40
|
+
local last_output=""
|
|
41
|
+
for attempt in 1 2 3; do
|
|
42
|
+
if last_output=$(launchctl bootstrap "gui/$UID" "$rendered" 2>&1); then
|
|
43
|
+
return 0
|
|
44
|
+
fi
|
|
45
|
+
if [[ "$last_output" == *"Input/output error"* ]]; then
|
|
46
|
+
sleep 1
|
|
47
|
+
continue
|
|
48
|
+
fi
|
|
49
|
+
printf '%s\n' "$last_output" >&2
|
|
50
|
+
return 1
|
|
51
|
+
done
|
|
52
|
+
printf '%s\n' "$last_output" >&2
|
|
53
|
+
return 1
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
for plist in "$ROOT_DIR"/scripts/launchagents/com.openclaw.ocmemog.{sidecar,ponder,guard}.plist; do
|
|
57
|
+
rendered="$LA_DIR/$(basename "$plist")"
|
|
58
|
+
render_plist "$plist" "$rendered"
|
|
59
|
+
plutil -lint "$rendered" >/dev/null
|
|
60
|
+
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
|
+
echo "Loaded $label"
|
|
67
|
+
done
|
|
68
|
+
|
|
69
|
+
if ! command -v ollama >/dev/null 2>&1; then
|
|
70
|
+
echo "Ollama not found. Install from: https://ollama.com/download"
|
|
71
|
+
echo "Then run: ollama pull phi3 && ollama pull nomic-embed-text"
|
|
72
|
+
exit 0
|
|
73
|
+
fi
|
|
74
|
+
|
|
75
|
+
if ! ollama list | rg -q "phi3"; then
|
|
76
|
+
echo "Pulling phi3..."
|
|
77
|
+
ollama pull phi3
|
|
78
|
+
fi
|
|
79
|
+
|
|
80
|
+
if ! ollama list | rg -q "nomic-embed-text"; then
|
|
81
|
+
echo "Pulling nomic-embed-text..."
|
|
82
|
+
ollama pull nomic-embed-text
|
|
83
|
+
fi
|
|
84
|
+
|
|
85
|
+
if ! command -v ffmpeg >/dev/null 2>&1; then
|
|
86
|
+
echo "ffmpeg not found. Install with: brew install ffmpeg"
|
|
87
|
+
fi
|
|
88
|
+
|
|
89
|
+
if [ -z "${OPENAI_API_KEY:-}" ]; then
|
|
90
|
+
echo "OPENAI_API_KEY not set. Whisper transcription will be disabled."
|
|
91
|
+
fi
|
|
92
|
+
|
|
93
|
+
echo "ocmemog install complete."
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import random
|
|
8
|
+
import time
|
|
9
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
10
|
+
from urllib import request as urlrequest
|
|
11
|
+
|
|
12
|
+
ENDPOINT = "http://127.0.0.1:17890"
|
|
13
|
+
ASYNC_DEFAULT = os.environ.get("OCMEMOG_INGEST_ASYNC_DEFAULT", "true").lower() in {"1", "true", "yes"}
|
|
14
|
+
INGEST_PATH = "/memory/ingest_async" if ASYNC_DEFAULT else "/memory/ingest"
|
|
15
|
+
|
|
16
|
+
QUERIES = [
|
|
17
|
+
"ssh key policy",
|
|
18
|
+
"synology nas",
|
|
19
|
+
"openclaw status --deep",
|
|
20
|
+
"ollama embeddings",
|
|
21
|
+
"memory pipeline",
|
|
22
|
+
"calix arden",
|
|
23
|
+
"gateway bind loopback",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def post(path: str, payload: dict, token: str | None = None) -> None:
|
|
28
|
+
data = json.dumps(payload).encode("utf-8")
|
|
29
|
+
req = urlrequest.Request(f"{ENDPOINT}{path}", data=data, method="POST")
|
|
30
|
+
req.add_header("Content-Type", "application/json")
|
|
31
|
+
if token:
|
|
32
|
+
req.add_header("x-ocmemog-token", token)
|
|
33
|
+
with urlrequest.urlopen(req, timeout=30) as resp:
|
|
34
|
+
resp.read()
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def run_load(mode: str, duration: int, concurrency: int, token: str | None) -> dict:
|
|
38
|
+
start = time.time()
|
|
39
|
+
latencies = []
|
|
40
|
+
errors = 0
|
|
41
|
+
total = 0
|
|
42
|
+
|
|
43
|
+
stop_at = start + duration
|
|
44
|
+
|
|
45
|
+
def worker():
|
|
46
|
+
nonlocal errors, total
|
|
47
|
+
while time.time() < stop_at:
|
|
48
|
+
t0 = time.time()
|
|
49
|
+
try:
|
|
50
|
+
if mode == "search":
|
|
51
|
+
query = random.choice(QUERIES)
|
|
52
|
+
post("/memory/search", {"query": query, "limit": 5}, token)
|
|
53
|
+
elif mode == "ingest":
|
|
54
|
+
content = f"load test {random.randint(1, 100000)}"
|
|
55
|
+
post(INGEST_PATH, {"content": content, "kind": "memory", "memory_type": "knowledge"}, token)
|
|
56
|
+
else:
|
|
57
|
+
if random.random() < 0.7:
|
|
58
|
+
query = random.choice(QUERIES)
|
|
59
|
+
post("/memory/search", {"query": query, "limit": 5}, token)
|
|
60
|
+
else:
|
|
61
|
+
content = f"load test {random.randint(1, 100000)}"
|
|
62
|
+
post(INGEST_PATH, {"content": content, "kind": "memory", "memory_type": "knowledge"}, token)
|
|
63
|
+
except Exception:
|
|
64
|
+
errors += 1
|
|
65
|
+
else:
|
|
66
|
+
latencies.append(time.time() - t0)
|
|
67
|
+
total += 1
|
|
68
|
+
|
|
69
|
+
with ThreadPoolExecutor(max_workers=concurrency) as pool:
|
|
70
|
+
futures = [pool.submit(worker) for _ in range(concurrency)]
|
|
71
|
+
for f in as_completed(futures):
|
|
72
|
+
pass
|
|
73
|
+
|
|
74
|
+
if latencies:
|
|
75
|
+
lat_sorted = sorted(latencies)
|
|
76
|
+
p95 = lat_sorted[int(len(lat_sorted) * 0.95) - 1]
|
|
77
|
+
avg = sum(latencies) / len(latencies)
|
|
78
|
+
else:
|
|
79
|
+
avg = 0
|
|
80
|
+
p95 = 0
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
"mode": mode,
|
|
84
|
+
"duration_s": duration,
|
|
85
|
+
"concurrency": concurrency,
|
|
86
|
+
"requests": total,
|
|
87
|
+
"errors": errors,
|
|
88
|
+
"avg_ms": round(avg * 1000, 2),
|
|
89
|
+
"p95_ms": round(p95 * 1000, 2),
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def main() -> None:
|
|
94
|
+
parser = argparse.ArgumentParser()
|
|
95
|
+
parser.add_argument("--mode", default="mixed", choices=["search", "ingest", "mixed"])
|
|
96
|
+
parser.add_argument("--duration", type=int, default=20)
|
|
97
|
+
parser.add_argument("--concurrency", type=int, default=10)
|
|
98
|
+
parser.add_argument("--token", default="")
|
|
99
|
+
args = parser.parse_args()
|
|
100
|
+
|
|
101
|
+
result = run_load(args.mode, args.duration, args.concurrency, args.token or None)
|
|
102
|
+
print(json.dumps(result, indent=2))
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
if __name__ == "__main__":
|
|
106
|
+
main()
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
ENDPOINT="${OCMEMOG_ENDPOINT:-http://127.0.0.1:17890}"
|
|
5
|
+
TOKEN="${OCMEMOG_API_TOKEN:-}"
|
|
6
|
+
MAX_ITEMS="${OCMEMOG_PONDER_ITEMS:-5}"
|
|
7
|
+
|
|
8
|
+
ARGS=("-H" "content-type: application/json")
|
|
9
|
+
if [[ -n "${TOKEN}" ]]; then
|
|
10
|
+
ARGS+=("-H" "x-ocmemog-token: ${TOKEN}")
|
|
11
|
+
fi
|
|
12
|
+
|
|
13
|
+
QUEUE_DEPTH=$(python3 - <<'PY'
|
|
14
|
+
import json, urllib.request, os
|
|
15
|
+
endpoint = os.environ.get("OCMEMOG_ENDPOINT", "http://127.0.0.1:17890")
|
|
16
|
+
req = urllib.request.Request(f"{endpoint}/memory/ingest_status")
|
|
17
|
+
with urllib.request.urlopen(req, timeout=10) as resp:
|
|
18
|
+
data = json.loads(resp.read().decode("utf-8"))
|
|
19
|
+
print(data.get("queueDepth", 0))
|
|
20
|
+
PY
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
if [[ "${QUEUE_DEPTH}" != "0" ]]; then
|
|
24
|
+
echo "ponder skipped: queueDepth=${QUEUE_DEPTH}" >/tmp/ocmemog-ponder.last.json
|
|
25
|
+
exit 0
|
|
26
|
+
fi
|
|
27
|
+
|
|
28
|
+
curl -s "${ENDPOINT}/memory/ponder" \
|
|
29
|
+
"${ARGS[@]}" \
|
|
30
|
+
-d "{\"max_items\":${MAX_ITEMS}}" >/tmp/ocmemog-ponder.last.json
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import subprocess
|
|
6
|
+
import time
|
|
7
|
+
from urllib import request as urlrequest
|
|
8
|
+
|
|
9
|
+
ENDPOINT = "http://127.0.0.1:17890"
|
|
10
|
+
QUERIES = [
|
|
11
|
+
"ssh key policy",
|
|
12
|
+
"synology nas",
|
|
13
|
+
"openclaw status --deep",
|
|
14
|
+
"ollama embeddings",
|
|
15
|
+
"memory pipeline",
|
|
16
|
+
"calix arden",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def post(path: str, payload: dict) -> dict:
|
|
21
|
+
data = json.dumps(payload).encode("utf-8")
|
|
22
|
+
req = urlrequest.Request(f"{ENDPOINT}{path}", data=data, method="POST")
|
|
23
|
+
req.add_header("Content-Type", "application/json")
|
|
24
|
+
with urlrequest.urlopen(req, timeout=20) as resp:
|
|
25
|
+
return json.loads(resp.read().decode("utf-8"))
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def run_search() -> dict:
|
|
29
|
+
results = {}
|
|
30
|
+
for q in QUERIES:
|
|
31
|
+
res = post("/memory/search", {"query": q, "limit": 5})
|
|
32
|
+
ids = [r.get("entry_id") for r in (res.get("results") or [])]
|
|
33
|
+
results[q] = ids
|
|
34
|
+
return results
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def overlap(a: list[str], b: list[str]) -> float:
|
|
38
|
+
if not a or not b:
|
|
39
|
+
return 0.0
|
|
40
|
+
return len(set(a) & set(b)) / max(1, len(set(a)))
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def main() -> None:
|
|
44
|
+
before = run_search()
|
|
45
|
+
import os
|
|
46
|
+
subprocess.run(["launchctl", "kickstart", "-k", f"gui/{os.getuid()}/com.openclaw.ocmemog.sidecar"], check=False)
|
|
47
|
+
time.sleep(2)
|
|
48
|
+
after = run_search()
|
|
49
|
+
|
|
50
|
+
rows = []
|
|
51
|
+
for q in QUERIES:
|
|
52
|
+
rows.append({"query": q, "overlap": round(overlap(before[q], after[q]), 3)})
|
|
53
|
+
|
|
54
|
+
print(json.dumps({"overlap": rows}, indent=2))
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
if __name__ == "__main__":
|
|
58
|
+
main()
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
REPO_ROOT = Path(__file__).resolve().parents[1]
|
|
8
|
+
sys.path.insert(0, str(REPO_ROOT))
|
|
9
|
+
|
|
10
|
+
from brain.runtime.memory import vector_index
|
|
11
|
+
|
|
12
|
+
if __name__ == "__main__":
|
|
13
|
+
count = vector_index.rebuild_vector_index()
|
|
14
|
+
print(f"reindexed: {count}")
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import json
|
|
6
|
+
import sys
|
|
7
|
+
import time
|
|
8
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
9
|
+
from typing import Any, Dict
|
|
10
|
+
from urllib import error as urlerror
|
|
11
|
+
from urllib import request as urlrequest
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _post(base_url: str, path: str, payload: Dict[str, Any], *, token: str | None, timeout: float) -> Dict[str, Any]:
|
|
15
|
+
req = urlrequest.Request(
|
|
16
|
+
f"{base_url}{path}",
|
|
17
|
+
data=json.dumps(payload).encode("utf-8"),
|
|
18
|
+
method="POST",
|
|
19
|
+
headers={"Content-Type": "application/json"},
|
|
20
|
+
)
|
|
21
|
+
if token:
|
|
22
|
+
req.add_header("x-ocmemog-token", token)
|
|
23
|
+
with urlrequest.urlopen(req, timeout=timeout) as resp:
|
|
24
|
+
return json.loads(resp.read().decode("utf-8"))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _run_sequence(base_url: str, *, token: str | None, timeout: float, worker_id: int, iteration: int) -> Dict[str, Any]:
|
|
28
|
+
session_id = f"soak-sess-{worker_id}"
|
|
29
|
+
thread_id = f"soak-thread-{worker_id}"
|
|
30
|
+
conversation_id = f"soak-conv-{worker_id}"
|
|
31
|
+
user_id = f"u-{worker_id}-{iteration}"
|
|
32
|
+
assistant_id = f"a-{worker_id}-{iteration}"
|
|
33
|
+
stamp = time.strftime("%Y-%m-%d %H:%M:%S")
|
|
34
|
+
|
|
35
|
+
ingest_user = _post(
|
|
36
|
+
base_url,
|
|
37
|
+
"/conversation/ingest_turn",
|
|
38
|
+
{
|
|
39
|
+
"role": "user",
|
|
40
|
+
"content": f"reliability soak iteration {iteration}: keep continuity hydrated",
|
|
41
|
+
"conversation_id": conversation_id,
|
|
42
|
+
"session_id": session_id,
|
|
43
|
+
"thread_id": thread_id,
|
|
44
|
+
"message_id": user_id,
|
|
45
|
+
"timestamp": stamp,
|
|
46
|
+
},
|
|
47
|
+
token=token,
|
|
48
|
+
timeout=timeout,
|
|
49
|
+
)
|
|
50
|
+
ingest_assistant = _post(
|
|
51
|
+
base_url,
|
|
52
|
+
"/conversation/ingest_turn",
|
|
53
|
+
{
|
|
54
|
+
"role": "assistant",
|
|
55
|
+
"content": f"I will checkpoint and ponder iteration {iteration}.",
|
|
56
|
+
"conversation_id": conversation_id,
|
|
57
|
+
"session_id": session_id,
|
|
58
|
+
"thread_id": thread_id,
|
|
59
|
+
"message_id": assistant_id,
|
|
60
|
+
"timestamp": stamp,
|
|
61
|
+
"metadata": {"reply_to_message_id": user_id},
|
|
62
|
+
},
|
|
63
|
+
token=token,
|
|
64
|
+
timeout=timeout,
|
|
65
|
+
)
|
|
66
|
+
hydrate = _post(
|
|
67
|
+
base_url,
|
|
68
|
+
"/conversation/hydrate",
|
|
69
|
+
{
|
|
70
|
+
"conversation_id": conversation_id,
|
|
71
|
+
"session_id": session_id,
|
|
72
|
+
"thread_id": thread_id,
|
|
73
|
+
"turns_limit": 12,
|
|
74
|
+
"memory_limit": 6,
|
|
75
|
+
},
|
|
76
|
+
token=token,
|
|
77
|
+
timeout=timeout,
|
|
78
|
+
)
|
|
79
|
+
checkpoint = _post(
|
|
80
|
+
base_url,
|
|
81
|
+
"/conversation/checkpoint",
|
|
82
|
+
{
|
|
83
|
+
"conversation_id": conversation_id,
|
|
84
|
+
"session_id": session_id,
|
|
85
|
+
"thread_id": thread_id,
|
|
86
|
+
"checkpoint_kind": "soak",
|
|
87
|
+
"turns_limit": 12,
|
|
88
|
+
},
|
|
89
|
+
token=token,
|
|
90
|
+
timeout=timeout,
|
|
91
|
+
)
|
|
92
|
+
checkpoint_expand = _post(
|
|
93
|
+
base_url,
|
|
94
|
+
"/conversation/checkpoint_expand",
|
|
95
|
+
{
|
|
96
|
+
"checkpoint_id": int((checkpoint.get("checkpoint") or {}).get("id") or 0),
|
|
97
|
+
"turns_limit": 24,
|
|
98
|
+
"radius_turns": 1,
|
|
99
|
+
},
|
|
100
|
+
token=token,
|
|
101
|
+
timeout=timeout,
|
|
102
|
+
)
|
|
103
|
+
ponder = _post(
|
|
104
|
+
base_url,
|
|
105
|
+
"/memory/ponder",
|
|
106
|
+
{"max_items": 4},
|
|
107
|
+
token=token,
|
|
108
|
+
timeout=timeout,
|
|
109
|
+
)
|
|
110
|
+
return {
|
|
111
|
+
"ok": all(
|
|
112
|
+
item.get("ok") is True
|
|
113
|
+
for item in (ingest_user, ingest_assistant, hydrate, checkpoint, checkpoint_expand, ponder)
|
|
114
|
+
),
|
|
115
|
+
"hydrateWarnings": hydrate.get("warnings", []),
|
|
116
|
+
"checkpointId": (checkpoint.get("checkpoint") or {}).get("id"),
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def main() -> None:
|
|
121
|
+
parser = argparse.ArgumentParser(description="Run a live reliability soak against the local ocmemog sidecar.")
|
|
122
|
+
parser.add_argument("--base-url", default="http://127.0.0.1:17890")
|
|
123
|
+
parser.add_argument("--iterations", type=int, default=8)
|
|
124
|
+
parser.add_argument("--workers", type=int, default=2)
|
|
125
|
+
parser.add_argument("--timeout", type=float, default=30.0)
|
|
126
|
+
parser.add_argument("--token", default="")
|
|
127
|
+
args = parser.parse_args()
|
|
128
|
+
|
|
129
|
+
started = time.monotonic()
|
|
130
|
+
failures = []
|
|
131
|
+
successes = 0
|
|
132
|
+
|
|
133
|
+
with ThreadPoolExecutor(max_workers=max(1, args.workers)) as pool:
|
|
134
|
+
futures = [
|
|
135
|
+
pool.submit(
|
|
136
|
+
_run_sequence,
|
|
137
|
+
args.base_url,
|
|
138
|
+
token=args.token or None,
|
|
139
|
+
timeout=args.timeout,
|
|
140
|
+
worker_id=(idx % max(1, args.workers)) + 1,
|
|
141
|
+
iteration=idx + 1,
|
|
142
|
+
)
|
|
143
|
+
for idx in range(max(1, args.iterations))
|
|
144
|
+
]
|
|
145
|
+
for future in as_completed(futures):
|
|
146
|
+
try:
|
|
147
|
+
result = future.result()
|
|
148
|
+
except Exception as exc:
|
|
149
|
+
failures.append({"error": str(exc)})
|
|
150
|
+
continue
|
|
151
|
+
if result.get("ok"):
|
|
152
|
+
successes += 1
|
|
153
|
+
else:
|
|
154
|
+
failures.append(result)
|
|
155
|
+
|
|
156
|
+
payload = {
|
|
157
|
+
"ok": not failures,
|
|
158
|
+
"iterations": max(1, args.iterations),
|
|
159
|
+
"workers": max(1, args.workers),
|
|
160
|
+
"successes": successes,
|
|
161
|
+
"failures": failures,
|
|
162
|
+
"elapsed_s": round(time.monotonic() - started, 3),
|
|
163
|
+
}
|
|
164
|
+
print(json.dumps(payload, indent=2))
|
|
165
|
+
if failures:
|
|
166
|
+
sys.exit(1)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
if __name__ == "__main__":
|
|
170
|
+
try:
|
|
171
|
+
main()
|
|
172
|
+
except urlerror.HTTPError as exc:
|
|
173
|
+
print(json.dumps({"ok": False, "error": f"http {exc.code}", "detail": exc.reason}, indent=2))
|
|
174
|
+
raise SystemExit(1)
|
|
175
|
+
except urlerror.URLError as exc:
|
|
176
|
+
print(json.dumps({"ok": False, "error": "unreachable", "detail": str(exc.reason)}, indent=2))
|
|
177
|
+
raise SystemExit(1)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
5
|
+
HOST="${OCMEMOG_HOST:-127.0.0.1}"
|
|
6
|
+
PORT="${OCMEMOG_PORT:-17890}"
|
|
7
|
+
PYTHON_BIN="${OCMEMOG_PYTHON_BIN:-}"
|
|
8
|
+
|
|
9
|
+
export PATH="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:${PATH:-}"
|
|
10
|
+
cd "${ROOT_DIR}"
|
|
11
|
+
|
|
12
|
+
export OCMEMOG_STATE_DIR="${OCMEMOG_STATE_DIR:-${ROOT_DIR}/.ocmemog-state}"
|
|
13
|
+
export PYTHONPATH="${ROOT_DIR}${PYTHONPATH:+:${PYTHONPATH}}"
|
|
14
|
+
mkdir -p "${OCMEMOG_STATE_DIR}" "${OCMEMOG_STATE_DIR}/logs"
|
|
15
|
+
|
|
16
|
+
# defaults for local ollama-backed inference/embeddings
|
|
17
|
+
export OCMEMOG_USE_OLLAMA="${OCMEMOG_USE_OLLAMA:-true}"
|
|
18
|
+
export OCMEMOG_OLLAMA_MODEL="${OCMEMOG_OLLAMA_MODEL:-phi3:latest}"
|
|
19
|
+
export OCMEMOG_OLLAMA_EMBED_MODEL="${OCMEMOG_OLLAMA_EMBED_MODEL:-nomic-embed-text:latest}"
|
|
20
|
+
export BRAIN_EMBED_MODEL_PROVIDER="${BRAIN_EMBED_MODEL_PROVIDER:-ollama}"
|
|
21
|
+
export BRAIN_EMBED_MODEL_LOCAL="${BRAIN_EMBED_MODEL_LOCAL:-}"
|
|
22
|
+
|
|
23
|
+
# always-on transcript watcher defaults
|
|
24
|
+
export OCMEMOG_TRANSCRIPT_WATCHER="${OCMEMOG_TRANSCRIPT_WATCHER:-true}"
|
|
25
|
+
export OCMEMOG_SESSION_DIR="${OCMEMOG_SESSION_DIR:-$HOME/.openclaw/agents/main/sessions}"
|
|
26
|
+
export OCMEMOG_TRANSCRIPT_POLL_SECONDS="${OCMEMOG_TRANSCRIPT_POLL_SECONDS:-30}"
|
|
27
|
+
export OCMEMOG_INGEST_BATCH_SECONDS="${OCMEMOG_INGEST_BATCH_SECONDS:-30}"
|
|
28
|
+
export OCMEMOG_INGEST_BATCH_MAX="${OCMEMOG_INGEST_BATCH_MAX:-25}"
|
|
29
|
+
export OCMEMOG_INGEST_ENDPOINT="${OCMEMOG_INGEST_ENDPOINT:-http://127.0.0.1:17890/memory/ingest_async}"
|
|
30
|
+
export OCMEMOG_REINFORCE_SENTIMENT="${OCMEMOG_REINFORCE_SENTIMENT:-true}"
|
|
31
|
+
export OCMEMOG_INGEST_SOURCE="${OCMEMOG_INGEST_SOURCE:-transcript}"
|
|
32
|
+
export OCMEMOG_INGEST_MEMORY_TYPE="${OCMEMOG_INGEST_MEMORY_TYPE:-reflections}"
|
|
33
|
+
|
|
34
|
+
# promotion/demotion thresholds for stress testing
|
|
35
|
+
export OCMEMOG_PROMOTION_THRESHOLD="${OCMEMOG_PROMOTION_THRESHOLD:-0.8}"
|
|
36
|
+
export OCMEMOG_DEMOTION_THRESHOLD="${OCMEMOG_DEMOTION_THRESHOLD:-0.4}"
|
|
37
|
+
|
|
38
|
+
if [[ -z "${PYTHON_BIN}" ]]; then
|
|
39
|
+
if [[ -x "${ROOT_DIR}/.venv/bin/python" ]]; then
|
|
40
|
+
PYTHON_BIN="${ROOT_DIR}/.venv/bin/python"
|
|
41
|
+
else
|
|
42
|
+
PYTHON_BIN="$(command -v python3)"
|
|
43
|
+
fi
|
|
44
|
+
fi
|
|
45
|
+
|
|
46
|
+
exec "${PYTHON_BIN}" -m uvicorn ocmemog.sidecar.app:app --host "${HOST}" --port "${PORT}"
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
REPO_ROOT = Path(__file__).resolve().parent.parent
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def main() -> None:
|
|
13
|
+
parser = argparse.ArgumentParser()
|
|
14
|
+
parser.add_argument("--in", dest="input", default=str(REPO_ROOT / "reports" / "load" / "soak-latest.jsonl"))
|
|
15
|
+
parser.add_argument("--out", default=str(REPO_ROOT / "reports" / "load" / "soak-report.html"))
|
|
16
|
+
args = parser.parse_args()
|
|
17
|
+
|
|
18
|
+
input_path = Path(args.input)
|
|
19
|
+
if not input_path.exists():
|
|
20
|
+
raise SystemExit(f"missing {input_path}")
|
|
21
|
+
|
|
22
|
+
rows = []
|
|
23
|
+
for line in input_path.read_text(encoding="utf-8").splitlines():
|
|
24
|
+
try:
|
|
25
|
+
rows.append(json.loads(line))
|
|
26
|
+
except Exception:
|
|
27
|
+
continue
|
|
28
|
+
|
|
29
|
+
if not rows:
|
|
30
|
+
raise SystemExit("no data")
|
|
31
|
+
|
|
32
|
+
def row_html(r):
|
|
33
|
+
return (
|
|
34
|
+
f"<tr><td>{r.get('timestamp')}</td>"
|
|
35
|
+
f"<td>{r.get('mode')}</td>"
|
|
36
|
+
f"<td>{r.get('concurrency')}</td>"
|
|
37
|
+
f"<td>{r.get('requests')}</td>"
|
|
38
|
+
f"<td>{r.get('errors')}</td>"
|
|
39
|
+
f"<td>{r.get('avg_ms')}</td>"
|
|
40
|
+
f"<td>{r.get('p95_ms')}</td></tr>"
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
html = """<html><head><title>ocmemog soak report</title>
|
|
44
|
+
<style>table{border-collapse:collapse}td,th{border:1px solid #ccc;padding:4px}</style>
|
|
45
|
+
</head><body>
|
|
46
|
+
<h2>ocmemog soak report</h2>
|
|
47
|
+
<table>
|
|
48
|
+
<tr><th>time</th><th>mode</th><th>concurrency</th><th>requests</th><th>errors</th><th>avg_ms</th><th>p95_ms</th></tr>
|
|
49
|
+
"""
|
|
50
|
+
html += "\n".join(row_html(r) for r in rows)
|
|
51
|
+
html += "</table></body></html>"
|
|
52
|
+
|
|
53
|
+
Path(args.out).write_text(html, encoding="utf-8")
|
|
54
|
+
print(args.out)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
if __name__ == "__main__":
|
|
58
|
+
main()
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import json
|
|
6
|
+
import time
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import subprocess
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
REPO_ROOT = Path(__file__).resolve().parent.parent
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def main() -> None:
|
|
16
|
+
parser = argparse.ArgumentParser()
|
|
17
|
+
parser.add_argument("--duration", type=int, default=3600)
|
|
18
|
+
parser.add_argument("--interval", type=int, default=60)
|
|
19
|
+
parser.add_argument("--concurrency", type=int, default=10)
|
|
20
|
+
parser.add_argument("--mode", default="mixed", choices=["search", "ingest", "mixed"])
|
|
21
|
+
parser.add_argument("--out", default=str(REPO_ROOT / "reports" / "load" / "soak-latest.jsonl"))
|
|
22
|
+
args = parser.parse_args()
|
|
23
|
+
|
|
24
|
+
out_path = Path(args.out)
|
|
25
|
+
out_path.parent.mkdir(parents=True, exist_ok=True)
|
|
26
|
+
|
|
27
|
+
start = time.time()
|
|
28
|
+
while time.time() - start < args.duration:
|
|
29
|
+
cmd = [
|
|
30
|
+
str(REPO_ROOT / "scripts" / "ocmemog-load-test.py"),
|
|
31
|
+
"--mode", args.mode,
|
|
32
|
+
"--duration", str(args.interval),
|
|
33
|
+
"--concurrency", str(args.concurrency),
|
|
34
|
+
]
|
|
35
|
+
result = subprocess.check_output(cmd).decode("utf-8").strip()
|
|
36
|
+
data = json.loads(result)
|
|
37
|
+
data["timestamp"] = time.strftime("%Y-%m-%dT%H:%M:%S")
|
|
38
|
+
with out_path.open("a", encoding="utf-8") as handle:
|
|
39
|
+
handle.write(json.dumps(data) + "\n")
|
|
40
|
+
time.sleep(1)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
if __name__ == "__main__":
|
|
44
|
+
main()
|