Everything below is prepared and verified; nothing is deployed. Tomorrow morning: merge three gated PRs in order, click four Coolify apps into existence, run one corpus migration, mint two fresh secrets, smoke each hop. The recommended cut keeps the agent seam local for one release — one moving piece less.
Three PRs carry the stack. The order matters: #96 is the deploy vehicle (the oll-memory service the other two call), #95 layers the seam's scout verbs on top, #94 adds the scout agent service. All three were CI-green at prep time (2026-07-14). Per the house merge gate, each needs an explicit per-PR go — merging is itself a morning console act.
| Order | PR | Branch | What it lands |
|---|---|---|---|
| 1st | #96 | feat/oll-memory | The memory spine — pgvector RAG service (services/oll-memory), caller metadata, stable-id upsert. Everything else deploys against this. |
| 2nd | #95 | feat/oll-mcp-thin | The Agent Seam — thin MCP adapter + 7 job-scout verbs (contract v5, 185 tests). Stays a local runtime in this release (§7). |
| 3rd | #94 | feat/oll-scout-agentic | The agentic scout MVP — JSON-LD source + cited-rationale scorer + dedup (58 tests). |
main before its go — do not merge through a red or stale check.Two pieces of the stack are stateless callers (the seam, the OpenCLAW scout loop) and two are the things worth hosting (the corpus, the GUI). The minimal cut moves only the second pair to the VPS. The seam keeps running locally exactly as today — it just points its OLL_MEMORY_BASE at the deployed service instead of the laptop container. One moving piece less on deploy morning; the seam's containerization becomes a calm follow-up instead of a morning blocker (it has no Dockerfile yet — §10).
The follow-up (not tomorrow): Dockerfile for services/oll-mcp → Coolify app → OpenCLAW seam URL flips host.docker.internal:5097 → the deployed seam.
X-Service-Token, fresh secret — §8) is the only door.oll_memoryoll_memory.CREATE EXTENSION IF NOT EXISTS vector; (the migration block in §6 includes this — the service's boot hook also attempts it, but don't rely on lazy creation for a prod deploy).DATABASE_URL.ollama/ollama, attach to network ollam, network alias ollama, persistent volume on /root/.ollama (models survive redeploys). No public domain — internal only.docker exec <ollama-container> ollama pull nomic-embed-text.| Coolify field | Value |
|---|---|
| Source | bytesbysamu/oll-am · branch main (after the §1 merges) |
| Base Directory | /services/oll-memory |
| Dockerfile | Dockerfile (in the base dir; non-root, HEALTHCHECK on /api/health) |
| Watch Paths | services/oll-memory/** |
| Network / alias | ollam · oll-memory |
| Port / domain | PORT=5008 (set explicitly — Coolify's default injection is 3000) · https://memory.oll.am |
| Auto-deploy | OFF once healthy (frozen-once-working, like every backend app) |
| Var | Value | Why |
|---|---|---|
| DATABASE_URL | postgresql://…neon…/oll_memory | Its own private DB (database-per-service). Boot-gated: missing = refuses to start. |
| OLL_MEMORY_SERVICE_TOKEN | <fresh secret — §8> | NOT poc-token. Every caller sends it as X-Service-Token. |
| EMBED_PROVIDER | ollama | Real semantic embeddings. This makes OLLAMA_EMBED_BASE_URL boot-required — fail-loud by design. |
| OLLAMA_EMBED_BASE_URL | http://ollama:11434 | The §3b app over the internal ollam network — no public hop. |
| EXTRACT_PROVIDER | mock (default — omit) | The scout stack only queries/ingests; real extraction (needs OLL_MODEL_* env) can be enabled later without a rebuild. |
| PORT | 5008 | Keeps the internal address oll-memory:5008 true for every caller. |
ollscout lives in its own repo (bytesbysamu/ollscout, like ollwrite). The production Dockerfile is committed and verified (939d407): Next.js 15 standalone, multi-stage node:22-alpine, non-root, HEALTHCHECK on 127.0.0.1:$PORT/, 12-factor PORT (default 3000). Build + run were proven locally against the real corpus — HTTP 200 with live rail content, container health green.
package.json's dev/start scripts bind 127.0.0.1 — that never applies in the container: standalone node server.js honors HOSTNAME=0.0.0.0 baked into the image. Nothing to configure.| Coolify field | Value |
|---|---|
| Source | bytesbysamu/ollscout · branch main |
| Base Directory / Dockerfile | / · Dockerfile (repo root) |
| Network | ollam (to reach oll-memory internally) |
| Port / domain | 3000 (image default) · https://scout.oll.am (DNS already wildcards → TLS auto) |
| Var | Value | Note |
|---|---|---|
| OLL_MEMORY_BASE | http://oll-memory:5008 | Internal ollam hop — the GUI never leaves the VPS to read the corpus. |
| OLL_MEMORY_SERVICE_TOKEN | <same fresh secret as §4> | Server-side only; never reaches the browser. |
| SCOUT_USER_ID | 2 | Single-user instance by design (§10) — reads mem:user:2:*. |
| OLL_WRITE_BASE | https://write.oll.am | Live write spine (default baked in — set anyway, explicit beats implicit). |
| OLL_WRITE_JWT | <fresh 72h Core JWT — §8> | Powers the draft/rewrite buttons. Expires in 72h — see §10. |
| SCOUT_USER_NAME | Sam’s signer name | Stamped on generated letters; optional. |
Recommendation: pg_dump/pg_restore. The dump carries the embeddings (768-dim vectors, same data_memory_chunk table both sides), all 58 stored cases, and — the part re-ingest cannot reproduce — the accumulated rulings history (statuses, verdict notes, tick register). Zero re-embedding, minutes of work. Run everything through the poc-pg-1 container so client/server versions can't disagree:
# 0) the Neon URL for the new DB — export without echoing it anywhere export NEON_MEM_URL='postgresql://…neon…/oll_memory?sslmode=require' # 1) dump the local corpus (custom format, portable) docker exec poc-pg-1 pg_dump -U mem -d oll_memory -Fc --no-owner --no-privileges -f /tmp/oll_memory.dump # 2) pgvector first — the restore fails without the extension docker exec -e URL="$NEON_MEM_URL" poc-pg-1 sh -c 'psql "$URL" -c "CREATE EXTENSION IF NOT EXISTS vector;"' # 3) restore into Neon docker exec -e URL="$NEON_MEM_URL" poc-pg-1 sh -c 'pg_restore -d "$URL" --no-owner --no-privileges /tmp/oll_memory.dump' # 4) the retrieval knob that does NOT travel with a single-DB dump — # re-apply it or prod retrieval silently degrades vs. the audited local setup docker exec -e URL="$NEON_MEM_URL" poc-pg-1 sh -c 'psql "$URL" -c "ALTER DATABASE oll_memory SET hnsw.ef_search = 80;"' # 5) sanity — chunk count should match the local side docker exec -e URL="$NEON_MEM_URL" poc-pg-1 sh -c 'psql "$URL" -tc "SELECT count(*) FROM data_memory_chunk;"' docker exec poc-pg-1 psql -U mem -d oll_memory -tc "SELECT count(*) FROM data_memory_chunk;"
In the minimal cut the seam and OpenCLAW don't move — OpenCLAW keeps calling host.docker.internal:5097 exactly as today. The single change: the seam's environment (launchd plist / deploy/run-http.sh env) flips its memory base to the deployed service, with the fresh token:
OLL_MEMORY_BASE=https://memory.oll.am OLL_MEMORY_SERVICE_TOKEN=<the fresh §8 secret> # poc-token dies with the poc stack
Restart the launchd job; the seam's OLL_MCP_MATCH_FLOOR needs no env at all anymore — the measured 0.64 floor is the baked default since the retrieval audit, so a restart without the var can no longer loosen the gate.
services/oll-mcp a Dockerfile (it has none — its deploy artifacts are a launchd plist + run-http.sh, a desktop-run design), deploy it as a Coolify app with OLL_MEMORY_BASE=http://oll-memory:5008 internal, and flip OpenCLAW's seam URL from host.docker.internal:5097 to the deployed seam. Then nothing scout-critical runs on the laptop.| Secret | Mint | Goes to |
|---|---|---|
| OLL_MEMORY_SERVICE_TOKEN | openssl rand -hex 32 | oll-memory app (§4) · ollscout app (§5) · local seam env (§7). The dev poc-token is burned — it was committed in the poc compose; it must never guard the prod corpus. |
| OLL_WRITE_JWT | cd services/oll-mcp && AUTH_JWT_SECRET=<Core's, from Coolify env> .venv/bin/oll-mcp mint-jwt --user 2 | ollscout app (§5). Identity-only 72h HS256 — the same setup-token pattern ollwrite uses. 72h expiry caveat in §10. |
| OLL_MCP_JWT | same mint — or nothing | Local seam. Preferred: nothing — run-http.sh already re-mints from OLL_MCP_ENV_FILE (AUTH_JWT_SECRET) on every start, so the daily launchd restart keeps it fresh automatically. |
AUTH_JWT_SECRET is read from Coolify's Core app env (or core/.env locally) — it is the one secret that is reused, not minted, because the JWT must verify against the live Core.docker exec <ollama-container> ollama list # nomic-embed-text present
curl -s https://memory.oll.am/api/health
# → {"service":"oll-memory","status":"ok"}
curl -s -X POST https://memory.oll.am/api/memory/query \
-H "X-Service-Token: $NEW_TOKEN" -H "Content-Type: application/json" \
-d '{"query":"software engineer","collection":"mem:user:2:jobs","acl":["u:2"],"top_k":3}'
# → non-empty results[] with real job chunks + vector scores
# (this one call proves the whole chain: token wall → Neon → Ollama embed → retrieval)
https://scout.oll.am/ → 200 and the rail shows the real cases — the "Cannot reach the scout memory" sheet means the token or the internal base is wrong./corpus → the ledger renders with real counts.OLL_WRITE_JWT).launchctl kickstart -k gui/$(id -u)/am.oll.mcp-seam # restart with the new env # then one ollam_memory_search / ollam_job_get through OpenCLAW or the MCP Inspector # → cited chunks from the MIGRATED corpus (same case ids as before the move)
poc-pg-1/poc-oll-memory-1 down-but-not-deleted for 48h as the rollback copy, then docker compose -f scripts/poc/docker-compose.scout-memory.yml down -v.| Delta / risk | Reality | Mitigation |
|---|---|---|
| scout.oll.am is public, the data is personal | The GUI has no login — it renders one person's CV analysis, applications and rulings to anyone who finds the URL. Fine on a laptop, not on the open internet. | Gate it at the edge before or at deploy: Coolify/Traefik basic-auth middleware on the domain (a console field, no code). A real Core-JWT login is the product-ization follow-up. |
| Single-user by design | SCOUT_USER_ID=2 is baked per instance; there is no tenancy in the GUI. This is a personal deployment, not the multi-user oll-scout service yet. | Accepted for this release; the multi-user model rides the Core-JWT login follow-up. |
| Ollama on the VPS — sizing | nomic-embed-text is small (~274 MB, 768-dim) and embeddings are cheap on CPU, but it's a new always-on container on a VPS already running ~15 containers; first pull needs ~1 GB free disk and inference wants ~1 GB RAM headroom. | Check docker stats/df -h before creating the app. Embeddings-only — never point generation at it. |
| 72h JWT expiry (OLL_WRITE_JWT) | ollscout's draft buttons hold a static 72h token — they die silently ~3 days after deploy. The seam self-heals (re-mint on restart); the GUI does not. | Accept for launch; re-mint via Coolify env edit + restart when drafts 401. Follow-up: the same re-mint-on-boot pattern, or a long-lived service token on oll-write. |
ef_search does not travel | ALTER DATABASE … SET hnsw.ef_search=80 is per-database config; a single-DB pg_restore does not carry it. Forgetting it = silently worse recall than the audited local setup. | Step §6.4 — it's in the migration block, not a footnote. |
| Re-ingest cost (if dump/restore fails) | Re-embedding ~58 cases × 2–4 chunks on VPS CPU is minutes and free — but the rulings/verdict history is unrecoverable by re-ingest. | That asymmetry is why dump/restore is the recommendation and re-ingest only the fallback. |
| oll-mcp has no Dockerfile | The seam was built as a desktop runtime (launchd + run-http.sh); it cannot be a Coolify app today. | Exactly why the minimal cut keeps it local. Containerizing it is the named follow-up (§7), not a hidden assumption. |
| Coolify PORT injection | Coolify defaults injected PORT to 3000; oll-memory's callers assume oll-memory:5008. | §4 sets PORT=5008 explicitly — the same class of bug that 502'd Core at C0. |
Related pages: oll-scout service deploy runbook (the #94 service itself, 2026-07-11) · the Memory-spine decision · the full scout review.