@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,277 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
5
+ TARGET_DIR="${ROOT_DIR}"
6
+ REPO_URL="${OCMEMOG_REPO_URL:-https://github.com/simbimbo/ocmemog.git}"
7
+ PLUGIN_PACKAGE="@simbimbo/memory-ocmemog"
8
+ PLUGIN_ID="memory-ocmemog"
9
+ ENDPOINT="${OCMEMOG_ENDPOINT:-http://127.0.0.1:17890}"
10
+ TIMEOUT_MS="${OCMEMOG_TIMEOUT_MS:-30000}"
11
+ DEFAULT_OLLAMA_MODEL="${OCMEMOG_OLLAMA_MODEL:-phi3:latest}"
12
+ DEFAULT_OLLAMA_EMBED_MODEL="${OCMEMOG_OLLAMA_EMBED_MODEL:-nomic-embed-text:latest}"
13
+ INSTALL_PREREQS="${OCMEMOG_INSTALL_PREREQS:-false}"
14
+ SKIP_PLUGIN_INSTALL="false"
15
+ SKIP_LAUNCHAGENTS="false"
16
+ SKIP_MODEL_PULLS="false"
17
+ DRY_RUN="false"
18
+
19
+ usage() {
20
+ cat <<'EOF'
21
+ Usage: scripts/install-ocmemog.sh [target-dir] [options]
22
+
23
+ Install/configure ocmemog for local OpenClaw use.
24
+
25
+ Arguments:
26
+ target-dir Optional clone/update target directory.
27
+
28
+ Options:
29
+ --help Show this help text.
30
+ --install-prereqs Auto-install missing ollama/ffmpeg via Homebrew.
31
+ --skip-plugin-install Skip OpenClaw plugin install/enable.
32
+ --skip-launchagents Skip LaunchAgent install/load.
33
+ --skip-model-pulls Skip local Ollama model pulls.
34
+ --dry-run Print what would happen without making changes.
35
+ --endpoint URL Override sidecar endpoint (default: http://127.0.0.1:17890).
36
+ --timeout-ms N Override plugin timeout summary value (default: 30000).
37
+ --repo-url URL Override git clone/update source.
38
+
39
+ Environment:
40
+ OCMEMOG_INSTALL_PREREQS=true Same as --install-prereqs.
41
+ OCMEMOG_OLLAMA_MODEL Default local model to pull.
42
+ OCMEMOG_OLLAMA_EMBED_MODEL Default local embedding model to pull.
43
+ EOF
44
+ }
45
+
46
+ while [[ $# -gt 0 ]]; do
47
+ case "$1" in
48
+ --help|-h)
49
+ usage
50
+ exit 0
51
+ ;;
52
+ --install-prereqs)
53
+ INSTALL_PREREQS="true"
54
+ shift
55
+ ;;
56
+ --skip-plugin-install)
57
+ SKIP_PLUGIN_INSTALL="true"
58
+ shift
59
+ ;;
60
+ --skip-launchagents)
61
+ SKIP_LAUNCHAGENTS="true"
62
+ shift
63
+ ;;
64
+ --skip-model-pulls)
65
+ SKIP_MODEL_PULLS="true"
66
+ shift
67
+ ;;
68
+ --dry-run)
69
+ DRY_RUN="true"
70
+ shift
71
+ ;;
72
+ --endpoint)
73
+ ENDPOINT="$2"
74
+ shift 2
75
+ ;;
76
+ --timeout-ms)
77
+ TIMEOUT_MS="$2"
78
+ shift 2
79
+ ;;
80
+ --repo-url)
81
+ REPO_URL="$2"
82
+ shift 2
83
+ ;;
84
+ --*)
85
+ printf 'Unknown option: %s\n\n' "$1" >&2
86
+ usage >&2
87
+ exit 1
88
+ ;;
89
+ *)
90
+ TARGET_DIR="$1"
91
+ shift
92
+ ;;
93
+ esac
94
+ done
95
+
96
+ export PATH="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:${PATH:-}"
97
+
98
+ log() {
99
+ printf '[ocmemog-install] %s\n' "$*"
100
+ }
101
+
102
+ warn() {
103
+ printf '[ocmemog-install] WARN: %s\n' "$*" >&2
104
+ }
105
+
106
+ have() {
107
+ command -v "$1" >/dev/null 2>&1
108
+ }
109
+
110
+ run_cmd() {
111
+ if [[ "$DRY_RUN" == "true" ]]; then
112
+ printf '[ocmemog-install] DRY RUN: '
113
+ printf '%q ' "$@"
114
+ printf '\n'
115
+ return 0
116
+ fi
117
+ "$@"
118
+ }
119
+
120
+ maybe_install_prereqs() {
121
+ if [[ "$INSTALL_PREREQS" != "true" ]]; then
122
+ return
123
+ fi
124
+ if ! have brew; then
125
+ warn "Homebrew not found; cannot auto-install prerequisites"
126
+ return
127
+ fi
128
+ if ! have ollama; then
129
+ log "Installing Ollama via Homebrew"
130
+ run_cmd brew install ollama || warn "brew install ollama failed"
131
+ fi
132
+ if ! have ffmpeg; then
133
+ log "Installing ffmpeg via Homebrew"
134
+ run_cmd brew install ffmpeg || warn "brew install ffmpeg failed"
135
+ fi
136
+ }
137
+
138
+ ensure_repo() {
139
+ if [[ "$TARGET_DIR" == "$ROOT_DIR" ]]; then
140
+ log "Using existing repo at $TARGET_DIR"
141
+ return
142
+ fi
143
+ if [[ -d "$TARGET_DIR/.git" ]]; then
144
+ log "Updating existing checkout at $TARGET_DIR"
145
+ run_cmd git -C "$TARGET_DIR" pull --ff-only
146
+ else
147
+ log "Cloning $REPO_URL to $TARGET_DIR"
148
+ run_cmd git clone "$REPO_URL" "$TARGET_DIR"
149
+ fi
150
+ ROOT_DIR="$TARGET_DIR"
151
+ }
152
+
153
+ ensure_python() {
154
+ if ! have python3; then
155
+ warn "python3 is required but not installed"
156
+ exit 1
157
+ fi
158
+ if [[ ! -x "$ROOT_DIR/.venv/bin/python" ]]; then
159
+ log "Creating virtualenv"
160
+ run_cmd python3 -m venv "$ROOT_DIR/.venv"
161
+ fi
162
+ if [[ "$DRY_RUN" == "true" ]]; then
163
+ log "Would install Python requirements into $ROOT_DIR/.venv"
164
+ return
165
+ fi
166
+ log "Installing Python requirements"
167
+ "$ROOT_DIR/.venv/bin/python" -m pip install --upgrade pip setuptools wheel
168
+ "$ROOT_DIR/.venv/bin/python" -m pip install -r "$ROOT_DIR/requirements.txt"
169
+ }
170
+
171
+ install_plugin() {
172
+ if [[ "$SKIP_PLUGIN_INSTALL" == "true" ]]; then
173
+ log "Skipping plugin install/enable by request"
174
+ return
175
+ fi
176
+ if ! have openclaw; then
177
+ warn "openclaw CLI not found; skipping plugin install/enable"
178
+ return
179
+ fi
180
+ log "Installing/enabling OpenClaw plugin if needed"
181
+ if [[ "$DRY_RUN" == "true" ]]; then
182
+ log "Would attempt package install: openclaw plugins install $PLUGIN_PACKAGE"
183
+ log "Would fall back to local path install if needed: openclaw plugins install -l $ROOT_DIR"
184
+ log "Would enable plugin: openclaw plugins enable $PLUGIN_ID"
185
+ return
186
+ fi
187
+ if openclaw plugins install "$PLUGIN_PACKAGE" >/dev/null 2>&1; then
188
+ log "Installed plugin package $PLUGIN_PACKAGE"
189
+ else
190
+ warn "Package install failed or package unavailable here; falling back to local path install"
191
+ openclaw plugins install -l "$ROOT_DIR"
192
+ fi
193
+ openclaw plugins enable "$PLUGIN_ID" || warn "Could not enable plugin automatically"
194
+ }
195
+
196
+ install_launchagents() {
197
+ if [[ "$SKIP_LAUNCHAGENTS" == "true" ]]; then
198
+ log "Skipping LaunchAgent install/load by request"
199
+ return
200
+ fi
201
+ if [[ ! -x "$ROOT_DIR/scripts/ocmemog-install.sh" ]]; then
202
+ warn "LaunchAgent installer missing at scripts/ocmemog-install.sh"
203
+ return
204
+ fi
205
+ log "Installing LaunchAgents"
206
+ run_cmd "$ROOT_DIR/scripts/ocmemog-install.sh"
207
+ }
208
+
209
+ ensure_ollama_models() {
210
+ if [[ "$SKIP_MODEL_PULLS" == "true" ]]; then
211
+ log "Skipping local model pulls by request"
212
+ return
213
+ fi
214
+ if ! have ollama; then
215
+ warn "Ollama not found. Install from https://ollama.com/download to enable local models."
216
+ return
217
+ fi
218
+ if ! ollama list | rg -q "$(printf '%s' "$DEFAULT_OLLAMA_MODEL" | sed 's/:.*$//')"; then
219
+ log "Pulling local model $DEFAULT_OLLAMA_MODEL"
220
+ run_cmd ollama pull "$DEFAULT_OLLAMA_MODEL"
221
+ fi
222
+ if ! ollama list | rg -q "$(printf '%s' "$DEFAULT_OLLAMA_EMBED_MODEL" | sed 's/:.*$//')"; then
223
+ log "Pulling local embed model $DEFAULT_OLLAMA_EMBED_MODEL"
224
+ run_cmd ollama pull "$DEFAULT_OLLAMA_EMBED_MODEL"
225
+ fi
226
+ }
227
+
228
+ validate_install() {
229
+ if ! have curl; then
230
+ warn "curl not found; skipping health check"
231
+ return
232
+ fi
233
+ if [[ "$DRY_RUN" == "true" ]]; then
234
+ log "Would validate sidecar health at $ENDPOINT/healthz"
235
+ return
236
+ fi
237
+ log "Waiting for sidecar health check at $ENDPOINT/healthz"
238
+ for _ in {1..20}; do
239
+ if curl -fsS --max-time 3 "$ENDPOINT/healthz" >/dev/null 2>&1; then
240
+ log "Sidecar is healthy"
241
+ return
242
+ fi
243
+ sleep 1
244
+ done
245
+ warn "Sidecar health check did not pass yet"
246
+ }
247
+
248
+ print_summary() {
249
+ cat <<EOF
250
+
251
+ ocmemog install summary
252
+ - repo: $ROOT_DIR
253
+ - endpoint: $ENDPOINT
254
+ - timeoutMs: $TIMEOUT_MS
255
+ - local model: $DEFAULT_OLLAMA_MODEL
256
+ - embed model: $DEFAULT_OLLAMA_EMBED_MODEL
257
+ - install prereqs automatically: $INSTALL_PREREQS
258
+ - skip plugin install: $SKIP_PLUGIN_INSTALL
259
+ - skip LaunchAgents: $SKIP_LAUNCHAGENTS
260
+ - skip model pulls: $SKIP_MODEL_PULLS
261
+ - dry run: $DRY_RUN
262
+
263
+ Next checks:
264
+ - openclaw plugins
265
+ - curl $ENDPOINT/healthz
266
+ - openclaw status --deep
267
+ EOF
268
+ }
269
+
270
+ ensure_repo
271
+ maybe_install_prereqs
272
+ ensure_python
273
+ install_plugin
274
+ install_launchagents
275
+ ensure_ollama_models
276
+ validate_install
277
+ print_summary
@@ -0,0 +1,22 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+ <plist version="1.0">
4
+ <dict>
5
+ <key>Label</key><string>com.openclaw.ocmemog.guard</string>
6
+ <key>ProgramArguments</key>
7
+ <array>
8
+ <string>/bin/bash</string>
9
+ <string>__ROOT_DIR__/scripts/ocmemog-guard.sh</string>
10
+ </array>
11
+ <key>RunAtLoad</key><true/>
12
+ <key>StartInterval</key><integer>900</integer>
13
+ <key>EnvironmentVariables</key>
14
+ <dict>
15
+ <key>PATH</key><string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
16
+ </dict>
17
+ <key>ProcessType</key><string>Background</string>
18
+ <key>StandardOutPath</key><string>/tmp/ocmemog-guard.out.log</string>
19
+ <key>StandardErrorPath</key><string>/tmp/ocmemog-guard.err.log</string>
20
+ <key>WorkingDirectory</key><string>__ROOT_DIR__</string>
21
+ </dict>
22
+ </plist>
@@ -0,0 +1,22 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+ <plist version="1.0">
4
+ <dict>
5
+ <key>Label</key><string>com.openclaw.ocmemog.ponder</string>
6
+ <key>ProgramArguments</key>
7
+ <array>
8
+ <string>/bin/bash</string>
9
+ <string>__ROOT_DIR__/scripts/ocmemog-ponder.sh</string>
10
+ </array>
11
+ <key>RunAtLoad</key><true/>
12
+ <key>StartInterval</key><integer>3600</integer>
13
+ <key>EnvironmentVariables</key>
14
+ <dict>
15
+ <key>PATH</key><string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
16
+ </dict>
17
+ <key>ProcessType</key><string>Background</string>
18
+ <key>StandardOutPath</key><string>/tmp/ocmemog-ponder.out.log</string>
19
+ <key>StandardErrorPath</key><string>/tmp/ocmemog-ponder.err.log</string>
20
+ <key>WorkingDirectory</key><string>__ROOT_DIR__</string>
21
+ </dict>
22
+ </plist>
@@ -0,0 +1,27 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+ <plist version="1.0">
4
+ <dict>
5
+ <key>Label</key><string>com.openclaw.ocmemog.sidecar</string>
6
+ <key>ProgramArguments</key>
7
+ <array>
8
+ <string>/bin/bash</string>
9
+ <string>__ROOT_DIR__/scripts/ocmemog-sidecar.sh</string>
10
+ </array>
11
+ <key>RunAtLoad</key><true/>
12
+ <key>KeepAlive</key>
13
+ <dict>
14
+ <key>SuccessfulExit</key><false/>
15
+ </dict>
16
+ <key>EnvironmentVariables</key>
17
+ <dict>
18
+ <key>PATH</key><string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
19
+ <key>PYTHONUNBUFFERED</key><string>1</string>
20
+ </dict>
21
+ <key>ProcessType</key><string>Background</string>
22
+ <key>ThrottleInterval</key><integer>10</integer>
23
+ <key>StandardOutPath</key><string>/tmp/ocmemog-sidecar.out.log</string>
24
+ <key>StandardErrorPath</key><string>/tmp/ocmemog-sidecar.err.log</string>
25
+ <key>WorkingDirectory</key><string>__ROOT_DIR__</string>
26
+ </dict>
27
+ </plist>
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ REF="${1:-}"
5
+ RADIUS="${2:-10}"
6
+ ENDPOINT="${OCMEMOG_ENDPOINT:-http://127.0.0.1:17890}"
7
+
8
+ if [[ -z "${REF}" ]]; then
9
+ echo "usage: ocmemog-context.sh <reference> [radius]" >&2
10
+ exit 1
11
+ fi
12
+
13
+ curl -s "${ENDPOINT}/memory/context" \
14
+ -H 'content-type: application/json' \
15
+ -d "{\"reference\":\"${REF}\",\"radius\":${RADIUS}}"
@@ -0,0 +1,178 @@
1
+ #!/usr/bin/env python3
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import json
6
+ import os
7
+ import tempfile
8
+ from pathlib import Path
9
+ from typing import Any, Dict, List
10
+
11
+ REPO_ROOT = Path(__file__).resolve().parents[1]
12
+ import sys
13
+ sys.path.insert(0, str(REPO_ROOT))
14
+
15
+ from brain.runtime.memory import store # noqa: E402
16
+ from ocmemog.sidecar import app # noqa: E402
17
+
18
+
19
+ def _reset_runtime(state_dir: str) -> None:
20
+ os.environ["OCMEMOG_STATE_DIR"] = state_dir
21
+ os.environ["OCMEMOG_TRANSCRIPT_ROOTS"] = state_dir
22
+ store._SCHEMA_READY = False
23
+ app.QUEUE_STATS.update({
24
+ "last_run": None,
25
+ "processed": 0,
26
+ "errors": 0,
27
+ "last_error": None,
28
+ "last_batch": 0,
29
+ })
30
+
31
+
32
+ def _message_id_list(items: List[Dict[str, Any]]) -> List[str]:
33
+ return [str(item.get("message_id") or "") for item in items if item.get("message_id")]
34
+
35
+
36
+ def _run_check(results: List[Dict[str, Any]], name: str, ok: bool, details: Dict[str, Any]) -> None:
37
+ results.append({"name": name, "ok": bool(ok), "details": details})
38
+
39
+
40
+ def run_scenario(scenario: Dict[str, Any]) -> Dict[str, Any]:
41
+ with tempfile.TemporaryDirectory() as tempdir:
42
+ _reset_runtime(tempdir)
43
+ scope = dict(scenario.get("scope") or {})
44
+ checkpoints = []
45
+ message_to_turn_id: Dict[str, int] = {}
46
+
47
+ for idx, turn in enumerate(scenario.get("turns") or []):
48
+ response = app.conversation_ingest_turn(
49
+ app.ConversationTurnRequest(
50
+ role=turn["role"],
51
+ content=turn["content"],
52
+ conversation_id=scope.get("conversation_id"),
53
+ session_id=scope.get("session_id"),
54
+ thread_id=scope.get("thread_id"),
55
+ message_id=turn.get("message_id"),
56
+ metadata=turn.get("metadata"),
57
+ timestamp=f"2026-03-15 18:00:{idx:02d}",
58
+ )
59
+ )
60
+ if turn.get("message_id"):
61
+ message_to_turn_id[str(turn["message_id"])] = int(response["turn_id"])
62
+ if turn.get("message_id") in set(scenario.get("checkpoint_after") or []):
63
+ checkpoints.append(
64
+ app.conversation_checkpoint(
65
+ app.ConversationCheckpointRequest(
66
+ conversation_id=scope.get("conversation_id"),
67
+ session_id=scope.get("session_id"),
68
+ thread_id=scope.get("thread_id"),
69
+ checkpoint_kind="benchmark",
70
+ turns_limit=32,
71
+ )
72
+ )["checkpoint"]
73
+ )
74
+
75
+ hydrate = app.conversation_hydrate(
76
+ app.ConversationHydrateRequest(
77
+ conversation_id=scope.get("conversation_id"),
78
+ session_id=scope.get("session_id"),
79
+ thread_id=scope.get("thread_id"),
80
+ turns_limit=32,
81
+ )
82
+ )
83
+
84
+ # Simulated restart/recovery: reset schema bootstrap and hydrate from persisted SQLite state.
85
+ store._SCHEMA_READY = False
86
+ recovered = app.conversation_hydrate(
87
+ app.ConversationHydrateRequest(
88
+ conversation_id=scope.get("conversation_id"),
89
+ session_id=scope.get("session_id"),
90
+ thread_id=scope.get("thread_id"),
91
+ turns_limit=32,
92
+ )
93
+ )
94
+
95
+ latest_checkpoint = checkpoints[-1] if checkpoints else None
96
+ checkpoint_expand = None
97
+ if latest_checkpoint:
98
+ checkpoint_expand = app.conversation_checkpoint_expand(
99
+ app.ConversationCheckpointExpandRequest(checkpoint_id=int(latest_checkpoint["id"]), turns_limit=48)
100
+ )
101
+ turn_expand = None
102
+ if scenario.get("turn_expand_message_id"):
103
+ turn_id = message_to_turn_id[str(scenario["turn_expand_message_id"])]
104
+ turn_expand = app.conversation_turn_expand(
105
+ app.ConversationTurnExpandRequest(turn_id=turn_id, radius_turns=8, turns_limit=48)
106
+ )
107
+
108
+ checks: List[Dict[str, Any]] = []
109
+ expect = dict(scenario.get("expect") or {})
110
+ latest_user_contains = str(expect.get("latest_user_contains") or "").strip()
111
+ if latest_user_contains:
112
+ value = str((hydrate.get("state") or {}).get("latest_user_ask") or "")
113
+ recovered_value = str((recovered.get("state") or {}).get("latest_user_ask") or "")
114
+ _run_check(checks, "hydrate.latest_user_contains", latest_user_contains in value, {"value": value, "expected": latest_user_contains})
115
+ _run_check(checks, "restart.latest_user_contains", latest_user_contains in recovered_value, {"value": recovered_value, "expected": latest_user_contains})
116
+
117
+ reply_chain_expected = list(expect.get("active_branch_reply_chain_contains") or [])
118
+ if reply_chain_expected:
119
+ reply_chain_ids = _message_id_list((hydrate.get("active_branch") or {}).get("reply_chain") or [])
120
+ recovered_reply_chain_ids = _message_id_list((recovered.get("active_branch") or {}).get("reply_chain") or [])
121
+ _run_check(checks, "hydrate.reply_chain_contains", all(item in reply_chain_ids for item in reply_chain_expected), {"value": reply_chain_ids, "expected": reply_chain_expected})
122
+ _run_check(checks, "restart.reply_chain_contains", all(item in recovered_reply_chain_ids for item in reply_chain_expected), {"value": recovered_reply_chain_ids, "expected": reply_chain_expected})
123
+
124
+ excluded = list(expect.get("active_branch_turns_exclude") or [])
125
+ if excluded:
126
+ branch_turn_ids = _message_id_list((hydrate.get("active_branch") or {}).get("turns") or [])
127
+ _run_check(checks, "hydrate.active_branch_excludes", all(item not in branch_turn_ids for item in excluded), {"value": branch_turn_ids, "excluded": excluded})
128
+
129
+ top_ranked_turn_message_id = str(expect.get("top_ranked_turn_message_id") or "").strip()
130
+ if top_ranked_turn_message_id and checkpoint_expand:
131
+ ranked = checkpoint_expand.get("salience_ranked_turns") or []
132
+ top_id = str((((ranked[0] if ranked else {}).get("turn") or {}).get("message_id") or ""))
133
+ _run_check(checks, "checkpoint_expand.top_ranked_turn", top_id == top_ranked_turn_message_id, {"value": top_id, "expected": top_ranked_turn_message_id})
134
+ if top_ranked_turn_message_id and turn_expand:
135
+ ranked = turn_expand.get("salience_ranked_turns") or []
136
+ top_id = str((((ranked[0] if ranked else {}).get("turn") or {}).get("message_id") or ""))
137
+ _run_check(checks, "turn_expand.top_ranked_turn", top_id == top_ranked_turn_message_id, {"value": top_id, "expected": top_ranked_turn_message_id})
138
+
139
+ passed = sum(1 for item in checks if item["ok"])
140
+ total = len(checks)
141
+ score = 1.0 if total == 0 else round(passed / total, 3)
142
+ return {
143
+ "name": scenario.get("name"),
144
+ "score": score,
145
+ "passed": passed,
146
+ "total": total,
147
+ "ok": passed == total,
148
+ "checks": checks,
149
+ "checkpoint_id": latest_checkpoint.get("id") if latest_checkpoint else None,
150
+ }
151
+
152
+
153
+ def main() -> int:
154
+ parser = argparse.ArgumentParser()
155
+ parser.add_argument("--fixture", default=str(REPO_ROOT / "tests" / "fixtures" / "continuity_benchmark.json"))
156
+ parser.add_argument("--report", default="")
157
+ args = parser.parse_args()
158
+
159
+ fixture = json.loads(Path(args.fixture).read_text(encoding="utf-8"))
160
+ scenarios = [run_scenario(item) for item in fixture.get("scenarios") or []]
161
+ overall_score = round(sum(item["score"] for item in scenarios) / max(len(scenarios), 1), 3)
162
+ continuity_bar = float(fixture.get("continuity_bar", 1.0))
163
+ report = {
164
+ "ok": overall_score >= continuity_bar and all(item["ok"] for item in scenarios),
165
+ "overall_score": overall_score,
166
+ "continuity_bar": continuity_bar,
167
+ "scenario_count": len(scenarios),
168
+ "scenarios": scenarios,
169
+ }
170
+ output = json.dumps(report, indent=2, sort_keys=True)
171
+ if args.report:
172
+ Path(args.report).write_text(output + "\n", encoding="utf-8")
173
+ print(output)
174
+ return 0 if report["ok"] else 1
175
+
176
+
177
+ if __name__ == "__main__":
178
+ raise SystemExit(main())
@@ -0,0 +1,122 @@
1
+ #!/usr/bin/env python3
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import json
6
+ import random
7
+ import time
8
+ from pathlib import Path
9
+ from urllib import request as urlrequest
10
+
11
+ import sys
12
+
13
+ REPO_ROOT = Path(__file__).resolve().parents[1]
14
+ sys.path.insert(0, str(REPO_ROOT))
15
+
16
+ from brain.runtime.memory import store
17
+
18
+ ENDPOINT = "http://127.0.0.1:17890"
19
+
20
+
21
+ def post(path: str, payload: dict) -> dict:
22
+ data = json.dumps(payload).encode("utf-8")
23
+ req = urlrequest.Request(f"{ENDPOINT}{path}", data=data, method="POST")
24
+ req.add_header("Content-Type", "application/json")
25
+ with urlrequest.urlopen(req, timeout=30) as resp:
26
+ return json.loads(resp.read().decode("utf-8"))
27
+
28
+
29
+ def get(path: str) -> dict:
30
+ with urlrequest.urlopen(f"{ENDPOINT}{path}", timeout=20) as resp:
31
+ return json.loads(resp.read().decode("utf-8"))
32
+
33
+
34
+ def demo_context_anchor() -> dict:
35
+ conn = store.connect()
36
+ row = conn.execute(
37
+ "SELECT source_reference, target_reference FROM memory_links WHERE link_type='transcript' ORDER BY rowid DESC LIMIT 1"
38
+ ).fetchone()
39
+ conn.close()
40
+ if not row:
41
+ return {"ok": False, "error": "no_transcript_links"}
42
+ reference = row[0]
43
+ context = post("/memory/context", {"reference": reference, "radius": 5})
44
+ return {"reference": reference, "context": context}
45
+
46
+
47
+ def demo_precision() -> dict:
48
+ queries = [
49
+ "ssh key policy",
50
+ "synology nas",
51
+ "openclaw status --deep",
52
+ "gateway bind loopback",
53
+ "ollama embeddings",
54
+ "memory pipeline",
55
+ "jira projects",
56
+ "calix arden",
57
+ ]
58
+ results = []
59
+ for query in queries:
60
+ t0 = time.time()
61
+ resp = post("/memory/search", {"query": query, "limit": 5})
62
+ elapsed = time.time() - t0
63
+ hits = 0
64
+ top = resp.get("results", []) or []
65
+ for item in top:
66
+ if any(token in str(item.get("content", "")).lower() for token in query.split()):
67
+ hits += 1
68
+ results.append({
69
+ "query": query,
70
+ "hits": hits,
71
+ "elapsed": round(elapsed, 3),
72
+ "top": [str(item.get("content", ""))[:160] for item in top[:2]],
73
+ })
74
+ hit_rate = round(sum(1 for r in results if r["hits"] > 0) / max(1, len(results)), 3)
75
+ return {"hit_rate": hit_rate, "samples": results}
76
+
77
+
78
+ def main() -> None:
79
+ parser = argparse.ArgumentParser()
80
+ parser.add_argument("--pretty", action="store_true")
81
+ args = parser.parse_args()
82
+
83
+ metrics = get("/metrics")
84
+ cold_count = 0
85
+ try:
86
+ conn = store.connect()
87
+ cold_count = conn.execute("SELECT COUNT(*) FROM cold_storage").fetchone()[0]
88
+ conn.close()
89
+ except Exception:
90
+ cold_count = 0
91
+
92
+ anchor = demo_context_anchor()
93
+ precision = demo_precision()
94
+
95
+ if args.pretty:
96
+ counts = metrics["metrics"]["counts"]
97
+ print("=== ocmemog demo (pretty) ===")
98
+ print(f"Memories: {counts}")
99
+ print(f"Cold storage: {cold_count}")
100
+ if anchor.get("context", {}).get("transcript", {}).get("snippet"):
101
+ snippet = anchor["context"]["transcript"]["snippet"].splitlines()[:5]
102
+ print("\nContext anchor snippet:")
103
+ for line in snippet:
104
+ print(f" {line}")
105
+ print("\nSearch quality (hit‑rate):", precision.get("hit_rate"))
106
+ for sample in precision.get("samples", [])[:6]:
107
+ print(f" - {sample['query']} → hits: {sample['hits']} (top: {sample['top'][0] if sample.get('top') else ''})")
108
+ return
109
+
110
+ print("=== ocmemog demo ===")
111
+ print(f"Memory counts: {metrics['metrics']['counts']}")
112
+ print(f"Cold storage count: {cold_count}")
113
+
114
+ print("\n--- Context anchor demo ---")
115
+ print(json.dumps(anchor, indent=2)[:1000])
116
+
117
+ print("\n--- Precision@5 sample ---")
118
+ print(json.dumps(precision, indent=2)[:1000])
119
+
120
+
121
+ if __name__ == "__main__":
122
+ main()
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
5
+ DURATION=${1:-30}
6
+ CONCURRENCY=${2:-10}
7
+ OUT=${3:-/tmp/ocmemog-failover.json}
8
+
9
+ "${ROOT_DIR}/scripts/ocmemog-load-test.py" \
10
+ --mode mixed --duration "${DURATION}" --concurrency "${CONCURRENCY}" > "${OUT}" &
11
+ PID=$!
12
+
13
+ sleep 5
14
+ launchctl kickstart -k gui/$UID/com.openclaw.ocmemog.sidecar
15
+
16
+ wait ${PID}
17
+ cat "${OUT}"