tuancookiez-hub/HyAtlas-Memory
A plugin for Hermes to connect Hy-Memory as a memory provider as well as a dashboard view
HyAtlas Memory is a Go-based memory server with an eight-layer memory model, in-process BGE embeddings, and an embedded vector store. It can extract facts, intentions, and entity pages through an OpenAI-compatible endpoint, then search and consolidate memories; retired memories can be restored.
- Supports Lite, Pro, and Ultra extraction modes
- Uses an embedded, disk-persisted vector store
- Provides reversible memory retirement with reasons
Basic static screen: be1ca8f (November 9, 2026), not security certification. Pro/ultra modes send memory content to the configured model; lite mode avoids LLM calls. Requires a separate server and downloaded runtime/model assets. Keep the service local.
full readme from github

HyAtlas Memory: memory you can take back
One binary. Eight layers. Three extraction modes. One Go binary (the embedded release build is about 160 MB because it carries the BGE model), no Python at runtime, in-process BGE embeddings. Release binaries: Linux amd64 · macOS arm64 · Windows amd64. Other platforms build from source.
HyAtlas is a complete rewrite of the HyAtlas memory system in pure Go (since v4.0). It replaces the Python floor (venv, zvec, Kuzu, FastAPI, HTTP embed subprocess) with a single binary: an embedded Chromem vector store, in-process BGE-small embeddings via onnxruntime-go, and LLM fact extraction, where HYATLAS_MODE sets how much the server reasons and HYATLAS_SYNC_EXTRACT sets whether a write waits for extraction. The 8-layer memory model (Profile · Raw · Fact · Summary · Knowledge · Schema · Intention · Entity) is implemented in full.
Current release: v5.0.0, the stable floor; what it changes is in CHANGELOG.md.
What it does, in short:
- Writing. Each conversation turn goes through one LLM call that extracts dated facts, updates or replaces older ones, open intentions with due dates, entity pages and reusable task experiences.
- Remembering in the background. Summaries are written per episode of work. A background pass merges and retires stale facts (with a reason, reversible), and builds a knowledge graph and patterns that record the facts behind them.
- Recall. Search understands dates ("last week") and the entities you name, finds long facts by any of their sentences, and shows what a fact replaced.
- Reversible by design. Replaced, merged, dropped and forgotten memories are retired with a reason, not erased, and can be restored; a note you replace or remove with Hermes's memory tool is forgotten here too. Rows are deleted only when you ask: a delete by id, a bulk delete with
HYATLAS_ADMIN=on, or opt-in raw retention. - Credentials. HyAtlas uses only the extraction key you give it (
llm_key, orHYATLAS_LLM_KEY); it never reads another application's credentials or takes Hermes's sign-in token. The last Python release was v3.5.0 (Zvec/Kuzu); it is no longer maintained. CHANGELOG.md has the full history.



