Skip to content

Commit 1102d43

Browse files
jkyberneeesclaude
andauthored
feat(memory): pluggable semantic embeddings via go-vector v1.3.0 (#27)
* feat(memory): pluggable semantic embeddings via go-vector v1.3.0 HTTPEmbedder Replace the hardwired RandomProjections usage across all memory similarity paths with a textEmbedder seam. The default stays RP (local, zero-cost, unchanged behavior); a new memory.embedding config section routes episode recall, episode dedup, the non-LLM ranker, and fact merge-on-write through any OpenAI-compatible embeddings API (Ollama, llama.cpp, OpenAI, ...) for real semantic similarity instead of bag-of-words overlap. - internal/memory/embedder.go: EmbeddingConfig + textEmbedder interface; rpTextEmbedder (corpus-fitted, bigram-featurized) and httpTextEmbedder (stateless, raw text, text->vector cache, batch misses-only refits). - episode_index.go: index owns a textEmbedder; persisted vectors carry an embedding-space fingerprint (episodes_index_meta.json) so switching provider/model/dims rebuilds automatically while legacy RP gobs keep loading; failed rebuilds back off 30s so a down backend is never re-hit on every loop turn. - episodes.go: embedder factory on EpisodeStore; findDuplicate and the embedder ranker fail safe (no dedup / recency order) on embed errors. - merge.go: MergeDetector classifies over the configured embedder; embed failures degrade to "nobody" (add without merge) instead of misclassifying. - config: memory.embedding overlay in resolveMemory + NewMemoryManager; ${ENV_VAR} expansion for base_url/api_key; 10s default timeout. - docs: CONFIG.md field reference + MEMORY.md mechanics/privacy notes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(memory): address PR #27 verification findings (off-lock rebuild + hardening) AI Verification Protocol pass (agents B/C/D) on PR #27 surfaced one HIGH finding plus defensive gaps. Repairs: - HIGH (2.1/2.5 network-I/O-under-lock): episodeVectorIndex.ensureFresh now fits + embeds the corpus on a FRESH embedder instance OFF the write lock, then swaps the store/embedder in atomically. A single-flight guard collapses concurrent rebuilds and a dirty-sequence check folds in a write that lands mid-rebuild. A slow/remote embedding backend can no longer serialize every concurrent recall behind one rebuild under odek serve. New test TestRebuildDoesNotSerializeRecall fails against the old lock-held-on-network code; whole package stays race-clean. - cosineVector now guards NaN/Inf (hostile/buggy embedding backend) so ranking order stays well-defined, mirroring MergeDetector.Classify. - MergeDetector.ReplaceEntry guards a desynced vecs/corpus (post-failed-Fit) so it can never index-panic. - Tests closing the verification coverage gaps: config-loader end-to-end parse of memory.embedding (C2), api_key ${ENV_VAR} expansion (C10), and the safety-critical failure-mode net — ranker recency fallback + merge add-without- merge (C12) and dedup-never-deletes-on-embed-error (C13). - Docs: corrected the rebuild-cache wording (one batch over the corpus, off lock), added the base_url egress/SSRF caveat, and the dims:0 model-change note. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(docker): llama.cpp embeddings sidecar + local model for memory.embedding Adds a private llama.cpp embeddings sidecar so the Docker setup uses real semantic embeddings for memory (recall, dedup, ranking, fact merge-on-write) out of the box — completing the consumer side of PR #27's pluggable embeddings. - docker/Dockerfile.embeddings: builds llama-server from source pinned to the same llama.cpp release as the main image (b9549, Bookworm base → no ABI mismatch) and bundles a pinned nomic-embed-text-v1.5 GGUF (768-dim, ~84 MB Q4_K_M) at a fixed HF revision. No first-run download, no volume — mirrors the bundled whisper / MiniCPM-V models. Runs --embeddings --pooling mean, non-root. - docker-compose.yml: llama-embeddings service on all 4 profiles, no host port (only the odek containers reach http://llama-embeddings:8080), /health healthcheck, wired into each odek service's depends_on. - config.restricted.json / config.godmode.json: memory.embedding → the sidecar. - README.md / .env.example / DOCKER_COMPOSE_USER_GUIDE.md: 'local memory embeddings out of the box' docs, quant/model override build args, disable path, and the egress-trust note. Verified: image builds; container reaches /health in ~20s; /v1/embeddings returns 768-dim vectors; cosine('fixed the auth bug','repaired the login issue')=0.70 vs 0.35 for an unrelated phrase — the semantic match RP cannot do. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore(docker): make embeddings + searxng integration settings explicit Both bundled configs already wired memory.embedding -> the llama.cpp sidecar and web_search -> the SearXNG sidecar; make those integration settings explicit and more robust: - memory.embedding: pin dims: 768 (nomic-embed-text-v1.5's output) so the HTTPEmbedder validates every response length and the index fingerprint is dimension-aware — a silent model dim change forces an explicit rebuild instead of self-healing to no-context. Add explicit timeout_seconds: 10. - web_search: add explicit timeout_seconds: 15 (the SearXNG query ceiling), so it is visible and tunable rather than relying on the implicit default. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(memory): fix flaky TestRebuildDoesNotSerializeRecall cleanup race The test launches the blocked-rebuild goroutine and returned right after close(release), so the rebuild's index-gob writes (saveLocked) could land in t.TempDir AFTER the test function returned, racing the TempDir RemoveAll cleanup — CI failed with 'directory not empty'. (Product code unaffected; purely a test-teardown ordering bug.) Fix: a deferred releaseBackend()+<-g1 unblocks the backend and waits for the rebuild goroutine to finish on every exit path (deferred funcs run before t.Cleanup), so the index is fully written before TempDir is removed. Verified 50x under -race. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 71c0609 commit 1102d43

23 files changed

Lines changed: 1650 additions & 143 deletions

docker/.env.example

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,16 @@ GIT_COMMITTER_EMAIL=you@example.com
6565
# ODEK_SCHEDULES_CATCHUP=false # run a missed fire once on startup
6666
# ODEK_SCHEDULES_ALLOW_TELEGRAM_MANAGEMENT=true # set false to make /schedule read-only (CLI-manages)
6767

68+
# ── Memory embeddings (llama.cpp sidecar; see docker/README.md) ──────────
69+
# The compose file runs a private llama.cpp server (the `llama-embeddings`
70+
# service) that gives the memory system real semantic embeddings. The model
71+
# (nomic-embed-text-v1.5) is BAKED INTO the image — no key, no first-run
72+
# download, nothing to set here. To change the model or quantization, rebuild
73+
# with build args, e.g.:
74+
# docker compose --profile restricted build \
75+
# --build-arg EMBED_QUANT=Q8_0 llama-embeddings
76+
# Then bump memory.embedding.model in config.*.json if you switch models.
77+
6878
# ── Web search (SearXNG sidecar; see docs/DOCKER_COMPOSE_USER_GUIDE.md) ──
6979
# The compose file runs a private SearXNG instance backing the `web_search`
7080
# tool. SearXNG reads this as server.secret_key at app load and it overrides

docker/Dockerfile.embeddings

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
# syntax=docker/dockerfile:1
2+
3+
# llama.cpp embeddings sidecar — backs odek's memory.embedding (http provider).
4+
#
5+
# Builds llama-server from source pinned to the SAME llama.cpp release as the
6+
# main image (LLAMA_VERSION), and bundles a pinned nomic-embed-text-v1.5 GGUF
7+
# so the service is fully offline after build — no first-run model download.
8+
# This mirrors how the main image bundles whisper.cpp + MiniCPM-V.
9+
#
10+
# nomic-embed-text-v1.5 is a 768-dim text embedding model (~84 MB at Q4_K_M).
11+
# odek sends raw summary/query text for both corpus and query, so the embedding
12+
# space stays symmetric even without nomic's optional task-instruction prefixes.
13+
#
14+
# Override the model with build args, e.g. a higher-quality quant:
15+
# --build-arg EMBED_QUANT=Q8_0
16+
# Available quants in this repo: Q4_K_M (default) | Q5_K_M | Q6_K | Q8_0 | f16
17+
18+
# ---- build stage: compile llama-server + fetch the model ----
19+
# Same Debian Bookworm base as the runtime stage so the binary links against the
20+
# exact glibc/libstdc++ present at runtime (no ABI mismatch — see the main
21+
# Dockerfile's llama.cpp stage for the rationale).
22+
FROM debian:bookworm-slim AS build
23+
ARG LLAMA_VERSION=b9549
24+
# Pin the embeddings model + git revision so the image is reproducible.
25+
ARG EMBED_HF_REPO=nomic-ai/nomic-embed-text-v1.5-GGUF
26+
ARG EMBED_HF_REVISION=0188c9bf409793f810680a5a431e7b899c46104c
27+
ARG EMBED_QUANT=Q4_K_M
28+
ARG EMBED_FILE=nomic-embed-text-v1.5.${EMBED_QUANT}.gguf
29+
30+
RUN apt-get update && apt-get install -y --no-install-recommends \
31+
git cmake make g++ curl ca-certificates \
32+
&& rm -rf /var/lib/apt/lists/*
33+
34+
# BUILD_SHARED_LIBS=OFF statically links llama's internal libraries; GGML_OPENMP
35+
# OFF drops the libgomp dependency; LLAMA_CURL=OFF because the model is bundled
36+
# (no runtime download). Target llama-server (the OpenAI-compatible HTTP server)
37+
# instead of the main image's llama-mtmd-cli.
38+
RUN git clone --depth 1 --branch "${LLAMA_VERSION}" \
39+
https://github.com/ggerganov/llama.cpp /llama \
40+
&& cmake -S /llama -B /llama/build \
41+
-DCMAKE_BUILD_TYPE=Release \
42+
-DBUILD_SHARED_LIBS=OFF \
43+
-DGGML_NATIVE=OFF \
44+
-DGGML_OPENMP=OFF \
45+
-DLLAMA_CURL=OFF \
46+
-DLLAMA_BUILD_TESTS=OFF \
47+
&& cmake --build /llama/build -j"$(nproc)" --target llama-server \
48+
&& install -m 755 /llama/build/bin/llama-server /usr/local/bin/llama-server \
49+
&& rm -rf /llama
50+
51+
# Fetch the pinned GGUF into a fixed image path.
52+
RUN mkdir -p /models \
53+
&& curl -fSL --retry 5 --retry-delay 5 --retry-connrefused --retry-all-errors \
54+
"https://huggingface.co/${EMBED_HF_REPO}/resolve/${EMBED_HF_REVISION}/${EMBED_FILE}" \
55+
-o /models/embed.gguf
56+
57+
# ---- runtime stage ----
58+
FROM debian:bookworm-slim
59+
# libstdc++6 for the C++ binary; curl only for the compose healthcheck;
60+
# ca-certificates is harmless and tiny. No outbound network is needed at runtime.
61+
RUN apt-get update && apt-get install -y --no-install-recommends \
62+
libstdc++6 ca-certificates curl \
63+
&& rm -rf /var/lib/apt/lists/*
64+
COPY --from=build /usr/local/bin/llama-server /usr/local/bin/llama-server
65+
COPY --from=build /models/embed.gguf /models/embed.gguf
66+
67+
# Run non-root (uid 1000), matching the main image's posture.
68+
RUN useradd -u 1000 -m llama
69+
USER llama
70+
EXPOSE 8080
71+
72+
# --embeddings enables the /v1/embeddings endpoint; --pooling mean matches
73+
# nomic-embed-text; --ctx-size 2048 is the model's training context (episode
74+
# summaries are capped at ~1 KB, well within it). Bind all interfaces — only
75+
# the compose network can reach it (no host port is published).
76+
ENTRYPOINT ["llama-server", \
77+
"--host", "0.0.0.0", "--port", "8080", \
78+
"--model", "/models/embed.gguf", \
79+
"--embeddings", "--pooling", "mean", "--ctx-size", "2048"]

docker/README.md

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ For the full walkthrough, threat model, and tuning, see
2121
```
2222
docker/
2323
├── Dockerfile # multi-stage build of the odek binary
24-
├── docker-compose.yml # 4 services across 4 profiles
24+
├── Dockerfile.embeddings # llama.cpp embeddings sidecar (bundled GGUF)
25+
├── docker-compose.yml # odek (4 profiles) + searxng + llama-embeddings
2526
├── config.restricted.json # Restricted permission policy
2627
├── config.godmode.json # Godmode (YOLO) permission policy
2728
├── .env.example # copy to .env, add your API key
@@ -187,6 +188,41 @@ sidecar** backing the `web_search` tool — no cloud search API, no keys.
187188
- To run **without** web search: comment out the `searxng` service (and the
188189
`depends_on: [searxng]` lines), and remove the `web_search` block from the configs.
189190

191+
## Local memory embeddings (out of the box)
192+
193+
The compose setup runs a **private [llama.cpp](https://github.com/ggerganov/llama.cpp)
194+
embeddings sidecar** backing odek's memory system — no cloud embeddings API, no keys.
195+
196+
Without it, memory similarity (episode recall, dedup, ranking, fact merge-on-write)
197+
runs on local bag-of-words vectors: fast, but purely lexical — *"fixed the auth bug"*
198+
and *"repaired login issue"* don't match. The sidecar swaps that for a real embedding
199+
model, so recall matches by **meaning**. See
200+
[`../docs/MEMORY.md`](../docs/MEMORY.md)*Pluggable Embeddings* and
201+
[`../docs/CONFIG.md`](../docs/CONFIG.md)`embedding`.
202+
203+
- The `llama-embeddings` service co-starts with every profile and is reachable only by
204+
the odek containers at `http://llama-embeddings:8080` (**no host port** — odek's memory
205+
is the only consumer). Both bundled configs set `memory.embedding` to it.
206+
- The image **bundles `llama-server` (built from source, pinned to the same llama.cpp
207+
release as the main image) and `nomic-embed-text-v1.5`** (768-dim, ~84 MB at Q4_K_M)
208+
— so there's **no first-run model download** and no volume, mirroring the bundled
209+
whisper / MiniCPM-V models. The server runs `--embeddings --pooling mean` and exposes
210+
the OpenAI-compatible `/v1/embeddings` endpoint.
211+
- **Graceful by design:** if the sidecar is still loading or unreachable, recall just
212+
degrades to "no context" and retries with a 30s backoff — the agent loop is never
213+
blocked, and a wrong dedup never deletes an episode. Default behavior without the
214+
service is local RandomProjections.
215+
- Want a higher-quality quantization? Rebuild with
216+
`--build-arg EMBED_QUANT=Q8_0` (available: `Q4_K_M` default, `Q5_K_M`, `Q6_K`, `Q8_0`,
217+
`f16`). To use a different model, override `EMBED_HF_REPO` / `EMBED_HF_REVISION` /
218+
`EMBED_FILE` and update `memory.embedding.model` in the configs.
219+
- To run **without** local embeddings: comment out the `llama-embeddings` service (and
220+
the matching `depends_on` entries), and remove the `embedding` block from the configs
221+
— memory falls back to RandomProjections automatically.
222+
- **Point `base_url` only at a server you trust:** every episode summary and fact entry
223+
is sent there for embedding. Here it's the in-network sidecar, so nothing leaves the
224+
compose network; if you repoint it at a cloud API, that text egresses.
225+
190226
## Verify the profiles differ
191227

192228
- **Restricted**: ask it to `rm -rf` everything in `/workspace` → denied, never runs.

docker/config.godmode.json

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@
1818
},
1919
"web_search": {
2020
"base_url": "http://searxng:8080",
21-
"max_results": 10
21+
"max_results": 10,
22+
"timeout_seconds": 15
2223
},
2324
"memory": {
2425
"enabled": true,
@@ -32,7 +33,14 @@
3233
"llm_extract": true,
3334
"llm_consolidate": true,
3435
"merge_threshold": 0.7,
35-
"add_threshold": 0.3
36+
"add_threshold": 0.3,
37+
"embedding": {
38+
"provider": "http",
39+
"base_url": "http://llama-embeddings:8080/v1",
40+
"model": "nomic-embed-text-v1.5",
41+
"dims": 768,
42+
"timeout_seconds": 10
43+
}
3644
},
3745
"dangerous": {
3846
"action": "allow",

docker/config.restricted.json

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@
1818
},
1919
"web_search": {
2020
"base_url": "http://searxng:8080",
21-
"max_results": 10
21+
"max_results": 10,
22+
"timeout_seconds": 15
2223
},
2324
"memory": {
2425
"enabled": true,
@@ -32,7 +33,14 @@
3233
"llm_extract": true,
3334
"llm_consolidate": true,
3435
"merge_threshold": 0.7,
35-
"add_threshold": 0.3
36+
"add_threshold": 0.3,
37+
"embedding": {
38+
"provider": "http",
39+
"base_url": "http://llama-embeddings:8080/v1",
40+
"model": "nomic-embed-text-v1.5",
41+
"dims": 768,
42+
"timeout_seconds": 10
43+
}
3644
},
3745
"dangerous": {
3846
"non_interactive": "deny",

docker/docker-compose.yml

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ services:
3030
- ./workspace:/workspace
3131
- ./config.restricted.json:/home/odek/.odek/config.json:ro
3232
restart: "no"
33-
depends_on: [searxng]
33+
depends_on: [searxng, llama-embeddings]
3434

3535
# ── Godmode (all permissions) — non-interactive, disposable container ──
3636
odek-godmode:
@@ -50,7 +50,7 @@ services:
5050
- ./workspace:/workspace
5151
- ./config.godmode.json:/home/odek/.odek/config.json:ro
5252
restart: "no"
53-
depends_on: [searxng]
53+
depends_on: [searxng, llama-embeddings]
5454

5555
# ── Telegram bot — Restricted (approvals via inline keyboards) ──
5656
# State (sessions, skills, telegram.pid) lives in the local ./.odek folder —
@@ -74,7 +74,7 @@ services:
7474
- ./.odek:/home/odek/.odek
7575
- ./config.restricted.json:/home/odek/.odek/config.json:ro
7676
restart: unless-stopped
77-
depends_on: [searxng]
77+
depends_on: [searxng, llama-embeddings]
7878

7979
# ── Telegram bot — Godmode (no prompts; unrestricted) ──
8080
# Same ./.odek state folder; config.godmode.json is layered on top of
@@ -93,7 +93,35 @@ services:
9393
- ./.odek:/home/odek/.odek
9494
- ./config.godmode.json:/home/odek/.odek/config.json:ro
9595
restart: unless-stopped
96-
depends_on: [searxng]
96+
depends_on: [searxng, llama-embeddings]
97+
98+
# ── llama.cpp embeddings — local backend for memory.embedding ──
99+
# Co-starts with every odek profile so the memory system gets real semantic
100+
# embeddings (recall, dedup, ranking, fact merge) out of the box — no cloud
101+
# embeddings API, no keys. Reachable only by the odek containers over the
102+
# compose network at http://llama-embeddings:8080 — NO host port is published
103+
# (odek's memory system is the only consumer). The GGUF is baked into the
104+
# image (see docker/Dockerfile.embeddings), so there is no first-run download
105+
# and no volume. Both bundled configs point memory.embedding.base_url here.
106+
# To run without local embeddings, comment this service and the matching
107+
# depends_on entries, and drop the "embedding" block from the configs (memory
108+
# then falls back to local RandomProjections — lexical, zero-cost).
109+
llama-embeddings:
110+
profiles: ["restricted", "godmode", "telegram-restricted", "telegram-godmode"]
111+
build:
112+
context: ..
113+
dockerfile: docker/Dockerfile.embeddings
114+
image: odek-embeddings:local
115+
# Health is observability only — odek tolerates the server being briefly
116+
# unavailable (recall degrades to "no context" and rebuilds back off 30s),
117+
# so startup is not gated on it. The model is bundled, so load is quick.
118+
healthcheck:
119+
test: ["CMD", "curl", "-fsS", "http://127.0.0.1:8080/health"]
120+
interval: 10s
121+
timeout: 5s
122+
retries: 30
123+
start_period: 60s
124+
restart: unless-stopped
97125

98126
# ── SearXNG — self-hosted metasearch backing the `web_search` tool ──
99127
# Co-starts with every odek profile so web search works out of the box.

docs/CONFIG.md

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,15 @@ The `memory` section controls the persistent memory system (see [docs/MEMORY.md]
206206
"auto_approve_episodes": false,
207207
"episode_dedup_threshold": 0.92,
208208
"max_episodes": 500,
209-
"episode_ttl_days": 0
209+
"episode_ttl_days": 0,
210+
"embedding": {
211+
"provider": "http",
212+
"base_url": "http://localhost:11434/v1",
213+
"model": "nomic-embed-text",
214+
"api_key": "${OPENAI_API_KEY}",
215+
"dims": 0,
216+
"timeout_seconds": 10
217+
}
210218
}
211219
}
212220
```
@@ -231,6 +239,48 @@ The `memory` section controls the persistent memory system (see [docs/MEMORY.md]
231239
| `episode_dedup_threshold` | 0.92 | Cosine similarity above which a newly written episode is treated as a near-duplicate of an existing one and **replaces** it (newest wins). An untrusted episode never replaces a trusted/approved one. `0` disables dedup. |
232240
| `max_episodes` | 500 | Maximum number of stored episodes. On each write, episodes beyond this count are evicted oldest-first (both the summary file and the index entry). `0` disables the cap. |
233241
| `episode_ttl_days` | 0 | Evict episodes older than this many days. `0` (default) disables TTL-based eviction. |
242+
| `embedding` | *(unset)* | Semantic embedding backend for episode recall, episode dedup, the non-LLM episode ranker, and fact merge-on-write. Unset = local RandomProjections (lexical bag-of-words — fast, zero-cost, but no real semantics). See below. |
243+
244+
### `embedding` — real semantic embeddings (optional)
245+
246+
By default every similarity computation in memory uses go-vector
247+
**RandomProjections**: a local, zero-dependency bag-of-words embedder. It is
248+
fast but purely lexical — *"fixed the auth bug"* and *"repaired login issue"*
249+
share no tokens and score ~0. Setting `embedding.provider` to `"http"` routes
250+
all of those paths through any **OpenAI-compatible embeddings API** instead
251+
(Ollama, llama.cpp server, LM Studio, vLLM, OpenAI, Voyage…), giving recall
252+
that matches by meaning.
253+
254+
| Field | Default | Description |
255+
|-------|---------|-------------|
256+
| `provider` | `"rp"` | `"rp"` = local RandomProjections; `"http"` = OpenAI-compatible embeddings API. An `"http"` config missing `base_url` or `model` silently falls back to `"rp"` so memory keeps working. |
257+
| `base_url` || API root, e.g. `http://localhost:11434/v1` (Ollama) or `https://api.openai.com/v1`. `${ENV_VAR}` expansion supported. |
258+
| `model` || Embedding model name, e.g. `nomic-embed-text`, `text-embedding-3-small`. |
259+
| `api_key` || Sent as `Authorization: Bearer <key>` when set. `${ENV_VAR}` expansion supported — keep secrets out of config files. |
260+
| `dims` | 0 | Expected vector dimensionality; `0` infers it from the first response (recommended). |
261+
| `timeout_seconds` | 10 | Per-request HTTP timeout. |
262+
263+
Operational notes:
264+
265+
- **Per-turn recall stays cheap.** Episode vectors live in a persisted index; a
266+
loop turn costs at most one embedding call (the query), bounded by
267+
`timeout_seconds`. If the backend is down, recall degrades to "no context"
268+
and rebuilds back off for 30s — the agent loop is never blocked. The index
269+
rebuild that follows a new episode (session-end) embeds the corpus on a fresh
270+
client *off* the index lock, so a slow backend never serializes concurrent
271+
recall; it is one batch call over the episode summaries.
272+
- **Switching backends is safe.** The persisted index records which embedding
273+
space it was built in; changing `provider`/`model`/`dims` automatically
274+
invalidates it and rebuilds on next use (one batch embedding call). Note: with
275+
`dims: 0`, if a server silently changes a model's output dimensionality (e.g.
276+
a model upgrade under the same name) the fingerprint cannot detect it; recall
277+
self-heals to "no context" on the dimension mismatch and rebuilds on the next
278+
write. Pin `dims` if you want such a change to force an explicit rebuild.
279+
- **`base_url` is an egress target — point it only at a server you trust.** Every
280+
episode summary and fact entry is POSTed there for embedding. The URL is used
281+
verbatim with no allowlist, so do not point it at internal/metadata endpoints
282+
(e.g. cloud metadata services) you would not otherwise expose. Prefer a local
283+
server (Ollama/llama.cpp) when episode/fact text must not leave the machine.
234284

235285
### ⚠️ `extract_facts` — automatic fact learning (opt-in, off by default)
236286

docs/DOCKER_COMPOSE_USER_GUIDE.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,9 +138,17 @@ The compose file also runs a private **SearXNG** metasearch sidecar that backs t
138138
It co-starts with every profile, is reachable only by the odek containers at
139139
`http://searxng:8080` (no host port), and needs only `SEARXNG_SECRET` set above —
140140
no Redis/Valkey. To disable web search, comment the `searxng` service and the
141-
`depends_on: [searxng]` lines in the compose file and drop the `web_search` block
141+
`depends_on` entries in the compose file and drop the `web_search` block
142142
from the config files.
143143

144+
A second sidecar, **`llama-embeddings`** (a llama.cpp server with a bundled
145+
`nomic-embed-text-v1.5` GGUF), gives the memory system real semantic embeddings —
146+
see [docker/README.md](../docker/README.md#local-memory-embeddings-out-of-the-box).
147+
It also co-starts on every profile, is reachable only at `http://llama-embeddings:8080`
148+
(no host port, no key, no first-run download), and both bundled configs point
149+
`memory.embedding` at it. Disable the same way: comment the service + its `depends_on`
150+
entries and drop the `embedding` block (memory falls back to local RandomProjections).
151+
144152
---
145153

146154
## 5. Permission policy files

0 commit comments

Comments
 (0)