@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.
Files changed (81) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/LICENSE +21 -0
  3. package/README.md +223 -0
  4. package/brain/__init__.py +1 -0
  5. package/brain/runtime/__init__.py +13 -0
  6. package/brain/runtime/config.py +21 -0
  7. package/brain/runtime/inference.py +83 -0
  8. package/brain/runtime/instrumentation.py +17 -0
  9. package/brain/runtime/memory/__init__.py +13 -0
  10. package/brain/runtime/memory/api.py +152 -0
  11. package/brain/runtime/memory/artifacts.py +33 -0
  12. package/brain/runtime/memory/candidate.py +89 -0
  13. package/brain/runtime/memory/context_builder.py +87 -0
  14. package/brain/runtime/memory/conversation_state.py +1825 -0
  15. package/brain/runtime/memory/distill.py +198 -0
  16. package/brain/runtime/memory/embedding_engine.py +94 -0
  17. package/brain/runtime/memory/freshness.py +91 -0
  18. package/brain/runtime/memory/health.py +42 -0
  19. package/brain/runtime/memory/integrity.py +170 -0
  20. package/brain/runtime/memory/interaction_memory.py +57 -0
  21. package/brain/runtime/memory/memory_consolidation.py +60 -0
  22. package/brain/runtime/memory/memory_gate.py +38 -0
  23. package/brain/runtime/memory/memory_graph.py +54 -0
  24. package/brain/runtime/memory/memory_links.py +109 -0
  25. package/brain/runtime/memory/memory_salience.py +235 -0
  26. package/brain/runtime/memory/memory_synthesis.py +33 -0
  27. package/brain/runtime/memory/memory_taxonomy.py +35 -0
  28. package/brain/runtime/memory/person_identity.py +83 -0
  29. package/brain/runtime/memory/person_memory.py +138 -0
  30. package/brain/runtime/memory/pondering_engine.py +577 -0
  31. package/brain/runtime/memory/promote.py +237 -0
  32. package/brain/runtime/memory/provenance.py +356 -0
  33. package/brain/runtime/memory/reinforcement.py +73 -0
  34. package/brain/runtime/memory/retrieval.py +153 -0
  35. package/brain/runtime/memory/semantic_search.py +66 -0
  36. package/brain/runtime/memory/sentiment_memory.py +67 -0
  37. package/brain/runtime/memory/store.py +400 -0
  38. package/brain/runtime/memory/tool_catalog.py +68 -0
  39. package/brain/runtime/memory/unresolved_state.py +93 -0
  40. package/brain/runtime/memory/vector_index.py +270 -0
  41. package/brain/runtime/model_roles.py +11 -0
  42. package/brain/runtime/model_router.py +22 -0
  43. package/brain/runtime/providers.py +59 -0
  44. package/brain/runtime/security/__init__.py +3 -0
  45. package/brain/runtime/security/redaction.py +14 -0
  46. package/brain/runtime/state_store.py +25 -0
  47. package/brain/runtime/storage_paths.py +41 -0
  48. package/docs/architecture/memory.md +118 -0
  49. package/docs/release-checklist.md +34 -0
  50. package/docs/reports/ocmemog-code-audit-2026-03-14.md +155 -0
  51. package/docs/usage.md +223 -0
  52. package/index.ts +726 -0
  53. package/ocmemog/__init__.py +1 -0
  54. package/ocmemog/sidecar/__init__.py +1 -0
  55. package/ocmemog/sidecar/app.py +1068 -0
  56. package/ocmemog/sidecar/compat.py +74 -0
  57. package/ocmemog/sidecar/transcript_watcher.py +425 -0
  58. package/openclaw.plugin.json +18 -0
  59. package/package.json +60 -0
  60. package/scripts/install-ocmemog.sh +277 -0
  61. package/scripts/launchagents/com.openclaw.ocmemog.guard.plist +22 -0
  62. package/scripts/launchagents/com.openclaw.ocmemog.ponder.plist +22 -0
  63. package/scripts/launchagents/com.openclaw.ocmemog.sidecar.plist +27 -0
  64. package/scripts/ocmemog-context.sh +15 -0
  65. package/scripts/ocmemog-continuity-benchmark.py +178 -0
  66. package/scripts/ocmemog-demo.py +122 -0
  67. package/scripts/ocmemog-failover-test.sh +17 -0
  68. package/scripts/ocmemog-guard.sh +11 -0
  69. package/scripts/ocmemog-install.sh +93 -0
  70. package/scripts/ocmemog-load-test.py +106 -0
  71. package/scripts/ocmemog-ponder.sh +30 -0
  72. package/scripts/ocmemog-recall-test.py +58 -0
  73. package/scripts/ocmemog-reindex-vectors.py +14 -0
  74. package/scripts/ocmemog-reliability-soak.py +177 -0
  75. package/scripts/ocmemog-sidecar.sh +46 -0
  76. package/scripts/ocmemog-soak-report.py +58 -0
  77. package/scripts/ocmemog-soak-test.py +44 -0
  78. package/scripts/ocmemog-test-rig.py +345 -0
  79. package/scripts/ocmemog-transcript-append.py +45 -0
  80. package/scripts/ocmemog-transcript-watcher.py +8 -0
  81. 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()