You are reading Nightly documentation for 0.12.6.dev0+g563af09.

This documentation may describe behavior that differs from Stable.

Open Stable documentation

Memory

Memory

Holaryn Agent keeps a durable local memory: everything it remembers lands in a database on your machine, searchable by keyword and — once you enable a real embedder — by meaning. No memory ever leaves your machine unless you point it at your own server.

How recall works

Recall is hybrid: every query runs two searches and merges the ranked results.

Leg Machinery Finds
Keyword SQLite full-text search (FTS5) Memories containing the query's words
Semantic An embedder + a vector index (sqlite-vec by default) Memories that mean what the query means, even with no shared words

The agent uses two tools against this store: remember (request promotion of an entity or episodic fact into an explicit scope) and recall (search one authorized scope). Both are ordinary tools under the autonomy policy, in the memory.write / memory.read categories. Session context is not saved merely because the model saw it; a matching automatic-saving policy can save, queue for review, or pause the promotion.

Every recall is explainable. The store records the structured/hybrid path, candidate ids and versions, keyword/vector/recency/confidence/provenance/pin/conflict score components, result and token budgets, and why each candidate was included or omitted. It retains only a query digest—not the prompt—and bounds this diagnostic history to 500 decisions.

Why recalled assertions are not automatically evidence

Holaryn tracks who wrote a memory separately from what kind of document the memory claims to
describe. A conclusion saved by the agent is sealed as a model assertion, defaults to the review
queue, and has capped confidence. Approval allows recall but does not certify the conclusion.
Team-member assertions and memories created before this provenance contract are treated the same
way for grounding: they can tell the agent what was said before, but they receive no citation id
and cannot serve as proof of their own claims.

A factual memory gets a validated citation only when a trusted importer independently records the
source and its revision. Operator-authored commitments, decisions, instructions, preferences, and
procedures can also be cited within the operator-intent domain. Pinning, confidence, repetition,
or labeling an assertion as a file never promotes it. In Settings, every result remains
inspectable and shows its evidence class and provenance contribution even when it is
attribution-only.

Memory blocks: always-present notes

Alongside searchable memory, the agent curates named markdown blocks that are injected into the system preamble of every new session — never subject to retrieval, simply always there. Three tools manage them: memory_pin(name, content) creates or replaces a block, memory_append(name, content) adds a line, and memory_unpin(name) deletes one. Budgets force curation: 4,000 characters per block, 12,000 injected in total.

Each block is one plain markdown file under <state dir>/memory-blocks/<name>.md — you can read and edit them with any text editor; edits take effect at the next session.

Embedders: from keyword-only to semantic

Out of the box the agent runs on the hash embedder: deterministic token hashing with zero dependencies and zero network — but no semantic recall. The agent warns you once per session:

memory is using the default hash embedder - recall is keyword-only. Enable semantic
memory in Settings -> Memory (one-click FastEmbed install), run `holaryn memory
install-extra fastembed`, or configure Ollama embeddings

Pick a real embedder in Settings → Memory (or via HOLARYN_EMBEDDER):

Choice What it is Semantic recall
hash (default) Token hashing; no downloads, no network, ever No
fastembed Local in-process embeddings; one-time model download (~70 MB default model), fully offline after that Yes
ollama A running Ollama server's embedding endpoint Yes
custom Your own module:attribute implementing the Embedder protocol Up to you

fastembed needs the optional extra — install it with the one-click Install button on the
Memory page, holaryn memory install-extra fastembed, or uv sync --extra fastembed from an
authorized source checkout. The button and install-extra install exactly the versions your
release locked, each file hash-checked, from the constraints file that ships with the agent;
in a source checkout they install nothing and point you to uv sync --extra instead (see
embedding). The default model is BAAI/bge-small-en-v1.5 (full registry names
required); for Ollama the default is nomic-embed-text, which you must ollama pull first
(HOLARYN_EMBED_MODEL and HOLARYN_OLLAMA_HOST configure both). Ollama requests carry memory
text, so they ignore HTTPS_PROXY-style proxy and certificate variables in the host's
environment unless you set HOLARYN_TRUST_ENV=1 (see
proxies and private certificate authorities).

There is never a silent fallback: selecting an embedder that cannot run fails with an instruction telling you how to fix it.

The mismatch guard and reindexing

The store records which embedder built the vector index. If the active embedder no longer matches (you switched embedders or models), semantic recall is disabled — never mixed with incompatible vectors — until you reindex. Your memories are never lost: text is the source of truth; vectors are derived data.

