@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,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}"
|