My home AI agent runs Hermes Agent (open source) on a small Linux server. It now has automatic, searchable memory across sessions from Honcho, the open-source memory service from Plastic Labs (AGPL-3.0, version 3.3.0). I tell the agent something in one chat, and a new chat later can recall it.
Everything runs on my own machines:
- Honcho runs on the agent server in four containers.
- Reasoning calls go to a 27B Qwen model on llama.cpp on a GPU box.
- Embedding calls go to Ollama on a Mac mini.
- Conversation text, embeddings and reasoning stay on the home network, without an API bill.
How it works: Honcho stores the conversation messages. A background worker called the deriver reads them and writes observations about each participant, which Honcho calls a peer. The agent gets that context injected into each turn and has tools to search it and to ask questions about the person.
This does not replace the agent's built-in memory, the small hand-curated MEMORY.md and USER.md files. The two run side by side, and Honcho imported the existing USER.md on its first run.
It is early. I installed and tested this on day one: a fact was extracted within a minute and recalled in a new session. How well it holds up over weeks is still open.
What you need
- Docker Compose. Podman with the standalone docker-compose binary also works; step 1 has the lines that differ. The stack is four services:
api(FastAPI, port 8000),deriver(the background worker), Postgres 15 with pgvector, and Redis. The repo also ships an MCP service, which Hermes does not need. - An OpenAI-compatible LLM endpoint with a model that does reliable tool calling. The Honcho docs state that "All Honcho agents (deriver, dialectic, dream) require tool calling," and that model choice matters more for tool-use reliability than for raw intelligence. Mine is a 27B Qwen model on llama.cpp on the GPU box. It scored 24/24 on my own tool-calling bench.
- An OpenAI-compatible embedding endpoint. Mine is Ollama on the Mac mini serving
qwen3-embedding:4b, trimmed to 1024-dimension vectors. - Hermes Agent plus the Honcho memory provider plugin from Plastic Labs.
Footprint: the stack uses about 610 MB of RAM at idle. That is api 290 MB, deriver 274 MB, Postgres 43 MB and Redis 5 MB.
The config uses Honcho's terms, so here they are:
- Workspace, peers, sessions, messages. A workspace holds peers. Both the user and the AI are peers. Sessions hold messages.
- Deriver. Background extraction of observations from messages.
- Dialectic. Ask Honcho a question about a person. It has five reasoning levels: minimal, low, medium, high and max.
- Summary. Session summaries.
- Dream. Periodic consolidation of observations.
Self-hosting is free. Plastic Labs also runs Honcho Cloud on the same open-source server, with a free tier; this post covers self-hosting.
Setup choices and why
Honcho on the agent server, API on 127.0.0.1, auth off. The agent is Honcho's one client. Since the API is not exposed on the LAN, I skip JWT and token setup. Postgres and Redis are not published to the host.
One local model per feature, set in config.toml. The defaults point at OpenAI's gpt-5.4-mini, so a feature left unset calls OpenAI. The features to override are deriver, summary, the five dialectic levels, dream deduction, dream induction and embedding.
Thinking off for fast work, on for deep work. Deriver, summary and dialectic minimal, low and medium run with thinking off, because extraction does not need it and speed matters. Dialectic high and max and both dream features run with thinking on, because they run rarely and go deeper. The switch goes through Honcho's documented provider_params.extra_body escape hatch, which passes chat_template_kwargs straight to llama.cpp.
1024-dimension embeddings, set before the first data. The dimension stays fixed for the life of the deployment, and the API does not start on a mismatch. The model can change later, as long as it can return the same dimension (step 7).
qwen3-embedding:4b for embeddings. It beat the 0.6B model on my retrieval test for about 2 GB more RAM on the Mac mini. The numbers are in step 7.
Three llama.cpp slots sharing one KV pool. The deriver makes background calls after each message. On a one-slot server those calls queue behind live chats.
One Honcho peer per person. More than one person messages the agent, and the local CLI user is a separate identity. Each person gets their own peer, and my own identities are aliased to one peer.
1. Adjust the compose file
Clone the Honcho repo and start from its compose file. Make these changes:
- Remove the MCP service.
- Bind the api to loopback:
127.0.0.1:8000:8000. - Delete the
ports:blocks from Postgres and Redis. - Mount config.toml read-only into both api and deriver.
- Keep
restart: unless-stoppedon each service. - Fully qualify the image names.
The changed keys look like this. The rest stays as upstream ships it.
services:
api:
ports: ["127.0.0.1:8000:8000"]
volumes: ["./config.toml:/app/config.toml:ro"]
restart: unless-stopped
deriver:
volumes: ["./config.toml:/app/config.toml:ro"]
restart: unless-stopped
database:
image: docker.io/pgvector/pgvector:pg15
restart: unless-stopped
redis:
image: docker.io/library/redis:8.2
restart: unless-stopped/app is the image's working directory, so Honcho picks up config.toml there. Keep the volumes the example file already lists and add this one.
Podman tip: run the standalone docker-compose binary against the user Podman socket with DOCKER_HOST=unix:///run/user/$UID/podman/podman.sock.
Podman tip: enable podman.socket and podman-restart.service as user units and turn on lingering, so unless-stopped containers come back after a reboot:
systemctl --user enable --now podman.socket podman-restart.service
loginctl enable-linger $USERPodman tip: short image names fail to resolve non-interactively. The fully qualified names above work on both runtimes.
Podman tip: on an SELinux host, mount config.toml with a lowercase :ro,z. An uppercase Z gives the file a label private to one container, so whichever of api and deriver starts second cannot read it. Honcho then starts on its OpenAI defaults without an error. I found this when the re-embed in step 7 got a 401 from OpenAI.
Hold off on starting the stack until the embedding dimension is set in step 3.
2. config.toml: one local model per feature
Each feature gets its own model block. Here is the deriver:
[deriver.model_config]
transport = "openai"
model = "qwen3.8"
[deriver.model_config.overrides]
base_url = "http://<gpu-box>:8080/v1"
api_key_env = "LLM_API_KEY"
[deriver.model_config.overrides.provider_params]
timeout = 600
[deriver.model_config.overrides.provider_params.extra_body.chat_template_kwargs]
enable_thinking = falseWhat each line does:
transport = "openai"covers OpenAI-compatible endpoints. The docs list OpenRouter, Together, Fireworks, LiteLLM, vLLM and Ollama.modelis whatever name the server answers to. My llama.cpp server runs one model, so it is that model's name.base_urlpoints at the GPU box on llama.cpp's default port.api_key_envnames the environment variable, in Honcho's own environment, that holds the llama.cpp server's key. Where my agent's keys live is covered in the keystore post.timeout = 600gives each call ten minutes, which suits a local model doing background work.enable_thinking = falseturns off thinking for this feature. It goes throughextra_bodyto llama.cpp as a chat template argument.
Repeat the same block for the remaining LLM features, changing the section names:
[summary.model_config]: thinking off.[dialectic.levels.minimal.model_config], and the same forlowandmedium: thinking off.[dialectic.levels.high.model_config]and[dialectic.levels.max.model_config]:enable_thinking = true.[dream.deduction_model_config]and[dream.induction_model_config]:enable_thinking = true.
A feature without a block falls back to OpenAI, so each one gets a block.
Tip: set a dummy LLM_OPENAI_API_KEY in Honcho's environment. The server expects a provider key at startup, and the local endpoint does not receive it.
Tip: some OpenAI-compatible servers lack json_schema support. For those, DERIVER_MODEL_CONFIG__STRUCTURED_OUTPUT_MODE=json_object exists. I did not need it with llama.cpp.