Run a reindex after any embedder change, from Settings → Memory or the CLI:

holaryn memory status     # active vs recorded embedder, match state, vector counts
holaryn memory reindex    # re-embed everything with the active embedder
holaryn memory install-extra fastembed|qdrant|postgres|mariadb

Reindexing is idempotent and safe to run at any time.

holaryn memory status reads the memory file to check its encryption, which can take a few seconds on a large memory store.

Reviewing and controlling memory

Settings → Memory is the operator view. In addition to store/embedder status and search, it provides:

  • filters for scope and lifecycle state, a review queue, and keyboard-native batch approval, archive, pin, and unpin;
  • writer provenance, evidence class, grounding eligibility, confidence, consent, sensitivity, retention, conflicts, immutable version history, and retrieval score reasons;
  • edit-as-new-version, pin/unpin, archive/activate, conflict/supersession links, verified deletion, and source-wide forget;
  • atomic verified JSON export into the state directory's exports folder;
  • the memory expiry policy and the expiry sweep (below); and
  • automatic-saving rules by exact/wildcard scope, category, and source type: Save automatically, Require review, or Pause saving.

Pending-review and archived records are stored but never recalled. Superseded records are marked stale and deindexed; conflicting active records remain visible with uncertainty rather than silently replacing one another. Editing reindexes the new version. Deletion removes primary, keyword, and vector representations before leaving a digest-only tombstone; if the vector backend cannot confirm its delete operation, the primary record is preserved and the operation fails.

A memory can carry a retention date. Once it passes, the memory is expired. By default
(exclude) the agent stops recalling it at once; with schedule it stays recallable until a sweep.
Either way it stays listed as expired until a sweep tombstones it through the verified delete path.
Sweeps run only when you ask, from the Memory page or the CLI, unless you opt in to a scheduled one:

holaryn memory policy                      # show the expiry policy
holaryn memory sweep --dry-run             # how many expired memories a sweep would remove
holaryn memory sweep                       # tombstone them now
holaryn memory sweep --schedule 'cron:0 3 * * *'   # opt in to a nightly host sweep
holaryn memory explain epi_0123            # why a memory exists and what it counts as

Scopes never bleed into one another: personal, project/profile, team:<id>, and organization-managed scopes are exact retrieval boundaries. A prior export is an external copy, so later source-forget cannot erase JSON that an operator moved or backed up elsewhere.

See the explainable-memory architecture and recovery contract for score formulas, promotion precedence, completion reports, and deliberate v0.7.9 boundaries.

Bigger setups: server databases

Defaults are a single local SQLite file — zero setup, right for one machine. Two panels in Settings → Memory scale beyond that:

  • Memory database — point at a PostgreSQL server (with pgvector) or MariaDB 11.7+ to share one durable memory across several Holaryn instances. Install the matching extra (holaryn memory install-extra postgres or holaryn memory install-extra mariadb), enter the server URL without a password, and set the password in the write-only field (or HOLARYN_MEMORY_DB_PASSWORD) — it is never written to a config file. Exactly one backend is active at a time; switching starts with an empty memory, and every instance sharing a database must use the same embedder.
  • Vector database — episodes and entities always live in the memory database; this panel only chooses where search vectors go. Default is local sqlite-vec; pick Qdrant server (extra: holaryn memory install-extra qdrant, API key via HOLARYN_QDRANT_API_KEY) for large memories or shared infrastructure. If the vector server is unreachable, memory keeps working with keyword-only recall and heals on the next reindex. With encrypted local state, search vectors are stored encrypted in a memory file this version creates or rebuilds: each vector is encrypted on its own under a key derived from the memory key. A memory file from an earlier release still holds its search vectors unencrypted until holaryn memory reindex rebuilds it, and their old bytes stay in the file's unused space until a cleanup erases them. holaryn memory status says what applies to your store. In Qdrant, vectors are never encrypted. Without encrypted local state, the memory file and its search vectors are not encrypted. See encrypted local state.

Credentials and backend URLs

The Qdrant URL, the Ollama host and the memory database URL are ordinary configuration: they are
stored in the config file or the environment, shown in Settings and, in reduced form, recorded in
the memory database. So Holaryn reads them by one strict grammar that admits only parts that cannot
carry a secret, and builds every client (qdrant-client, httpx for Ollama, psycopg, PyMySQL) from
the parts it reads, never from the text you entered (SA-629):