Architecture
flowchart TD
H["Hermes turn (plugin)"] --> A["POST /api/v1/add"]
A --> RAW["Embed + store L2 raw"]
RAW --> CTX["Gather context: 5 related facts (>= 0.70),<br/>open intentions, entity pages"]
CTX --> X["Extract (LLM, one call): facts with dates,<br/>replaces / updates, intention with due date,<br/>intentions done, entity pages, task experiences"]
X -.-> RC["Reconcile (LLM, opt-in, local models):<br/>update / replace / link similar stored facts"]
X --> P["Promote: L3 facts (0.92 restatement fold),<br/>L1 mirror, L7 intention, L8 pages"]
P --> NOW["Now page (LLM),<br/>at most every 15 min"]
P --> EP["Episode summary (LLM),<br/>after a 30-min gap"]
P --> ST[("chromem vectors L1-L8,<br/>long-fact clauses, doc index")]
EP --> ST
KEY["Plugin hands over its own key<br/>(loopback only, memory only)"] -.-> X
CU["Catch-up every 10 min:<br/>turns not yet extracted"] -.-> X
FG["Hermes memory replace / remove<br/>-> POST /api/v1/forget"] --> RET["Retire matching rows<br/>(reversible, with a reason)"]
RET --> ST
T["Ticker: 6h, catch-up while due"] --> CL["Topic clusters (0.55),<br/>only changed clusters"]
CL --> C["Consolidate (LLM, per cluster): merges,<br/>drops + reason, schemas, L5 edges, arc"]
C --> G["Guards: merge >= 0.65, schema dup >= 0.92,<br/>schemas keep their evidence, restore"]
G --> ST
C --> KG["L5 graph with evidence"]
C -.-> XS["Cross-topic pass (LLM, daily):<br/>patterns that span topics"]
XS --> G
Q["POST /api/v1/search"] --> F["Vector (+ long-fact clauses) and stemmed keyword,<br/>fused by rank, weighted by the kind of query"]
F --> F2["Dates in the query, entity boost,<br/>graph-assisted recall, idle-time recency,<br/>usage feedback, near-duplicate demotion, MMR diversity"]
F2 --> OUT["Now -> pages -> profile (+ schemas behind the facts)<br/>-> proactive -> experiences -> normal,<br/>with event dates and replaced versions"]
Lite mode keeps only the raw write and search: no LLM call at all. Pro adds extraction, episodes and the Now page. Ultra adds consolidation, which is what fills L5 and L6. Every LLM step goes to the endpoint you configure; nothing else leaves the machine.
| Layer | What it holds |
|---|---|
| L1 Profile | User preferences, style, constraints (mirrors L3 facts labelled user_preferences) |
| L2 Raw | Every incoming memory as-is |
| L3 Fact | LLM-extracted factual atoms |
| L4 Summary | One summary per episode of work (writes with no gap over HYATLAS_EPISODE_GAP), plus one rolling arc per owner from consolidation |
| L5 Knowledge | Entity/relation graph (JSON-persisted). Built by the ultra slow path from relations corroborated across at least two turns, each with evidence citations |
| L6 Schema | Recurring patterns generalised by the ultra slow path |
| L7 Intention | LLM-extracted current goal / next step |
| L8 Entity | One page per thing that matters (a project, tool, person, place): its current state plus a dated history. Written by each extraction; a changed value moves the old one into history |
Stack
- Vector store: Chromem-go v0.7.0 — embedded, disk-persisted, no server process
- Embeddings:
bge/bge.go— BGE-small-en-v1.5 (384-dim) via onnxruntime-go (cgo). WordPiece tokenizer, mean-pool, L2-normalize. No Python. - LLM extraction: Via any OpenAI-compatible endpoint. One structured call per write (pro, ultra) fills L3 Fact, L7 Intention, L8 entity pages and, for user preferences, L1 Profile from the L2 Raw entry. The call is given today's date and shown the owner's few nearest existing facts and open intentions, so it can date a fact (
event_at) or an intention (due_at), mark a fact whose value changed as replaced, fold new detail or a repeat of the same event into an existing fact as updated (with a count), and mark an intention as moved on or done; those rows are superseded, not deleted, and record how (superseded_how). An intention is written only for a goal beyond the one exchange, and not again for a turn that just continues an open one. The plugin shows each recalled memory with its date. Blocking or background followsHYATLAS_SYNC_EXTRACT. L5 and L6 come from the separate ultra consolidation pass. - Graph:
graph/graph.go— exact-match traversal over JSON-persisted nodes and relations, scoped per owner - HTTP: Standard Go
net/http. No framework.
Differences from v3.5 (historical)
This table lists differences that the code in this repository shows. It does not include benchmark numbers.
| v3.5 (Python) | v4 and later (Go) | |
|---|---|---|
| Runtime | venv, zvec, Kuzu, FastAPI, HTTP embed subprocess | One binary, no Python |
| Release binary | — | About 160 MB, embedded (model included). A plain build is small and needs a model folder |
| Listening port | 19527 |
19528 by default |
| L4 Summary | Dormant | Active |
| Restart | — | Reloads the doc index and graph from the data directory |
Repository layout
cmd/hyatlas-go/ the server (package main): HTTP API, extraction, consolidation, search;
dashboard/dist is the embedded web dashboard
bge/ in-process BGE-small embedder (onnxruntime-go)
graph/ the L5 knowledge graph store
memory/ layer and mode definitions
plugins/hyatlas/ the Hermes plugin (memory provider, CLI, Desktop pane, dashboard tab)
scripts/ the one-line installer and the ONNX export script
docs/ images and infographic sources
Quick start (Linux / macOS / Windows)
The one-liner (recommended)
curl -fsSL https://raw.githubusercontent.com/tuancookiez-hub/HyAtlas-Memory/main/scripts/install.sh | bash
The script looks for a prebuilt release asset for your platform
(hyatlas-go-<tag>-<os>-<arch>). Release assets exist for Linux amd64, macOS
arm64 and Windows amd64. On any other platform it builds from source, which needs
Go 1.26+ and a C compiler. The installer refuses an older go instead of switching
toolchains. It installs the binary to a directory on your PATH, and verifies the
install by starting the server and probing /healthz.
Release binaries are embedded builds and already carry the BGE-small model and onnxruntime, so the script downloads nothing else for them.
Source builds fetch the model (~133 MB) and the onnxruntime 1.28.1 library that matches your CPU (Linux x64 or aarch64, macOS arm64, Windows x64 or arm64). Intel macOS has no onnxruntime package for this version, so the installer stops there with a clear error.
If the model fetch fails (for example HuggingFace is blocked), the binary is still installed, the manual steps are printed, and the script exits with status 1. A missing Go or C compiler, or a failed source build, stops the script before anything is installed.
Source builds clone the tag named by
HYATLAS_VERSION. The default branch is built instead only when that tag is missing andHYATLAS_VERSIONis not set, and the installer warns about it. If you setHYATLAS_VERSIONand the tag is missing, the install fails.Go 1.26 or newer is required for source builds. A plain
go buildwith an older Go and the defaultGOTOOLCHAIN=autofetches the toolchain thatgo.modnames (1.26.5). The installer does not do that: it stops on an oldergo.Verification starts the server on a free loopback port and waits about 15 seconds for
/healthz. The last line says which of four outcomes happened:Final line Exit Meaning installed and verified0 The server answered /healthzwith the BGE embedder (an embedded release binary, or a source build with its model).binary verified, model missing1 The server answered /healthzonly with the stub embedder (HYATLAS_EMBED_BASE=local). That shows the binary runs, not that embeddings work. The exit code is the model step's.NOT verified1 The server did not answer /healthz. Starthyatlas-goby hand to see its error.skipped, not verified0 A source build with HYATLAS_NO_MODEL=1. Without the model the server cannot start, so nothing was probed.Setup questions. The installer asks for the mode, the LLM endpoint and the key on the terminal. Under
curl | bashit asks on/dev/tty. With no terminal at all it asks nothing. AnyHYATLAS_MODE,HYATLAS_LLM_BASE,HYATLAS_LLM_MODELorHYATLAS_LLM_KEYalready exported are saved to$HERMES_HOME/.env(default~/.hermes/.env), which is created with mode 0600.
Useful env vars (installer only):
| Variable | Purpose | Default |
|---|---|---|
HYATLAS_VERSION |
Release tag to install. A source build clones this tag (see above) | v5.0.0 |
HYATLAS_INSTALL_DIR |
Where the binary goes | ~/.local/bin (Windows: %LOCALAPPDATA%\hyatlas) |
HYATLAS_MODEL_DIR |
Where the installer caches the model (source builds) | ~/.hyatlas/models (Windows: %LOCALAPPDATA%\hyatlas\models) |
HYATLAS_MODE |
Extraction mode written to the Hermes .env: lite | pro | ultra. Any other exported value stops the installer with an error and nothing is written |
ultra (asked interactively) |
HYATLAS_LLM_BASE, HYATLAS_LLM_MODEL, HYATLAS_LLM_KEY |
LLM endpoint, model and key (asked interactively if unset) | (none) |
HYATLAS_NO_MODEL=1 |
Skip the model download on source builds | (downloads) |
The server's own variables (HYATLAS_SYNC_EXTRACT, HYATLAS_CONSOLIDATE_*,
HYATLAS_RAW_RETENTION, and the rest) are listed in the configuration table below.
Manual install (3 steps)
Step 1 — Prerequisites (click to expand)
| OS | What's needed | Install command |
|---|---|---|
| Linux | Go 1.26+, gcc (for cgo) | apt install golang-go gcc (or distro equivalent) |
| macOS | Go 1.26+, Xcode CLI tools | xcode-select --install + install Go 1.26+ from go.dev |
| Windows | Go 1.26+, MinGW-W64 (for cgo) | winget install BrechtSanders.WinLibs.POSIX.UCRT then restart terminal |
cgo is required — onnxruntime-go links against the platform's C runtime via cgo. You need a C compiler (gcc, clang, or MinGW-W64).
Step 2 — Build
git clone https://github.com/tuancookiez-hub/HyAtlas-Memory.git
cd HyAtlas-Memory
go build -o hyatlas-go ./cmd/hyatlas-go # plain build (small; reads the model from a models/ folder at runtime)
go build -tags embedded -o hyatlas-go ./cmd/hyatlas-go # embedded build (about 160 MB, model and onnxruntime bundled in)
For the embedded build to compile,
cmd/hyatlas-go/models/must contain four files, becausego:embedlists them by name:bge-small-en-v1.5.onnx,bge-small-en-v1.5.onnx.data(an empty file is fine),vocab.txt, and the onnxruntime library for your platform:libonnxruntime.soon Linux,libonnxruntime.dylibon macOS,onnxruntime.dllon Windows. See Model assets for where to get them.
Step 3 — Run
# Local BGE embeddings (the "no Python" path) are the default. Set HYATLAS_MODEL_DIR
# only for a plain build whose model is not in one of the search-order folders in
# "Model assets" below. An embedded build ignores it.
export HYATLAS_EMBED_BASE=bge
export HYATLAS_MODEL_DIR=/path/to/models
# Required for LLM extraction in pro/ultra. No endpoint is assumed: without all
# three the server stores the raw trace only and reports llm=unconfigured.
# Any OpenAI-compatible API works, including a local one.
export HYATLAS_LLM_BASE="https://inference-api.nousresearch.com/v1" # example
export HYATLAS_LLM_MODEL="poolside/laguna-s-2.1:free" # example
export HYATLAS_LLM_KEY="your-nous-agent-key"
./hyatlas-go
The server listens on 127.0.0.1:19528 (loopback only by default). It has no
authentication, so keep it on loopback. Use HYATLAS_GO_HOST to change the bind
address only if you understand the exposure.
Who may talk to it. The server refuses requests that a web page could make on your behalf, on every bind address:
- A
Hostheader that names a DNS name is refused with 403, including on/healthz. Onlylocalhostand IP literals pass. This blocks DNS rebinding. To accept a hostname, such as a LAN name or a reverse proxy, list it inHYATLAS_ALLOWED_HOSTS. Entries are hostnames (no scheme, and any port in an entry is ignored). Then the server accepts that name. - An
Originheader that is not loopback, not the request's own host, and not in the allowlist is refused with 403. Anullor non-http(s) origin is refused too. - A request with
Sec-Fetch-Site: cross-siteis refused with 403. - Requests with no
Originheader (curl, the Hermes plugin) are not affected by theOriginrule, and the server's own/dashboard/pages are not affected.
Each write returns an extraction_status: pending (extraction runs in the
background), done or failed (the write waited for extraction), unconfigured
(no LLM endpoint, model or key), or skipped (lite mode).
Privacy — what leaves your machine
The server binds loopback only, but loopback is not the whole story, and the default configuration is not fully local:
| What | Goes where | Default | How to keep it local |
|---|---|---|---|
| Memory text (each write's text, sent for extraction) | The extraction LLM. Each write is one request, and a failed call or unparseable reply is retried once. From the Hermes plugin, a write is one turn: the user's message and the assistant's text only. Tool calls, tool output, system messages and Hermes compaction summaries are not sent. Each message is capped at 4,000 characters and the turn at 12,000. One extraction call sends at most 16,000 bytes of the write's text (the stored raw row keeps the full text) | Nowhere — no endpoint is shipped, so an unconfigured server makes no LLM call and reports unconfigured |
Already opt-in: set HYATLAS_LLM_BASE/_MODEL/_KEY to choose where it goes. Point them at a local OpenAI-compatible server to keep it on-machine, or use HYATLAS_MODE=lite |
| Stored facts and schema patterns (ultra only) | The same LLM, by the consolidation pass. One request per owner (user_id/agent_id pair) that is due: one of its topic clusters has changed members since the cluster's last successful call. An owner with one fact, or with nothing due, makes no request. Each request covers one cluster and carries up to HYATLAS_CONSOLIDATE_BATCH (50) facts of that owner, each cut to 400 characters with its fact and turn IDs, plus up to 20 existing schema patterns cut to 200 characters. Facts that a pass merges or drops are superseded: kept and hidden from search, not deleted. Each drop is stored with its reason. Once a day at most per owner (HYATLAS_CROSS_SCHEMA_EVERY, off to disable), one more request sends that owner's schema texts (up to 40, each cut to 300 characters) to look for patterns that span topics |
Nowhere until an endpoint is configured | Use pro or lite |
| Embeddings (each memory's text, and each search query) | In-process BGE-small (onnxruntime-go) | Local — HYATLAS_EMBED_BASE=bge, no network |
Already local. If you set HYATLAS_EMBED_BASE to a URL, the text goes there instead, in every mode, including lite |
| Stored memories, vector index, graph | HYATLAS_GO_DATA (default ./data) |
Local | Already local |
| Telemetry / usage reporting | — | None. The only outbound HTTP the server makes is to the LLM and embedding endpoints you configure | — |
So out of the box: embeddings and storage are local, and extraction stays off
until you configure an endpoint. Once HYATLAS_LLM_BASE, HYATLAS_LLM_MODEL
and HYATLAS_LLM_KEY are all set, every write in pro or ultra is sent to that
endpoint to derive facts, summaries and intentions. That is the point of the
feature, and it means choosing the endpoint is choosing where the text goes. The
endpoint is yours to choose: any OpenAI-compatible API, including one on your own
machine. To keep conversation text on the machine, set HYATLAS_MODE=lite: no LLM
call is made at all, so only the raw trace and local embeddings are stored.
(lite keeps text local only while HYATLAS_EMBED_BASE is bge or local; an
HTTP embedder receives the text too.)
The three modes form a ladder of reasoning scope, not of latency:
| Mode | LLM calls | Reasoning scope | Layers | Consolidation |
|---|---|---|---|---|
lite |
none | — | 1 / 8 — L2 Raw only | no |
pro |
one per write, plus one per episode summary | within one turn | 6 / 8 — L1, L2, L3, L4, L7, L8 | no |
ultra (default) |
one per write + periodic batch | across memories and time | 8 / 8 at steady state (7 / 8 with HYATLAS_CONSOLIDATE_GRAPH=off) |
yes |
The two systems own disjoint layers:
- System1 (per turn) — L1 Profile, L2 Raw, L3 Fact, L7 Intention, L8 Entity pages.
What one turn can actually evidence. L4 Summary comes from episodes: the
writes of one owner with no gap longer than
HYATLAS_EPISODE_GAP(30 minutes) form one stretch of work, which gets one summary when it ends. That costs one LLM call per episode, not per write. - System2 (slow path) — L5 Knowledge, L6 Schema. A relation worth keeping is
corroborated by more than one turn, and a schema is a recurring pattern, so
neither can come from a single turn. Ultra is the only mode that runs System2,
which is why it is the only one that fills L5 and L6.
HYATLAS_CONSOLIDATE_GRAPH=offstops L5 (and the arc).
Whether a write blocks on its extraction is a separate knob
(HYATLAS_SYNC_EXTRACT=on|off), not part of the mode. Pro blocks by default and
ultra does not, but either can be overridden — capability follows the mode,
latency follows the knob.
HYATLAS_SYNC_EXTRACT |
Effect |
|---|---|
| (unset) | follow the mode: pro blocks, ultra returns immediately |
on |
the write waits for extraction and reports done / failed |
off |
the write returns pending; extraction runs behind it |
Ultra-only tuning:
| Variable | Default | Meaning |
|---|---|---|
HYATLAS_CONSOLIDATE_EVERY |
6h |
how often the slow path runs |
HYATLAS_CONSOLIDATE_BATCH |
50 |
max facts per owner per consolidation call |
HYATLAS_CONSOLIDATE_GRAPH |
on | off (also false, 0, no) stops consolidation writing L5 knowledge edges and the cross-session arc; the prompt then does not ask for them. Each edge is also stored as a searchable L5 memory ("Foxtrot uses Postgres 16") |
HYATLAS_CONSOLIDATE_PARALLEL |
2 |
calls run at once (1 to 4) |
HYATLAS_CONSOLIDATE_CATCHUP |
20m |
interval between passes while clusters are still due |
HYATLAS_CONSOLIDATE_RECLUSTER |
(unset = never) | rebuild an owner's clusters after this long |
HYATLAS_RAW_RETENTION |
(unset = never delete) | age after which uncited L2 Raw is deleted |
Durations are Go durations (30m, 6h, 2160h) or a plain number of seconds.
Go has no d unit, so write 90 days as 2160h. A value that does not parse is
logged and treated as unset, so raw decay stays off.
HYATLAS_CONSOLIDATE_BATCH caps the facts one owner sends per call. An owner with
more facts than the cap is split into several clusters, each its own call.
HYATLAS_RAW_RETENTION is opt-in because it deletes. An L2 raw row is kept if a
live graph edge cites it (as its primary source or any of its sources), or if a
live fact (L1 or L3) was extracted from it. Only raw rows that are old and cited by
nothing are removed. The decay pass runs once per consolidation pass, so raw rows
can age out only when a pass runs.
Facts that the ultra pass merges or drops are superseded, not deleted: they keep
invalid_at and superseded_by, and GET /api/v1/list returns them only with
include_superseded=true.
The 8 / 8 above is steady state, not the first minute. A fresh ultra install
sits at 6/8 until the slow path first runs, which is up to 6h away by default. L5
is filled only when a pass finds relations corroborated by at least two turns, and L6 only when a pass finds recurring patterns. To run a pass now, instead of waiting for the tick:
curl -X POST http://127.0.0.1:19528/api/v1/digest
The response wraps the pass in report: digest_ok: true, then report with
facts_in, owners_run, owners_unchanged, skipped_owners, clusters_run,
clusters_pending, merged, edges, dropped, schemas, arc, pruned_raw,
protected_raw, duration_ms and errors. edges counts distinct L5 edges created. Re-citing an existing edge adds
evidence and is not counted.
/api/v1/status carries consolidations (the number of completed passes) and
last_consolidated (the last report), so "the slow path ran" is checkable rather
than something you have to take on faith. Both reset when the server restarts, so
they count passes since the current process started. consolidations is -1 when
the mode has no slow path (lite and pro). A pass that finds nothing to reconcile
still reports as run with zero changes, which is what distinguishes it from a pass
that never happened. An owner whose facts have not changed since its last successful
pass counts under owners_unchanged and is not sent to the LLM again.
At the end of a pass, an owner with at least 4 live schemas from at least 2 topics also gets
a cross-topic pass, at most once per HYATLAS_CROSS_SCHEMA_EVERY (24h; the last run is kept
in consolidate_state.json, so a restart does not repeat it). One extra LLM call, at most once a
day per owner, sends that owner's schema texts (newest 40) and asks for up to 3 patterns that hold
across two or more of them. Each is written as a new L6 schema marked cross_topic, with the
facts behind the schemas it generalises as evidence, unless the owner already has a schema that
says the same (the 0.92 duplicate guard). Existing schemas are never retired or edited by it.
The report counts them under cross_schemas.
Facts are consolidated in topic clusters of up to HYATLAS_CONSOLIDATE_BATCH
facts, built once per owner and kept in consolidate_state.json. An owner here is a
person: user IDs grouped by HYATLAS_USER_ALIASES are consolidated together, so a
contradiction between facts written under two of their IDs meets in one call. A new fact joins the
cluster of its nearest neighbour (similarity 0.55 and up) when that cluster has room,
otherwise the newest time-ordered cluster. A cluster is due only when its members
changed since its last successful call, so after the first full coverage a pass
handles just the clusters that received new facts. A pass runs due clusters across
owners, up to HYATLAS_CONSOLIDATE_PARALLEL (2) calls at once, and starts no call
when less time is left in its 10-minute budget than a call takes: 1.25 times the
median of recent successful calls, at least 3 minutes. While clusters are still due
(clusters_pending > 0) the next pass runs after HYATLAS_CONSOLIDATE_CATCHUP (20m)
instead of HYATLAS_CONSOLIDATE_EVERY, and a server started with work pending begins
with a catch-up pass. How long the first coverage takes depends on the model: on a
free model with 25-fact clusters a pass ran 7 clusters in 9 minutes, about half a
day for ~150 clusters. Progress is saved after every cluster. A cluster that fails
three passes in a row is skipped until its members change or 24 hours pass, and the
skip is listed in errors; when two or more calls of a pass fail and none succeeds,
the provider is taken to be down and no cluster is charged. A failed LLM call, merge or supersede makes the cluster
retry on the next pass. A failed arc, schema, L5 edge or L1 mirror
write is listed in errors but does not cause a retry, because the facts were
already reconciled.
Otherwise the endpoint is yours to choose per the tier you are on — set
HYATLAS_LLM_BASE, HYATLAS_LLM_MODEL and HYATLAS_LLM_KEY to any
OpenAI-compatible API, or point HYATLAS_LLM_BASE at a server you host
(ollama, vLLM, llama.cpp, LM Studio) and leave HYATLAS_EMBED_BASE=bge.
Nothing is transmitted until all three are set. There is no default endpoint,
so a fresh install makes no LLM call at all: writes store the raw trace plus
local embeddings, status reports llm: "unconfigured", and each write reports
extraction_status: "unconfigured". Once you configure an endpoint, extraction
runs on every write in pro and ultra and your turn text goes there — so
choosing the endpoint is choosing where memory text leaves the machine. Set
HYATLAS_MODE=lite to opt out entirely regardless of configuration.
When the Hermes plugin spawns this server it passes an explicitly allowlisted
environment — OS essentials plus HYATLAS_* only — rather than a copy of the
agent's environment, so provider API keys the agent holds do not reach the
server process. The plugin forwards only the non-secret LLM endpoint and model
settings, and only when you have set them in the plugin config. It forwards no
LLM key at spawn time. A key reaches the server only if you
provide it: your own HYATLAS_LLM_KEY, a plain-text HYATLAS_LLM_KEY_FILE, the
llm_key secret saved by hermes memory setup, or a launcher or tool you run that
posts it to POST /api/v1/llm/key from the same machine (kept in memory only). A
Nous Portal API key, or the key of any OpenAI-compatible provider, works. Until a key
arrives,
/api/v1/status reports llm: "awaiting_key" and writes are stored without
extraction; once a key arrives the catch-up loop extracts them (see
HYATLAS_CATCHUP_EVERY), and /api/v1/status shows how many are still waiting
as unextracted.
Neither the server nor its launcher reads another program's credential file.
Earlier versions of the sample launcher read Hermes' auth.json; a program that
opens another application's credentials and then calls the internet is what
credential stealers do, and behaviour-based antivirus (Bitdefender, for one) can
quarantine it together with every file it wrote. The extraction key is your own. HYATLAS_LLM_KEY_FILE takes a plain-text token file only, so it cannot be pointed at a JSON credential file.
Optional: a local memory model (Hy-Memory-30B-A3B). If you want extraction and consolidation to stay on your machine, Tencent's Hy-Memory-30B-A3B (Apache-2.0) is a good fit. It is trained for exactly this job, writing long-term memory: deciding what to keep and how new facts update old ones. Tencent's model card scores it level with large general models as a memory writer. It is a mixture-of-experts model with about 30B parameters in total and about 3B active per token, so it runs faster than its size suggests. It is heavy, though:
| Setup | Build | Needs |
|---|---|---|
| Minimum | Q4_0 / Q4_K_M GGUF (17–18 GB) | 24 GB of RAM and a modern 6-core CPU with AVX2. No GPU needed: with about 3B parameters active per token it runs on the CPU, roughly 8–15 tokens/s on DDR4 |
| Comfortable | Q4_K_M GGUF | 32 GB of RAM plus a 6–8 GB GPU, with the experts kept in system RAM (about 1.5–2× faster) |
| Full speed | Q4_K_M GGUF | 24 GB+ of VRAM, with the whole model on the GPU |
| Higher precision | Q8_0 GGUF (about 32 GB) | 48 GB of VRAM, or 64 GB of RAM |
| Full weights | BF16 (vLLM, SGLang), about 60 GB | Data-centre GPUs |
The model stays loaded the whole time, so on a 24–32 GB machine it takes most of the memory. Builds below 4 bits (Q2, Q1, TQ1, 4–9 GB) load on 12–16 GB of RAM, but they lose too much accuracy for memory writing: wrong facts and broken JSON.
Serve it from any OpenAI-compatible server and point HyAtlas at it. For example, with llama.cpp and a community GGUF:
llama-server -m Hy-Memory-30B-A3B-Q4_K_M.gguf --port 8080 -c 32768 --n-cpu-moe 48 --jinja --chat-template-kwargs '{"reasoning_effort":"no_think"}'
export HYATLAS_LLM_BASE="http://127.0.0.1:8080/v1"
export HYATLAS_LLM_MODEL="hy-memory"
export HYATLAS_LLM_KEY="local" # any non-empty value; a local server ignores it
Notes:
--n-cpu-moekeeps that many layers' experts in system RAM. Raise it until the model fits your VRAM, and lower it for speed.- The model card runs it with
reasoning_effort: "no_think". Set that on the server as shown, because HyAtlas does not send it per request. - To try it without giving up a hosted model, set it as the fallback instead
(
HYATLAS_LLM_FALLBACK_BASE,HYATLAS_LLM_FALLBACK_MODEL). It then answers only when the primary fails. - Consolidation prompts are several thousand tokens. On a mid-range desktop with the experts in RAM, expect minutes per consolidation call. That is fine for a background pass, but slower than a hosted model.
- It was trained on the Mem0, MemOS and Hy-Memory prompt formats. HyAtlas uses its own, so check a few consolidation passes before relying on it.
All configuration is via environment variables — the binary reads no settings from its command line:
| Variable | Default | Purpose |
|---|---|---|
HYATLAS_GO_PORT |
19528 |
HTTP listen port |
HYATLAS_GO_HOST |
127.0.0.1 |
Bind address (loopback only by default). IPv6 literals work with or without brackets |
HYATLAS_ALLOWED_HOSTS |
(empty) | Comma-separated hostnames the server accepts in Host and Origin, in addition to localhost and IP literals. A DNS name not listed here gets 403 on every route, including /healthz. Entries are hostnames; a port in an entry is ignored. Add a name only if you reach the server through it, such as a reverse proxy or a LAN hostname. |
HYATLAS_GO_DATA |
./data |
Data directory (relative paths resolve against the working directory): chromem collections, doc_index.json, consolidate_state.json, and by default graph.json |
HYATLAS_GRAPH_PATH |
<data>/graph.json |
L5 graph store location |
HYATLAS_EMBED_BASE |
bge |
bge = local in-process BGE embedder (no network). Set to a URL for an OpenAI-compatible embedder, or local for a deterministic stub that is not semantic. |
HYATLAS_EMBED_MODEL |
text-embedding-3-small |
Model name sent to an OpenAI-compatible embedder. Used only when HYATLAS_EMBED_BASE is a URL. |
HYATLAS_EMBED_KEY |
(empty) | Bearer token for that embedder. Used only when HYATLAS_EMBED_BASE is a URL. |
HYATLAS_MODEL_DIR |
(unset: search order below) | Where the BGE model lives, for a plain build. When set, it is the only directory searched. When unset, the server looks in <cwd>/models, then <exe dir>/models, then ~/.hyatlas/models (Windows %LOCALAPPDATA%\hyatlas\models). An embedded build ignores this variable. |
HYATLAS_MODE |
ultra |
lite | pro | ultra. An unrecognised value is fatal at startup. See the mode table above. |
HYATLAS_SYNC_EXTRACT |
(follows the mode) | on | off. Whether a write waits for extraction. An unrecognised value is fatal at startup. |
HYATLAS_CONSOLIDATE_EVERY |
6h |
Ultra only: interval between slow-path passes. Must be positive in ultra: 0, off, false, no or a negative value is fatal at startup; use pro to run without the pass. |
HYATLAS_CONSOLIDATE_BATCH |
50 |
Ultra only: max facts per owner per consolidation call. Larger owners are split into several clusters, one call each. |
HYATLAS_CONSOLIDATE_GRAPH |
on | Ultra only: off (also false, 0, no) stops consolidation writing L5 knowledge edges and the cross-session arc, and the prompt then does not ask for them. Each edge is stored in the graph (drawn by the dashboard and the Desktop starmap) and as a searchable L5 memory; edges from older versions are indexed for search at startup. |
HYATLAS_CONSOLIDATE_PARALLEL |
2 |
Ultra only: how many consolidation calls run at once (1 to 4) |
HYATLAS_CONSOLIDATE_CATCHUP |
20m |
Ultra only: the interval between passes while clusters are still due |
HYATLAS_CONSOLIDATE_RECLUSTER |
(unset = never) | Ultra only: rebuild an owner's topic clusters after this long; clusters whose members are unchanged keep their progress |
HYATLAS_CROSS_SCHEMA_EVERY |
24h |
Ultra only: the least time between two cross-topic schema passes for one owner; 0 or off disables it. The pass makes one extra LLM call, sending that owner's schema texts, only for an owner with at least 4 schemas from at least 2 topics |
HYATLAS_RAW_RETENTION |
(unset = never delete) | Ultra only: age after which an L2 raw row cited by no live edge or fact is deleted |
HYATLAS_USER_ALIASES |
(unset = no aliases) | Groups of user IDs that are one person, for search: groups separated by ;, IDs in a group by ,, e.g. "221727702992945152,default,hermes-memory-archive". A search for any ID in a group covers the whole group, and so do list, write-time de-duplication, the related facts shown to extraction, consolidation, forget, episodes and the Now page. Episodes and Now pages are kept under the first ID of the group |
HYATLAS_MIN_SCORE |
0.60 |
Search drops vector hits below this cosine similarity. 0 disables the floor. A request may override it with min_score |
HYATLAS_DEDUPE_SCORE |
0.92 |
Write-time de-duplication: a new L3 fact at or above this similarity to the owner's nearest live fact supersedes it. 0 disables it |
HYATLAS_ADMIN |
off | on enables the two /api/v1/admin/* maintenance endpoints (see Maintenance (admin) in the API Reference), and bulk delete (/api/v1/delete_all by layer, user, agent or all). Otherwise they return 403. Deleting one memory by id needs no switch |
HYATLAS_CATCHUP_MAX_AGE |
168h (7 days) |
Pro and ultra: how old a raw memory may be and still be caught up. Older rows, and rows written in lite mode, are never re-extracted automatically (/api/v1/reprocess still can). 0 or off means no age limit |
HYATLAS_CATCHUP_EVERY |
10m |
Pro and ultra: how often the server re-extracts raw memories that extraction never reached (written while no key was set, or whose extraction call failed). Up to 20 per run, oldest first, one at a time; a row under 2 minutes old is left alone, and the run stops at the first model error. A reply that cannot be parsed counts a try on that row and the run goes on; after 3 tries the row is left alone. Only rows written in pro or ultra, and younger than HYATLAS_CATCHUP_MAX_AGE, are taken. A key arriving also triggers a run. 0, off, false or no turns it off |
HYATLAS_LLM_BASE |
(unset) | OpenAI-compatible LLM endpoint. Memory text is sent here once you set it — see Privacy above. Unset means no LLM call at all. |
HYATLAS_LLM_MODEL |
(unset) | LLM model name. Base, model and key must all be set for extraction to run. |
HYATLAS_LLM_KEY |
(empty) | LLM bearer token |
HYATLAS_LLM_KEY_FILE |
(empty) | Read the key live from this file on every call, for a rotating key file of your own. The file must hold the token as plain text. A file whose content starts with { (JSON, such as another program's credential file) is refused with one warning in the log, and the key from HYATLAS_LLM_KEY is used instead. Wins over HYATLAS_LLM_KEY, which is the fallback if the file cannot be read or is refused. A key posted to POST /api/v1/llm/key wins over both. |
HYATLAS_RECENCY |
on | off stops newer memories winning close calls in search (see Search) |
HYATLAS_MMR_LAMBDA |
0.75 | Relevance weight of the diversity pass in search, in (0, 1]; 1 turns it off (see Search) |
HYATLAS_INTENTION_TTL |
168h (7 days) |
How long an L7 intention without a due date stays in search results. Older intentions are kept but not returned. 0, off, false or no means no limit: they are all returned. An intention with a due date stays until a day after it is due, then drops out |
HYATLAS_SNAPSHOT_EVERY |
24h |
How often the server zips its data directory (first one a minute after start). 0 or off turns snapshots off |
HYATLAS_SNAPSHOT_KEEP |
7 |
How many snapshots to keep; older ones are deleted |
HYATLAS_SNAPSHOT_DIR |
<data>-snapshots |
Where snapshots go: a folder next to the data directory by default; do not point it inside the data directory |
HYATLAS_RECONCILE |
off |
on: after extraction, one more LLM call compares the new facts with similar stored ones (cosine 0.70 or more) and merges repeats (UPDATE, which keeps a recurring event as a series), retires facts that are no longer true (SUPERSEDE), or links related facts without retiring them (LINK). Doubles the LLM calls per write: meant for a local model, not a hosted free tier |
HYATLAS_EPISODE_GAP |
30m |
pro and ultra: a gap between an owner's writes longer than this ends an episode of work, which then gets one L4 summary. 0 or off writes no episode summaries |
HYATLAS_NOW_EVERY |
15m |
How often an owner's Now page may be rebuilt after writes (pro and ultra only). 0 or off rebuilds after every write |
HYATLAS_USAGE_LEARNING |
on | off stops search learning from which recalled memories replies actually used |
HYATLAS_LLM_FALLBACK_MODEL |
(unset) | A second model, tried only when a call to the primary fails (error, empty reply or timeout). The primary then gets two thirds of the call's time. Unset means no fallback |
HYATLAS_LLM_FALLBACK_BASE |
(the primary's) | OpenAI-compatible endpoint for the fallback model |
HYATLAS_LLM_FALLBACK_KEY |
(see purpose) | Key for the fallback endpoint. Unset reuses the primary key or key file only when the fallback is on the primary's host; a fallback on another host gets no key rather than the primary's |
HYATLAS_EXTRACT_TIMEOUT |
300s |
How long one extraction call may take |
The binary reads no settings from the command line. It answers only --version
and --help; any other argument is refused instead of starting a second server.
Durations. Every duration setting (the HYATLAS_CONSOLIDATE_* intervals, HYATLAS_RAW_RETENTION, HYATLAS_CATCHUP_EVERY, HYATLAS_EPISODE_GAP, HYATLAS_NOW_EVERY, HYATLAS_INTENTION_TTL, HYATLAS_SNAPSHOT_EVERY) reads a value the same way. 15m or 6h is a Go duration, a bare number is seconds, and 0, off, false or no turns the feature off. An empty value uses the default. A value that is none of these logs a line and uses the default, so a typo never changes the schedule silently.
Snapshots. The server backs itself up: every HYATLAS_SNAPSHOT_EVERY it writes
hyatlas-<UTC time>.zip of the whole data directory to HYATLAS_SNAPSHOT_DIR,
pausing writes while it copies, and keeps the newest HYATLAS_SNAPSHOT_KEEP. To
restore, stop the server, replace the data directory with a snapshot's contents,
and start it again.
Stopping. Ctrl-C or SIGTERM stops the server cleanly: it finishes the requests in
progress, waits up to 10 seconds for background extractions that writes just served,
then writes the doc index and usage counts. Whatever extraction is still left is
picked up by the catch-up loop on the next start. A hard kill skips all of this, so
the Windows launcher (hyatlas-go.ps1 stop) and the Hermes plugin's stop on Windows
call POST /api/v1/flush before they stop the process.
Model assets
The BGE-small model + the platform-matching onnxruntime shared library live in models/ (gitignored). For a plain build, the server reads them at runtime. For an embedded build, go:embed bundles them into the binary at compile time from cmd/hyatlas-go/models/ (copy the same files there before building), so the release binary (built with -tags embedded) needs no model folder at all.
Where a plain build looks (when HYATLAS_MODEL_DIR is unset), in order:
<cwd>/models(a checkout run from the repo root)<exe dir>/models(the folder the binary sits in)- the installer's default:
~/.hyatlas/models, or%LOCALAPPDATA%\hyatlas\modelson Windows
If HYATLAS_MODEL_DIR is set, that directory is the only one searched. A plain build without a model in the searched places refuses to start and names every directory it checked. An embedded build never searches these places: it unpacks the bundled model into a temporary directory at startup.
Directory layout (per platform):
| OS | Models dir contents |
|---|---|
| Windows | bge-small-en-v1.5.onnx + .onnx.data + vocab.txt + onnxruntime.dll |
| Linux | bge-small-en-v1.5.onnx + .onnx.data + vocab.txt + libonnxruntime.so |
| macOS | bge-small-en-v1.5.onnx + .onnx.data + vocab.txt + libonnxruntime.dylib |
Where to get them:
- Model:
https://huggingface.co/Xenova/bge-small-en-v1.5/resolve/main/onnx/model.onnx(saved asbge-small-en-v1.5.onnx) and.../resolve/main/vocab.txt. The release workflow and the installer use these URLs. - onnxruntime 1.28.1: the release archives that
.github/workflows/release.ymldownloads,onnxruntime-<platform>-1.28.1from microsoft/onnxruntime releases. Take thelib/file for your platform (onnxruntime.dll,libonnxruntime.soorlibonnxruntime.dylib).
To regenerate the ONNX from the HuggingFace model, see scripts/export_bge_onnx.py.
Version pins (critical)
onnxruntime_gov1.32.0 — declares ONNX API 28onnxruntime1.28.1 shared library — must expose API 28- The
.dataexternal weights resolve relative to the process CWD. The embedderchdirs to the model dir while it loads the model, then changes back.
What changed since v3.5.0
- Pure Go rewrite — single binary (about 160 MB embedded release), no Python venv, no zvec, no Kuzu, no FastAPI
- In-process BGE embeddings via onnxruntime-go (cgo) — no HTTP embed subprocess
- L4 Summary enabled — was dormant in v3.5
- L5 bitemporal graph (v4.1.0+) — every L5 relation carries its source L2 memory IDs plus a bitemporal timestamp; the
/api/v1/graph-as-of?ts=<unix>endpoint lets you rewind the graph to any past moment. - Mind Palace (v4.1.0+) — a temporal visualization of the L5 knowledge graph in the Hermes Desktop
hyatlaspane. Its design is inplugins/hyatlas/desktop/SPEC.md. The Desktop app itself is not part of this repository. - Living memory (v4.6.0–v4.7.x) — the Now page, L8 entity pages, usage learning, topic-clustered consolidation with drop reasons, guards and restore.
- Precision per fact (v4.8.0) — event dates and date-aware search, updates with counts, intention due dates, episode summaries, history on read, near-duplicate demotion, idle-time recency, and the localhost key hand-over that replaced reading
auth.json. - A stable floor (v5.0.0) — turns that were never extracted are caught up, forgetting mirrored from Hermes's memory tool, linked facts and recurring events under
HYATLAS_RECONCILE, the current session's intentions first, one alias rule everywhere, a clean shutdown, and a faster, inverted-index search. - Smarter recall (v4.9.0) — task experiences, schemas with evidence, long facts found by their sentences, an entity boost and graph-assisted recall, query-kind weighting, and an opt-in second reconcile call for local models.
API Reference
Base URL: http://127.0.0.1:19528
Method notes: the handlers read their parameters from the query string, the JSON
body, or both. Where a method is shown, that is the one the Hermes plugin and the
docs use. Some handlers also accept other methods, so check routes() in cmd/hyatlas-go/server.go
if you depend on that. Every route is behind the Host/Origin guard described above.
| Method | Path | Parameters and response |
|---|---|---|
GET |
/healthz |
Liveness. Returns {"status":"ok"}. |
GET |
/api/v1/status |
status, version, vdb, embed, llm (ok | unconfigured | unused), llm_model, llm_base, mode, mode_detail, uses_llm, extract_sync, write_pipeline (ok or degraded: …), writes, searches, layers (per layer, live rows; L5 = graph node count), graph_nodes, graph_edges, unextracted (live raw rows that extraction has not reached; the catch-up loop works through them, and lite always reports 0), consolidations (-1 without a slow path; resets on restart), last_consolidated (last report; omitted until a pass has run). graph_nodes and graph_edges count all owners. |
GET |
/api/v1/metrics |
layers, total, graph_nodes, graph_edges (all owners). |
POST |
/api/v1/add |
Body: text (or data, required), user_id, agent_id, session_id, metadata. Returns success, memory_id, extraction_status (done | failed | pending | unconfigured | skipped). A missing text returns 400. |
POST |
/api/v1/search |
Body: query (required), limit, layer, user_id and agent_id (strings, as the Hermes plugin sends them), user_ids and agent_ids (arrays; only the first entry of each is used), min_score (float, overrides HYATLAS_MIN_SCORE; 0 disables the floor), reader (ranking: legacy, hybrid_tag or hybrid by default; see Search), session_id (the caller's session: its own L7 intentions rank above other sessions'). Returns memories: pages, profile, proactive, experiences and normal lists, each item with memory_id, content, score, layer, gmt_created, user_id, agent_id; order, the channel names in the order to show them (now first, then the channels); and now when the owner has a Now page. |
GET or POST |
/api/v1/list |
Query or body: layer, user_id, agent_id, limit (default 20), offset, include_raw (default on; false hides L2 rows when no layer is given), include_superseded (true also returns rows the ultra pass merged or dropped, with invalid_at, superseded_by and superseded_how, how the row was retired: replaced, updated, merged, restated, dropped, done or forgotten; drop_reason is added when there is one). A user_id lists the rows of every ID in its alias group. The string all is not special here. Returns total, offset, limit, memories, layers, graph_nodes, graph_edges. |
GET or POST |
/api/v1/graph |
node (the L5 node ID), user_id, agent_id. Read from the query string or the JSON body; the body wins when both are set. all or empty means no owner filter. Returns node, neighbors, node_count, edge_count (for that owner), extract_err. |
GET |
/api/v1/edges |
Query: n (default 500), k_semantic (default 3), user_id, agent_id (all = no filter). Returns nodes, knowledge (L5 relations, each with sources), co_session, semantic, total_edges. |
GET |
/api/v1/learning/graph |
Query: n, k_semantic, user_id, agent_id. Returns the starmap feed: nodes, edges, memory, stats. |
GET |
/api/v1/graph-as-of |
Query: ts (unix seconds, default now), n (default 500), user_id, agent_id. Returns `no |
README truncated. Continue reading on GitHub