3. Embeddings, then the bootstrap order
The embedding block goes in the same config.toml:
[embedding]
VECTOR_DIMENSIONS = 1024
[embedding.model_config]
transport = "openai"
model = "qwen3-embedding:4b"
max_batch_size = 32
dimensions_mode = "always"
[embedding.model_config.overrides]
base_url = "http://<embedding-box>:11434/v1"
api_key = "ollama"The 4B model's full output is 2560 dimensions. Qwen3-Embedding models are trained so a vector can be cut short and still work, and Ollama honors the OpenAI dimensions parameter. dimensions_mode = "always" makes Honcho send dimensions=1024 on each call, so the vectors match VECTOR_DIMENSIONS. Honcho's default auto mode also sends it once VECTOR_DIMENSIONS is set; I set it explicitly so the intent is visible in the file. api_key is a placeholder; Ollama does not check it. The Mac mini already serves embeddings to other services in the house, so the only new piece there was the model (ollama pull qwen3-embedding:4b).
Honcho's database columns default to vector(1536). The order below comes from the docstring of Honcho's own embedding script:
- Start the database and Redis.
- Run the migrations with
scripts/provision_db.py. - Run
scripts/configure_embeddings.py --yes. It alters the columns from vector(1536) to vector(1024) and rebuilds the HNSW indexes. - Start the api and the deriver.
docker compose up -d database redis
docker compose run --rm --no-deps --entrypoint /app/.venv/bin/python api scripts/provision_db.py
docker compose run --rm --no-deps --entrypoint /app/.venv/bin/python api scripts/configure_embeddings.py --yes
docker compose up -d api deriverThe order matters because the script stops if embeddings already exist, and the API does not start on a dimension mismatch. I chose the dimension once, before the first message.
4. llama.cpp: three slots so memory work does not block chats
On the GPU box, add two flags to llama-server:
llama-server -m <model>.gguf --port 8080 --parallel 3 --kv-unified--parallel 3 gives the server three slots. --kv-unified makes those slots share one context pool, so a single long chat can still use the full window.
Measured on a 5090-class GPU:
- One request at a time: about 135 tok/s, the same as before.
- Three at once: about 86 tok/s each, 246 tok/s in aggregate, against 130 tok/s when the same requests queue.
- VRAM cost: about 0.7 GB more.
For the agent, this means a reply and two deriver extractions can run at the same time. The reply no longer waits behind the extractions.