Setting Accepted Put a credential here instead
Qdrant URL (HOLARYN_QDRANT_URL) http:// or https://, a host, an optional port and a path. No user name, password, query or fragment The environment variable HOLARYN_QDRANT_API_KEY for the Holaryn host (restart the host after setting it)
Ollama host (HOLARYN_OLLAMA_HOST) The same The Ollama embedder cannot send a credential. For a server that needs one, choose Custom embedder on this page (or set HOLARYN_EMBEDDER to its module:attribute path) and have it read the credential from an environment variable
Memory database URL (HOLARYN_MEMORY_DB_URL) postgresql://, postgres://, mysql:// or mariadb://, an optional user name, one host, an optional port and the database name. A PostgreSQL URL may also carry application_name, connect_timeout, sslcert, sslkey, sslmode, sslrootcert and target_session_attrs, each once and with a value of its own kind; a MariaDB or MySQL URL carries no options. No password, other option or fragment The database password: the Database password field on this page, or HOLARYN_MEMORY_DB_PASSWORD. The passphrase of an encrypted PostgreSQL client key (sslkey): HOLARYN_MEMORY_DB_SSL_KEY_PASSWORD

The parts themselves are narrow:

  • Host: written in ASCII. A name of letters, digits and hyphens between dots, with no dot at
    the end (enter an international name in its xn-- form, for example xn--bcher-kva.example
    for bücher.example), an IPv4 address in dotted decimal, or an IPv6 address in square
    brackets. A host whose last part is a number (such as example.123 or 0x7f.0.0.1) must be a
    dotted-decimal IPv4 address.
  • Port: a number from 1 to 65535, or none.
  • Path (Qdrant, Ollama): parts of letters, digits, ., _, ~ and -, each after one
    /. No two slashes in a row, no part that is only . or .., no percent sign, @, : or
    backslash. One slash at the end is ignored.
  • User name and database name (database): letters, digits, ., _ and - (the database
    name also ~). No percent-encoding, no Unix socket and no second host. A URL without a user
    name connects as the account the Holaryn host runs as (see below).
  • Anywhere: no space, no control character. Spaces before and after the whole value are
    ignored.

The path is public. It is kept and shown like the rest of the URL, so it must not contain a
token. A reverse proxy that needs one must take it some other way.

When a URL is checked. Settings refuses a value when you save it, with a message that says
what is wrong and what to use instead, and never repeats the value. A value set in the environment
or saved by an earlier release is checked when memory is built: when a session starts, by the
holaryn memory commands, by the readiness check and by this page. Starting the host does not
build memory, so the host starts, but memory does not open: the Store status says why (after the
words "The memory database is unreachable:") and where the value comes from, and the settings
form leaves the refused value out.

Resumed work. A run records which memory destination it may use. When the host resumes a run
journaled before the upgrade, it keeps that run's memory only if the database, the Ollama host and
the other memory settings are as they were and the vector backend is not Qdrant (an earlier release
did not record the Qdrant URL, so an unchanged Qdrant deployment cannot be shown). Kept memory is
then built, and a URL that is now refused fails the run, which is recorded as failed with the
refusal; correct the setting and start the work again. In every other case the run resumes with
memory turned off.

Only the binding of a release before SA-629 is recognised that way. A run journaled by a
development build of this change before its final form (run binding versions 2 and 3, never
released) resumes with memory turned off even when its settings did not change: those bindings
did not include every setting the store now connects with, so they cannot show that the
destination is the same. Start such work again to give it memory.