5. Hermes plugin with one peer per person
- Install the plugin:
``bash hermes plugins install honcho ``
- Select it as the memory provider in the agent's config.yaml:
``yaml memory: provider: honcho ``
- Write
~/.hermes/honcho.json:
``json {"baseUrl": "http://127.0.0.1:8000", "hosts": {"hermes": {"enabled": true, "workspace": "hermes", "aiPeer": "assistant", "peerName": "me", "pinUserPeer": false, "userPeerAliases": {"<messaging-account-id>": "me", "uid:<n>": "me"}, "recallMode": "hybrid", "writeFrequency": "async"}}} ``
What each key does:
baseUrlon loopback makes the plugin skip API-key auth automatically.workspace,aiPeerandpeerNamename the workspace, the agent's own peer and my peer.pinUserPeer: falsegives each other person who messages the agent their own peer, instead of folding them into mine.userPeerAliasesmaps my messaging account and the local CLI user onto my peer.recallMode: hybridinjects context into each turn and also gives the agent five tools:honcho_profile,honcho_search,honcho_context,honcho_reasoningandhoncho_conclude.writeFrequency: asynckeeps writes to Honcho off the reply path, so replies are not slowed.
Tip: the CLI user appears as uid:<n>, where <n> is the numeric user id of the account running the CLI. Without an alias its messages land on a separate peer, and the log says "session user peer 'uid-<n>' is not the declared owner".
Restart the agent, then check the resolved config and the connection:
hermes honcho statusTrimmed to the useful lines, with my peer names replaced:
Honcho status
Host: hermes
Enabled: True
Auth: API key (not set)
Workspace: hermes
AI peer: assistant
User peer: me
Recall mode: hybrid
Dialectic cad: every 1 turn
Reasoning: base=low, cap=high, heuristic=on
Write freq: async
Connection...
OKAuth: API key (not set) is expected: a local baseUrl skips key auth.
6. Test it
- In one session, tell the agent a harmless fact. Mine was "my favorite tree is the big oak by the pond."
- Within a minute, check the database. In Honcho's schema,
documentsholds the observations:
``bash docker compose exec -T database psql -U postgres -tAc \ "select count() from messages; select count() from documents;" ``
The messages were stored, their embeddings were present, and the deriver had written one explicit observation that contained the fact.
- Open a new session and ask the agent to use
honcho_searchto look up your favorite tree. Mine answered correctly. The context injected at the start of the session carried the fact as well, so the agent had it before it searched.
Counts at the end of day one, a handful of peers in the workspace:
count
-------
18
(1 row)
count
-------
35
(1 row)That is 18 messages and 35 observations. The observations include what Honcho imported from the existing USER.md.