What is recorded and shown. Holaryn names a backend by its identity: the Qdrant collection or
the Ollama model and the URL in canonical form (for example
qdrant:holaryn-memory@https://qdrant.example:6333/proxy), and a server database by its location
with its user name and database name. The canonical form is rebuilt from the parts: a lower-case
scheme and host (an IPv6 address in its short form), the port the client dials (Qdrant's client
dials 6333 when the URL names no port; for Ollama an explicit 80 or 443 is left out) and the path
without the slash at the end. The memory database stores this identity to detect a backend
change, and holaryn memory status, the readiness check, this page, log lines and error messages
show it. Two servers behind one host and port that differ by path have different identities, so
moving between them asks for a reindex. The run binding also takes the database settings
Holaryn connects with (next section).

Errors. Holaryn's own errors for the Qdrant index, the Ollama embedder and the PostgreSQL and
MariaDB stores name the backend and say what failed, then the error's type: for example "Qdrant
search failed for the collection memory at https://qdrant.example:6333. Cause: connection failed.
Error type: ConnectError." A database error adds its SQLSTATE or error number. None carries what
the client library said, and none has the library's exception attached. Holaryn turns off
qdrant-client's start-up version check, which put the server's reply into a warning. Not covered:
a FastEmbed or custom embedder reports its own errors, and the client libraries' own debug-level
log records are theirs.

If a credential was ever in one of these URLs, rotate it, and move it first. Earlier releases
recorded the whole Qdrant URL and Ollama host in the memory database and showed them in status and
errors. Move the credential to its setting above before you save a section of this page: the
form leaves a refused value out, and saving writes what the form shows over the stored value (the
host logs a warning that names the setting, not the value, once the save is written). On its
first open after the upgrade, the store rewrites a recorded identity without the credential. It
rewrites it to the canonical form, which needs no reindex, only when the server the earlier
release reached is certain: a user name and password never changed it, and qdrant-client ignored
a query and a fragment. Any other record (an Ollama host with a query or fragment, a value without
a scheme, an international name, anything the grammar refuses) is rewritten as
<address not shown>, which reads as a backend change: reindex when the store asks. The old value
is not removed: SQLite keeps it in free pages and the write-ahead log until the file is rebuilt, a
PostgreSQL or MariaDB server keeps it until its own cleanup reclaims it, and backups and copies
made before still hold it. Upgrade every instance that shares one server memory database
together; an older instance reads the rewritten record as a backend change.

What decides the memory database connection

A PostgreSQL client (libpq) fills every connection setting it is not given from an environment
variable, a service file, files in the account's home directory or a default compiled into it,
and PyMySQL takes a missing user name from the environment. So the memory database URL alone
would not say where the connection goes, as whom, or with what trust. Holaryn passes every
setting that decides those explicitly, and keeps a PostgreSQL store closed while an environment
variable libpq would read for a setting the URL leaves out is set (SA-629). The Store status then
names the variables, never their values; unset them for the Holaryn host and restart the host, or
put the setting in the URL where the URL has one. The list is the one libpq 18.0 reads
(psycopg-binary 3.3.4 bundles libpq 18.0.3); a variable a later libpq adds is refused too,
because the store reads the list of the libpq it loads. If that list cannot be read, the store
stays closed and the Store status says that the PostgreSQL client library's settings could not
be read.

Source Rule Why
Host and database name (PGHOST, PGDATABASE) Always taken from the URL, which must name both They choose the server and the database
Port (PGPORT; libpq's default 5432) Taken from the URL, else 5432 (MariaDB 3306), passed explicitly. PGPORT set while the URL has no port keeps the store closed It chooses the server
User name (PGUSER; libpq's and PyMySQL's defaults) Taken from the URL, else the name of the account the Holaryn host runs as, read as libpq reads it (the account's entry on Linux and macOS, GetUserName on Windows), never from USER or a similar variable; passed explicitly. PGUSER set while the URL has no user name keeps the store closed It chooses the login
PGHOSTADDR Keeps the store closed It sends even an explicit host and port to another address
PGSERVICE Keeps the store closed. Without a service name, PGSERVICEFILE, PGSYSCONFDIR and the service files (~/.pg_service.conf, pg_service.conf) are never read A service entry can fill the server, the login and the TLS settings
PGSSLMODE (and the older PGREQUIRESSL) sslmode is the URL's, else prefer (verify-full with sslrootcert=system), passed explicitly. Set while the URL has no sslmode, it keeps the store closed TLS policy
PGTARGETSESSIONATTRS, PGCONNECT_TIMEOUT The URL's, else any and 10 seconds, passed explicitly. Set while the URL leaves them out, they keep the store closed Which server is accepted; how long to wait
PGGSSENCMODE gssencmode=disable, passed explicitly to libpq 12 and later (an earlier libpq has no GSSAPI encryption and does not know the keyword); the variable keeps the store closed GSSAPI encryption would replace TLS, so sslmode alone decides
PGSSLROOTCERT Keeps the store closed while the URL has no sslrootcert It chooses the trust anchors
PGSSLCRL, PGSSLCRLDIR, PGSSLSNI, PGSSLNEGOTIATION, PGSSLCOMPRESSION, PGSSLMINPROTOCOLVERSION, PGSSLMAXPROTOCOLVERSION, PGMINPROTOCOLVERSION, PGMAXPROTOCOLVERSION, PGCHANNELBINDING, PGREQUIREAUTH, PGKRBSRVNAME, PGGSSLIB, PGGSSDELEGATION, PGOPTIONS Keep the store closed They change how the connection is secured or authenticated, or the session (PGOPTIONS can set a role); the URL has no setting for them
Any other variable the loaded libpq reads Keeps the store closed Its effect is not known here
SSL_CERT_FILE, SSL_CERT_DIR with sslrootcert=system Keep the store closed They replace OpenSSL's system trust store
PGPASSWORD, PGPASSFILE, the password file (~/.pgpass; on Windows %APPDATA%\postgresql\pgpass.conf) Used when Holaryn has no password of its own; Holaryn's password, when set, is passed and wins A credential Holaryn never stores or shows; it changes neither the server nor the user name. An operator may keep the password out of Holaryn this way
PGSSLCERT, PGSSLKEY, PGSSLCERTMODE, ~/.postgresql/postgresql.crt and postgresql.key (Windows %APPDATA%\postgresql\) Used when the URL names no client certificate A credential; the user name it logs in as is fixed above
~/.postgresql/root.crt and root.crl (Windows %APPDATA%\postgresql\) Used as libpq documents: root.crt makes sslmode=require verify the server like verify-ca, and is the trust store for verify-ca and verify-full without sslrootcert; root.crl can only reject more libpq has no setting that turns them off (an empty sslrootcert falls back to the file). Put sslrootcert in the URL to choose the trust store yourself
PGAPPNAME, PGCLIENTENCODING Used A label the server shows (the URL's application_name wins); the text encoding
PGREQUIREPEER, PGLOADBALANCEHOSTS Used No effect on one TCP host: one applies only to Unix-domain sockets, which the URL cannot name; the other orders the addresses of the one host name
libpq's compiled defaults (channel_binding=prefer, sslnegotiation=postgres, sslsni=1, ssl_min_protocol_version=TLSv1.2, krbsrvname=postgres) Apply as built Fixed by the bundled library, not by the host's environment
Kerberos credentials (KRB5CCNAME), OPENSSL_CONF Not controlled A credential cache libpq uses only if the server asks for GSSAPI authentication; OpenSSL's process-wide configuration

The run binding of a resumed run takes the same effective parts: the port and the user name
Holaryn passes when the URL leaves them out, and every libpq setting above it passes. A URL with
and without the default port, or without a user name and with the account's name, bind alike.

The TLS file options. sslcert, sslkey and sslrootcert must be paths libpq reads as
files: parts of letters, digits, ., _, ~ and - between forward slashes. On Windows a path
may start with a drive letter and a colon (C:/holaryn/client.key); no other colon is allowed,
and on Linux or macOS none at all, because libpq reads a colon in sslkey as an OpenSSL engine
and key (engine:key), not a file. sslrootcert may also be the word system, the system trust
store. libpq then requires sslmode=verify-full, which Holaryn passes when the URL has no
sslmode; any other sslmode is refused. It needs libpq 16 or later; on an earlier library
Holaryn refuses it, since that library would read system as a file name.

MariaDB, Qdrant and Ollama. PyMySQL reads an option file (my.cnf) only when asked to, and
Holaryn does not ask; it reads no MYSQL_* variable. Its default user name comes from USER and
similar variables, so Holaryn always passes the user name (above). The Qdrant client and the Ollama
embedder use httpx, whose proxy, certificate and .netrc variables apply only when
HOLARYN_TRUST_ENV=1 (see connections and models); that is
unchanged.

Troubleshooting

Symptom Likely cause Fix
Recall feels like plain text search Still on hash Pick fastembed or Ollama, then reindex
"embedder mismatch" in status Changed embedder/model without reindexing holaryn memory reindex
fastembed import error Extra not installed Install button, holaryn memory install-extra fastembed, or the installer with fastembed added to its extras
Ollama connection error Server down, wrong host, or model not pulled Start Ollama, check the host field, ollama pull <model>
Vector count is 0 after switching Index not rebuilt Reindex
A memory is not recalled It is pending review, archived, stale, in another scope, or over the retrieval budget Inspect its state and latest decision in Settings → Memory
Edit or batch action reports a version conflict Another action created a newer immutable version Refresh the list/detail and retry against the new version
Delete/forget fails while using Qdrant The vector backend did not complete deletion Restore the backend connection, then retry; Holaryn keeps the primary record rather than claiming completion