7. Changing the embedding model after data exists
I started on qwen3-embedding:0.6b and moved to qwen3-embedding:4b on day two. Both return 1024 dimensions on request, so the database columns stayed as they were.
The test: 25 short facts of the kind the agent remembers, and 24 questions that ask for them indirectly, such as "how is the house heated" for a fact about the heat pump. Each model embedded the facts and questions the way Honcho calls it, at 1024 dimensions, and I ranked the facts by cosine similarity.
| Model | Top-1 hits | MRR | Time per query | Memory when loaded |
|---|---|---|---|---|
| qwen3-embedding:0.6b | 17/24 | 0.825 | 40 ms | 2.4 GB |
| qwen3-embedding:4b | 19/24 | 0.860 | 150 ms | 4.4 GB |
The 4B model found two more facts on the first try, and 150 ms per query is small next to a local LLM call. Public MTEB tables put the 8B model about a point above the 4B for roughly double the memory again. On a 16 GB Mac mini that runs other services, the 4B is the better trade.
Vectors from two different models are not comparable, so each stored row needs a new embedding from the new model. Honcho 3.3.0 ships a script that fills in messages with no embedding, but not one that redoes existing ones. This short script runs inside the api container and uses Honcho's own embedding client and config:
"""Re-embed all Honcho documents/message_embeddings row with the currently configured embedding model.
Run inside the api container from /app.
"""
import asyncio
import sys
sys.path.insert(0, "/app")
from sqlalchemy import text # noqa: E402
from src.config import settings # noqa: E402
from src.db import engine # noqa: E402
from src.embedding_client import embedding_client # noqa: E402
async def main():
print("model", settings.EMBEDDING.MODEL_CONFIG.model, "dims", settings.EMBEDDING.VECTOR_DIMENSIONS)
async with engine.begin() as conn:
for table in ("documents", "message_embeddings"):
rows = (await conn.execute(text(f"SELECT id, content FROM {table}"))).all()
vecs = await embedding_client.simple_batch_embed([r.content for r in rows], on_oversize="truncate")
for r, v in zip(rows, vecs):
assert len(v) == settings.EMBEDDING.VECTOR_DIMENSIONS
await conn.execute(text(f"UPDATE {table} SET embedding = CAST(:v AS vector) WHERE id = :id"),
{"v": "[" + ",".join(map(str, v)) + "]", "id": r.id})
print(table, "re-embedded", len(rows))
asyncio.run(main())The steps:
# 1. Back up the database
docker compose exec -T database pg_dump -U postgres -Fc postgres > honcho-before-reembed.dump
# 2. Pull the new model on the embedding box, change `model` in config.toml, recreate api and deriver
docker compose up -d --force-recreate api deriver
# 3. Re-embed all rows
docker compose cp reembed.py api:/tmp/reembed.py
docker compose exec -w /app api .venv/bin/python /tmp/reembed.pymodel qwen3-embedding:4b dims 1024
documents re-embedded 35
message_embeddings re-embedded 18With 53 vectors it took a few seconds. The job grows with each message, so choosing the model early keeps it cheap.
Then I re-ran the test from step 6. A new test note came back first in search for a reworded question, and in a fresh session the agent recalled both the new note and the facts from day one. I deleted the test sessions afterwards.
Results and what's next
What is verified:
- Honcho 3.3.0 runs in four containers on the agent server and uses about 610 MB of RAM at idle.
- Reasoning calls go to llama.cpp on the GPU box and embedding calls go to Ollama on the Mac mini, instead of OpenAI.
- A fact told in one session was extracted within a minute and recalled in a fresh session.
- Each person who messages the agent has their own peer, and my identities share one.
- Three llama.cpp slots let deriver work run beside live chats, at about 0.7 GB of extra VRAM.
- Moving from the 0.6B to the 4B embedding model on day two meant re-embedding 53 vectors in a few seconds, and recall still worked afterwards.
This is two days of use. Still open: how well the observations hold up over weeks, how dream consolidation behaves, and whether dialectic at the high and max levels is worth its cost on a local model.
Next:
- Let it run for a few weeks, then compare what Honcho has learned against the hand-curated USER.md.
- Use the dialectic tools on a real question about a person.
- Watch the deriver's load on the GPU box.
Sources
- Honcho self-hosting guide: https://honcho.dev/docs/v3/contributing/self-hosting
- Honcho configuration reference: https://honcho.dev/docs/v3/contributing/configuration
- Honcho Hermes Agent integration: https://honcho.dev/docs/v3/guides/integrations/hermes
- Honcho repository: https://github.com/plastic-labs/honcho
Comments
// Comments are reviewed before appearing. No spam. No noise.