hermes atlas
310·repos hermes·v0.21.5 ★ star this repo

itpartypattaya/hermes-plugin-holographic

Holographic memory provider for Hermes Agent: local SQLite fact store, HRR retrieval, 18 languages. Maintained by itpartypattaya.

★ 35 langPython licenseMIT updated2026-11-07

Holographic Memory Provider is a local memory provider for Hermes Agent that stores facts in SQLite and supports FTS5 search, trust scoring, entity resolution, and holographic retrieval. By default, it requires no API key, network access, or model calls; meaning-based search is an optional online add-on.

  • Stores facts in a local SQLite database
  • Supports trust scoring and entity resolution
  • Optional online search by meaning

Basic static screen: f75c636 (November 9, 2026), not security certification. Local by default. Optional embeddings and reranking send selected memory content to configured endpoints. Secret and injection screens are heuristics, not a security boundary.

full readme from github

Holographic Memory Provider

Local memory provider for Hermes Agent: a SQLite fact store with FTS5 search, trust scoring, entity resolution and HRR (holographic reduced representation) retrieval. By default everything stays on your machine: no API key, no network, no model calls. Search by meaning is an optional online add-on, off unless you turn it on.

Maintained by itpartypattaya since October 2026. This is the provider that shipped inside Hermes core as plugins/memory/holographic/; Nous Research handed it off when the bundled copy was scheduled to leave core (announced for October 15, 2026). Same provider name, same config, same database: users who have memory.provider: holographic keep their facts. See CHANGELOG.md for what changed.

Task outcomes and memory explanations (0.20.0)

An accurate fact can still be the wrong answer to a task — for example, a fact about another person with the same name. The optional local experience journal records the actual fact versions returned, the task outcome and a short correction separately from truth/trust. It does not change ranking or automatically correct a fact.

Add to plugins.hermes-memory-store in config.yaml, then restart the gateway or start a new CLI session:

experience: on
experience_trace: on  # optional: candidate/stage ids as well as the actual emission receipt

Both are off by default. When enabled, fact_experience provides recent, outcome and explain; a search returns memory_attempt, and prefetch identifies a nonempty emission; a prefetch that recalls nothing writes nothing. With collection on, one line in the system prompt asks the agent to record an outcome when the person corrects or rejects an answer that used recalled facts (0.20.3); with it off the prompt is unchanged. Explain reads that original snapshot without replaying search or calling a model. Task outcomes are agent reports, never authenticated human confirmations; old fact_feedback calls keep their original meaning and are not imported as duplicate evidence.

Records expire after 30 days, with at most 1000 attempts per scoped database and 10 revisions per attempt; the limits also apply when a writer opens the store with collection turned off. Forgetting a referenced fact/version also erases the dependent task/correction; merge and end preserve history. Detailed tracing costs local storage and synchronous writes; no fixed latency or token-saving percentage is promised. If the journal encounters a busy SQLite writer, it retries briefly with limited SQLite busy waits. After repeated failure the search still returns, but that attempt has no memory_attempt and cannot receive an outcome. Slow-operation warnings contain stage timings only, not fact or query text. This is best-effort recording, not a guarantee against long locks, disk failures or process termination. See the full behavior. Dreaming 3.6.0 can read live summaries without another correction-text cache. See the full contract and examples.

How it works

Architecture: before each turn Hermes core sends the message through the relevance gate and search; writes pass a
validator that refuses instructions and credentials and land in one SQLite transaction with the fact's history; recall
and history tools read the same file; search by meaning is an optional online add-on

Recall before a turn: greetings recall nothing; the message's words become keys; 15 candidates come from FTS5,
character n-grams or a word scan, reordered by meaning if that is on; a relevance gate, a repeat window and the screen
decide; up to five facts by their words, one by meaning alone and two of named entities go into the turn as
data

Install

While your Hermes still bundles holographic, the bundled copy is used and this plugin stays dormant. After core drops it, hermes update installs the catalog plugin named holographic for every profile that has it configured. To install by hand:

hermes plugins install holographic          # from the plugin catalog
hermes config set memory.provider holographic

or hermes memory setup and pick holographic.

The catalog entry may still pin an older release while a version bump waits for review; to install a given release exactly, pass its commit: hermes plugins install holographic --ref <40-character sha of the tag> --force.

Memory went off after hermes update

Core dropped its bundled copy on 2026-10-11 (Hermes v0.21.7) and installs this plugin in its place. If that install fails (a dependency resolution error in the plugin workspace, a network error), Hermes reports "Memory provider 'holographic' is not installed, so external memory is off", and a failed install may leave memory.provider empty and holographic listed under plugins.disabled in config.yaml. Your facts are not touched: memory_store.db stays where it was. To bring memory back:

  1. hermes plugins install holographic (add --force if a copy is already in ~/.hermes/plugins/holographic/); if it fails on dependencies, update Hermes first — the cause is usually in core's lock, not in this plugin.
  2. In config.yaml: remove holographic from plugins.disabled, and set memory.provider: holographic.
  3. Restart the gateway (or start a new CLI session) and check with hermes memory status.

Config

In config.yaml under plugins.hermes-memory-store:

Key Default Description
db_path $HERMES_HOME/memory_store.db SQLite database path
auto_extract false Store user sentences that state a preference or decision (English, Russian, Chinese) after each completed turn, and also before context compression and at session end (not in scope: user, where a whole transcript cannot be attributed), at trust 0.4; questions, replies to the assistant ("I like that, sounds good") and restatements of a stored fact are skipped
mirror_memory_writes false Also store every built-in memory add (MEMORY.md / USER.md) as a fact
default_trust 0.5 Trust score of a new fact
min_trust_threshold 0.3 Facts below this trust are not recalled
temporal_decay_half_life 0 Days; 0 turns recency decay off. Age counts from when the fact's current version was recorded: feedback, keywords and tags do not make a fact younger
hrr_dim 1024 HRR vector dimensions
hrr_weight 0.3 Weight of the HRR term in search ranking
prefetch_min_coverage 0.3 Share of a message's informative words (rarer words weigh more) a fact must match to be recalled before the turn; a fact sharing one word of a longer message is recalled only when no other fact holds that word or it may be a name (capitalized inside the sentence, or in a script without capitals)
prefetch_clause_coverage off Experimental. on: a message that asks two separate questions (Russian or English) gives each of them a second look at the facts the coverage gate left out, so the answer to the shorter question is not lost. Only for a fact that holds two words of its question and names the person that question is about. Not in the setup wizard; a value it does not recognise reads as off. See Two questions in one message
prefetch_repeat_turns 6 A fact recalled before a turn is not injected again for this many turns of the session
prefetch_max_tokens 0 Approximate tokens the block recalled before a turn may take, header and every line included (0.21.0). The budget never shortens a line: a fact whose line does not fit is left out whole; the rest keep their order. The older 300-character cap on each prefetch line still applies with or without a budget, so a longer fact's line ends in "…"; stats counts what was left out. 0 = off: up to five facts as before. See docs/context-budget.md
recall_source off short: each fact recalled before a turn also says where it was written (recorded 2026-10-12 · Anton · this chat; no ids). Off by default: it costs tokens and tells the room which chat a fact came from. list, history and export always show the source in full
cron_writes true A cron job's explicit fact_store / fact_feedback calls may write (nightly memory jobs such as hermes-dreaming's extraction and keywords exist to make them). Automatic writes (auto-extraction, mirroring, recall counts) stay off in cron, subagents and flush contexts; with embeddings on, a cron job that may write also makes missing fact vectors; false makes cron read-only too
recall_indicator true Report recalled facts to the core's recall indicator ("🧠 Holographic — recalled 2 memories"). On Telegram and other messaging platforms that line arrives as a separate message before the reply; false turns it off, recall itself is unchanged. Like every key here it is read when the gateway starts: a change takes effect after a restart
embeddings off on: search by meaning through an embeddings API you name (embedding_url, embedding_model, embedding_key_env, …) — ONLINE: fact texts and messages are sent there. See Search by meaning
rerank off on: rescore the candidate pool before a turn with a cross-encoder (rerank_url, rerank_model, rerank_window, …) — ONLINE: candidate texts and the message are sent there. See Rescoring by a cross-encoder
scope profile user: a separate database per user (holographic-users/<platform>-<user id>.db next to db_path); in a shared chat the store follows the author of each turn, and with no user identity nothing is recalled or written. chat: a separate database per chat (holographic-chats/<platform>-<chat id>.db) — for one bot serving many unrelated groups; thread: per forum topic / thread (…-t<thread id>.db), the chat's own database outside threads. Without a chat (CLI, cron, subagents) nothing is recalled or written. File names: an id made only of A-Z a-z 0-9 _ . - is used as is (Telegram's -1001234567890); any other character becomes _, the name is cut to 60 characters, and -<first 10 hex of sha256(the original "platform-id")> is appended, so distinct ids never share a file

A number that is not one (hrr_dim: abc, min_trust_threshold: high), and a value outside a key's fixed set (scope: users, recall_source: full, embeddings: maybe, auto_extract: sure; 0.17.8 — before, it was read as the default, and a mistyped scope put everyone in one store) turn memory off for the session: the system prompt and every fact_store call name the key and what it must be, never the value, and no database is opened or written until it is fixed. The embedding_* numbers are replaced by their default, or brought into their range, with a warning in the gateway log that names the key. A config.yaml the core cannot parse: Hermes serves the copy it last read whole, and the plugin uses its settings from it with a warning; with no such copy (or a core that raises) nothing tells what scope was, so memory is off until the file reads (0.17.8; it used to run on the defaults). Warnings give the error's type only: a YAML error quotes the line it stopped at, which may hold a key.

Optional task outcomes and recorded explanations (separate from trust, off by default): docs/experience.md.

Two questions in one message (experimental)

«что Злате нужно взять с собой на плавание в четверг и какой чай она любит?» asks two things; the fact about tea holds every word of its question and still covers too little of the whole message to be recalled. With prefetch_clause_coverage: on (off by default) a fact the coverage gate left out gets a second look against each of the two questions alone — only when the message clearly splits into two Russian or English questions, the fact holds at least two words of its question, and, for a question about a person, that person is one entity the store knows and the fact names them. A rescued fact never takes the place of one that would be recalled anyway, with or without the reranker. On a synthetic set it brought both answers into the top five in 15 of 16 two-question messages instead of 12, and changed no answer on about 620 older test questions; it has not been checked on real conversations. What it misses and every risk: docs/two-topics.md.

Tools

Tool Description
fact_store add, search, probe, related, reason, contradict, change, end, correct, revert, merge, forget, history, update, remove, list, stats, export, review, entities, alias, rename, delete_entity, split, rebuild_entities, enrich
fact_feedback Rate a fact helpful or unhelpful (moves its trust score), or record that the person confirmed it (0.15.0)
  • search: keyword candidates in any word form (Маши finds Маша, deploys finds deploy, kucingnya finds kucing; see Languages), reranked by word overlap and HRR similarity, weighted by trust.
  • Before each turn the provider recalls up to five facts: search candidates that pass a relevance gate (the fact must cover enough of the message's informative words; "ok, thanks" recalls nothing) plus the top facts of entities the message names. A fact already injected in this session is not repeated for prefetch_repeat_turns turns. Facts are framed as data (#id [trust] text), one line each. prefetch_max_tokens caps the block in approximate tokens by leaving whole facts out (docs/context-budget.md).
  • add takes entities (who the fact is about; needed for one-word names such as a first name, so later facts mentioning it in any form link to it), keywords (see Synonyms) and valid_from (the date it started to hold, when the message says). A name in entities that cannot be one (a path, a URL, key=value, a single letter, not a string) is not linked, and the answer of add, update, change and correct lists it under entities_not_linked by its position in the list (index, from 0) with the reason, not the name itself (a refused name may be KEY=secret); the fact is stored all the same. entities given with a new text (change, correct, update) replace the names declared on the fact before; without them those stay, but for a name only the old text gave; revert brings back the names of the version it restores (see Who a new text is about). Its answer lists similar facts, possible_conflicts (same subject, a different claim) and past_versions; the agent decides. Text that is not a fact (JSON, tool or chat-role markup, instruction-like phrasing, a credential with its value or a token whose shape alone says it is one — an API key prefix, a Telegram bot token (also inside a Bot API address), a JWT, a private key (0.14.5) —; instructions and credentials in all eighteen languages of Languages, also glued to Chinese text (0.15.1); an environment variable that holds a key, a token or a password, by its name — any in capitals ending like …_KEY or …_TOKEN and 310 popular ones of model and service providers — and an address with a password in it (0.15.2) —, an image (markdown or HTML), invisible characters, long blobs, >1000 characters) is refused with the reason. A name that begins with a credential word joined to the rest by a hyphen is no credential when the whole word names a thing (0.17.2, issue #6): a file with a common extension (token-saver-stats.jsonl, plugins/token-saver/filters/web.yaml), a host of three labels and more (password-reset.example.com), a package with a version after == or @ (token-saver==1.2, token-saver@1.2.3); a file name shaped like a password is none (password-Secret2026.conf, password-P@ssw0rd.txt: a capital with a digit, or "@"), nor is an e-mail address. A credential word that stands alone is followed by its value, whatever it looks like: password Summer2026.txt, password: secret/hunter2.txt, Пароль файла: Лето2026.txt, password-wifi: Qwerty123, Password wifi: correct-horse, and the token counter.py module and Token file: tokens.json too; so is a dot that is no extension (password-Qwerty.123), a one-letter or a key file's extension (token-Secret.key), a two-label host (password-letmein.co), a tag after : (password-wifi:192.168.1.1, and so token-service:1.4). The cost, chosen: password-hunter2.txt is not caught. A request to send a credential stays refused whatever follows the word ("send credentials-backup.json", and so "share the token-saver report" too); a Russian credential word counts as a word, not as the start of one (секретарю, ключевые цифры, учётная политика, токенизатор, "пин на карте" are none; пин-код, "пин от карты", секретная фраза, фраза восстановления are). Recall checks a stored fact again before it goes into a prompt (0.13.0): one an older release, or another program, wrote past today's rules (markup, instructions, credentials, invisible characters, images) is left out of recall and logged once; search, list and history still show it, so it can be changed or forgotten. A quoted instruction to the assistant (a note quoted as telling it to drop its rules) may be stored as a report of what something says, but recall leaves it out too (0.14.3): in the prompt it reads the same as the bare instruction.
  • stats (what is stored and how much of it was ever recalled; recall_error while recall before a turn fails, so a broken store does not pass for one with nothing relevant), export (Markdown view), review (clean-up candidates: low trust, never recalled for 60 days, near-duplicates), enrich (facts without keywords, most recalled first, to give keywords with update).
  • entities lists names with their aliases, fact counts and standing (0.16.0): established when someone named it (entities=[...], alias, rename) or two or more facts name it, provisional when the rules below guessed it from one fact and nobody confirmed it; status lists one kind, and the answer says how many provisional names the store holds. A name whose letters have no case (Chinese, Japanese, Thai, Arabic...) that the facts only ever put in plain quotes stays provisional however many facts quote it (0.16.2): in Chinese a button or a step goes in quotes as readily as a name. Then it takes a person naming it, or two facts of which one uses it in title marks 《》, in "X aka Y" or in running text where mentions are found (a name of several words, or one a person declared: a one-word Chinese name nobody declared is not looked up in running text, where a quoted label is written bare as often). alias gives an entity a second name (or merges two entities; entity stays the name). rename gives it another name (the old one becomes an alias; a name another entity has is refused, so a typo never merges two). delete_entity removes a name that is not one — a phrase, a command, a label — with its aliases and links (entities: [...] for up to 100 in one call, 0.16.2); every fact stays, a tombstone (a keyed hash, not the name) keeps text from creating it again until someone names it explicitly, and the file is rewritten as after forget. An alias "X aka Y" in a fact's text gave goes with the last fact that says it, and so does X on the facts only that alias linked; an alias written before 0.16.0 is of unknown origin, kept like a given one and dropped only when a forgotten fact said it. A name with a comma is no name (the legacy aliases column is comma-separated). review lists provisional names older than 60 days under entity_candidates. Spelling variants are one entity (Hermes Agent = hermes-agent), and X aka Y makes Y an alias.
  • An alias a fact's text gave is a hint, not a decision (0.17.0). "X aka Y" lets later texts that say Y link to X, and probe Y find X's facts — but no action someone takes with a name is resolved through it: a name declared (entities=[...] in add, update, change, correct) that is only such an alias becomes an entity of its own, and the answer says so under split, with the alias call that makes them one again; alias, rename and delete_entity by it are refused, and so is rename of someone else into it (its hint would have merged the two). Deleting X drops such aliases without keeping Y from being found again (text_aliases_dropped). Until 0.16.3, "Alice Smith aka Bob Brown" after facts about Alice made a later entities=["Bob Brown"] file the second person's facts under her, and rename or delete_entity by "Bob Brown" renamed or deleted her. alias {entity: X, content: Y} confirms such a name; split {entity: <an alias>} makes any alias an entity of its own. Facts whose text names Y then go to Y; one whose text names neither stays with X if someone declared X on it, and the answer lists it under facts_to_check — why a declared link exists the file does not record. If someone gave X the alias (alias), X stays an entity after the split even without facts; otherwise the answer says from_dropped when X went with its last fact. entities shows such names as text_aliases, review lists them under alias_candidates however old (up to its limit), stats gives their number. What this does not do: a text that says Y still links to X, so recall can bring X's facts for a message about Y until someone splits them; it guards what this version writes — an older version writing the same file still resolves a declared name through such an alias; and an alias written before 0.16.0 is of unknown origin even if a text gave it: it is shown under aliases, not refused, and split separates it. A name declared with a text already stored (an add of the same text, a correct of the date only, a change into another fact's text) now counts too; until 0.16.3 it was dropped.
  • probe: every fact linked to a named entity, from the entity index (name or alias, any letter case). reason: facts linked to all of several entities; when a name is not a known entity, keyword search that still requires every name. Entities are linked when a fact is stored: capitalized names of two to five words in any cased script on one line, ending where the script changes (Alice Smith, Мария Иванова, Jean-Luc Picard; "Docker Hub直连可用" gives Docker Hub), short quoted terms ("Phoenix", «Сбер Бизнес», “华为”, 「华为」, 《平凡的世界》, straight quotes beside Chinese characters too) and both sides of X aka Y (Y becomes X's alias only while no fact has named Y on its own); code spans and blocks give none, and a known name of several words is mentioned only as written together on one line (in Chinese, never across a seam between two words); a name begins with a letter or a digit, holds no invisible character, passes these checks in its NFKC form too, and flags, key=value, paths, URLs, IPs and commas are not names, and neither is a version number with nothing beside it that names a thing ("0.17.0 installed"; "Python 3.12" is one), a quoted span whose letters with case are all lower case and that holds no identifier with a letter in it ('count', '14 days', «быстро беги»; 'kanban.db' and 'gpt-4o' are), a quoted greeting ("Good morning", «Спасибо большое», “谢谢”), a count ("3个月", "15分钟", "1 小时": an Arabic number and units; 360安全卫士 and 115网盘 are names), a quoted Chinese span that reads as something said — ending in a sentence-final particle (“知道了”) or with three or more function words besides 的 (“先去搜索再告诉我答案”; titles and brands with one or two, 平凡的世界, 饿了么, 有赞科技有限公司, are names) — or one of more than 16 Han characters. A name someone gives (entities, alias, rename) skips these rules and needs only a letter ("3M", "C++"). A one-word name is not linked; probing it falls back to keyword search. When these rules change, a writer rebuilds the index on its first open (up to 2,000 facts and 500,000 characters; a larger store says so in stats and is rebuilt with rebuild_entities), reading the facts in the order they were stored as adding them would; declared names, aliases someone gave and their links stay as they are.
  • related: facts structurally tied to an entity in their HRR vector, above a noise floor.
  • Every recall action honours min_trust (default min_trust_threshold).
  • contradict: candidate conflicts, i.e. pairs about the same subject that make a similar but not identical claim ("lives in Paris" / "lives in London"). It suggests; it never edits.

History, sources and forgetting (0.8.0)

A fact that stops being current is not overwritten. The agent says what happened, and the store keeps the history:

action when effect
change {fact_id, content, valid_from?, keywords?} it changed (moved, new job, new price) the old text becomes a past version of the same fact id; the old text's keywords go unless given again (the result names those dropped)
end {fact_id, valid_to?} it no longer holds and nothing replaces it the fact leaves the current store; its history stays
correct {fact_id, content?, valid_from?, keywords?} it was wrong the wrong version is never shown as the past; keywords given replace the fact's, otherwise they stay
revert {fact_id} undo the last change, correction or end the previous version is current again
merge {fact_id, from, keywords?, expect?} two or more facts say the same thing (0.19.0) the copies leave the present into the keep; each keeps its history under its own id
forget {fact_id} the user asks to forget it deleted for good, every version included; only a tombstone without text stays
  • Ordinary recall sees only the present. Past versions live in their own table and index; search and list read them only with as_of=YYYY-MM-DD ("what held on that date"). The answer separates results (known to hold then) from undetermined (may have held: a start or an end that is not known to the day). The store invents no dates: a fact without valid_from is known to hold from the day it was recorded, and so is every fact stored before 0.8.0.
  • history {fact_id} lists the versions with their dates, status and source.
  • Source. Each version records where it was written: via (tool, auto_extract, mirror), platform, chat type, chat and thread id, session, and the author of the turn. Chat names are not stored.
  • Forget means gone. The fact, its versions, links and orphaned entities are deleted, both search indexes are rebuilt, the file is rewritten (VACUUM, with secure_delete on) and the write-ahead log emptied, so no copy of the text stays in free pages. A tombstone (the id and keyed hashes, never the text) stops auto-extraction from bringing the fact back from old messages and tells other tools which ids to clean from their own copies.
  • Merge, not forget, for duplicates (0.19.0). merge {fact_id: <keep>, from: [ids]} closes each copy's current version as merged: the copy leaves search, recall and list; its versions, sources and confirmations stay under its own id (history {keep} shows them under merged_from, history {copy} says merged_into), and its last text answers exists with the keep when added again, also from auto-extraction — while the keep is still at the version it had at the merge (into_version_id); once the keep is changed or ended, the copy's text is its past claim (past_version); after a change, end or correction of the keep auto-extraction still skips it. The keep keeps its own text, trust and origin; keywords are joined (or given), the counts added once. expect: {"<id>": <version_id>} merges only if every named fact is still at that version (version_id is in history), otherwise changed_since and nothing is written, so a merge a person approved merges exactly the texts they saw. A scheduled job merges only facts it stored in the same session. There is no unmerge; forget of the keep forgets its copies too. The first merge raises the history schema (see Database and downgrades).
  • Older prompts keep working: add supersedes=X is change X (the id now stays X), update content= is correct, remove is forget.

Who stands behind a fact (0.15.0)

The source says where a fact was written; it did not say whether the person said it or the agent concluded it. Six months on, a rule the user set and a guess the agent made looked the same.

  • Origin. add, change and correct take origin: said (the person said it), confirmed (the agent's conclusion, and the person agreed) or inferred (the agent's own conclusion). It is kept with the version, shown by history, list, export and search, and counted in stats. Auto-extracted sentences are said (they are the user's own words); facts stored before 0.15.0 and mirrored memory entries have no origin. A correct that names no origin keeps the version's (fixing a typo does not make a fact nobody's); a change is a new claim and takes only the origin it is given. A session opened before an update keeps the tools it was given and offers no origin until a new session starts: its facts land without one. A new fact or a changed version stored with no origin has none (unknown in stats), and the tool description says so; an add of a text already stored keeps that fact's origin and trust. In the database the origin is kept with the version, in fact_versions.src_origin of the current row (status = 'current'), not on facts. Stored times are UTC (fact_versions.recorded_at, facts.created_at / updated_at); for a window in local days use fact_versions.recorded_on, the same moment as a date in Hermes's timezone (UTC when none is set).
  • Starting trust. What the person said or confirmed starts 0.1 above default_trust, the agent's conclusion 0.1 below it (within 0 and 1), a fact without an origin at default_trust as before; a change starts over at its new origin's trust, and so does a new text through correct or update (its origin is the version's unless the call names one), without the old text's helpful marks and recall count; a new date alone keeps them. Measured on 40 pairs of conflicting facts about the same person (one said, one inferred, stored in alternating order): without origins the one said came first in 17; with a step of 0.05, 0.1 or 0.15, in 40 of 40, in search and before a turn alike. An inference with no rival was still recalled every time (10 of 10). 0.1 keeps the order after a helpful vote or two on the inference.
  • Confirmations. fact_feedback {action: confirmed, fact_id} records that the person confirmed a fact — said it again or agreed when asked — and the same text added again with origin: said in a later session counts too (not in the session that stored it: that is the agent repeating itself). At most one confirmation per fact per session, or per day where there is no session. Trust rises by 0.05 for each person who confirms a fact the first time: one group-chat member saying "yes, true" session after session raises it once (without a known id, once per session). An inference that has been confirmed counts as confirmed; a change drops the confirmations, which were of the old claim, and so does a new text through correct or update. A nightly job that finds a person's message backing a fact can confirm it the same way.
  • Recall says what is a guess. Before a turn, an inference nobody confirmed ends with (inferred), so the model does not repeat it as something it was told. review lists inferences nobody confirmed in 60 days.

On a family store the agent stored 22 facts in four months and never the same one twice, so confirmation by repetition alone will be rare there: the explicit confirmed and a nightly job are where confirmations come from.

The full contract, including what the plugin cannot clean (Hermes's own session database, hermes backup archives): docs/history.md.

Nightly dreaming: hermes-dreaming

The store keeps what the agent was told; it does not decide what deserves to become a permanent memory. hermes-dreaming (in the Hermes plugin catalog as hermes-dreaming) does that at night, deterministically and without its own model calls: an optional extraction job stores candidate facts here through fact_store, and the nightly pass reads this store read-only, corroborates each fact with what people actually said in their chats, proposes the few worth writing to MEMORY.md / USER.md, asks whether old entries still hold, and gives facts without keywords their keywords. With 0.8.0 the two work as one: a human's "no longer true", "that was wrong" or "forget it" to a dream question becomes end, correct or forget here, and after a forget the dream removes its own copies of the fact (cache, diary lines, its cron logs) by the tombstone's id. Use hermes-dreaming 2.4.0 or later with this release: cron jobs may write facts only from 0.8.0 on (cron_writes).

Memory map (experimental)

memory_map.py exports one store as a read-only JSON snapshot a person can read: the facts with who said them and how much they are trusted, people and topics, contradictions and which side search leans on, the week's changes, forgotten facts as a number and a date, and what the nightly review asks about this store. The hermes-dashboard "Memory map" tab draws it. Nothing calls it unless you do (python <plugin dir>/memory_map.py --store PATH --json); the provider is unchanged by it.

Experimental: the format and the rules may change. Its output is personal data — keep any page made from it behind a password. What it shows and hides, and its limits (a closed word list for "person", "search leans on" only for a question that fits both facts equally, the night's files outside the database snapshot, a picture that ages until the next rebuild, at most 1000 facts and 300 names by default — --max-facts / --max-entities, 0 for no limit): docs/memory-map.md.

How to see it:

  • In hermes-dashboard (a page behind basic-auth, rebuilt by its cron): turn on its views.memory_map and point memory_map.plugin_dir / memory_map.store at this plugin and the store — step by step in the dashboard's README, "Memory map (experimental)".
  • Your own page or tool: run the exporter and draw the JSON; every key is described in docs/memory-map.md. Draw only what it gives — the privacy rules are applied by the exporter, so a renderer must not open the database itself — and escape every text (they are what people told the agent).
  • A ready-made standalone HTML file and a fact_store action that builds it are not in this release.

Languages

How a word is matched across languages: words, language, function words, keys, match; examples in eighteen languages
that match (German compounds, Russian and English spellings of one word, Traditional and Simplified Chinese, Japanese
spelling variants) and examples that do not

A word matches another when they share its plain form or its stem in the language of the text:

  • Stems: Snowball for English, Russian, Spanish, French, Portuguese, Indonesian, German, Hindi, Italian, Turkish, Polish and Dutch, with two plural rules it lacks (Portuguese ação / ações, Spanish lápiz / lápices); for Arabic, Larkey's light stemmer (the one Lucene ships) next to Snowball's, plus the word without a glued conjunction or preposition before an alef (لإيرما, "for Irma"; ولإيرما, "and for Irma"); for Bengali, a light suffix stripper (Snowball has none) that leaves at least three letters, so মাটি (soil) is not মা (mother). Italian, Turkish, Polish and Dutch words (0.11.0) are stemmed as written and folded: Snowball's Turkish and Polish read the letters folding drops (çocuğun is çocuk, domów is dom), and a word typed without its marks still meets one typed with them. Turkish also gets the noun without the s of a possessive that Snowball leaves (kedisi → kedi); Dutch keeps a ge- that Snowball takes off (gezin, family, is not zin, sense). Polish needs snowballstemmer 3.1 or newer (current Hermes installs 3.1.1); with 2.x Polish words compare as written and the rest of Polish (function words, greetings) works.
  • Which language: the script decides for Cyrillic, Arabic, Devanagari and Bengali. Latin text is English, Spanish, French, Portuguese, Indonesian, German, Italian, Turkish, Polish or Dutch by the function words it uses ("the", "el", "le", "o", "yang", "der", "il", "bir", "się", "het"). Text with none of them is stemmed in English, Spanish, French, Portuguese, Indonesian and German, and in Turkish, Polish or Dutch only when it has their letters (ı ğ ş; ą ę ł ń ś ź ż ć; ij): Polish stems of English words join unrelated ones (army / arm, apply / apple), so a one-word Polish or Italian question gets its stems only with a function word or such a letter. A word is dropped as a function word only when the other words show that language, or when it is the only sign of it in a sentence of four words or more: "son" alone, or in "where does my son study", is a search word; in "il boit son café" it is French "his". With no other evidence the English list applies, as it did to all Latin text before 0.5.0. Function words of the four newer languages that are also everyday words, names or abbreviations elsewhere (Italian come, dove, per; Turkish kim, ben; Polish bo; Dutch door, met, van, wat) do not tell the language and are dropped only where that language alone fits the other words: "Ana come carne" stays Spanish, "Kim Lane lives in Oslo" keeps kim. Italian mia (my) is not on the list: it is a common name.
  • Folding: Latin accents and ligatures (esta finds está, coeur finds cœur), Arabic vowel marks and letter variants, the Indic nukta, and native digits (2027 finds ২০২৭, HR24 finds HR২৪). Folding also merges what only an accent tells apart (Spanish año / ano), as the FTS5 index already did. Turkish ı and Polish ł compare as i and l (İstanbul, ISTANBUL and istanbul meet, KIZIM meets kızım, szkola meets szkoła); since the FTS5 index keeps those two letters, such a word is also searched as written.
  • One word, two scripts (0.14.5): Russian text writes a borrowed word either way, and бэкап finds backup, телеграме finds Telegram, впн finds VPN, визы finds visa, and the other way round — as the same word, without search by meaning. 55 such words are listed (apps and services, devices, server, backup, token, hotel, visa, chat, site, bot…; _LOANWORDS in text.py). A word belongs to its group by its exact form — the spelling, an English plural, a Russian case ending — never by its stem: Snowball cuts токен to ток (electric current) and виза to виз, as in визит, and neither meets token or visa. A Latin spelling joins its group only in English or in text of no known language (0.14.6): French chat is a cat, German über folds to uber, Spanish botes are boats. A spelling whose Russian forms are another word is not listed (убер gives уберу, of убрать), and a short one is searched by its forms as whole words, so бот does not bring in ботинки. Words two languages share by meaning, not spelling (password / пароль), are not listed: that is what keywords are for.
  • Hyphens: French elision and inversion (d'Irma, prend-il, est-ce) and Portuguese enclisis (chama-se) are taken apart in text of that language, reduplication (anak-anak) in any; a hyphen between scripts splits (hr-vps-এর). A letters-only hyphenated token is an identifier in English text or text of no determined language (hr-vps, carry-on): a message naming it is about that one, and a fact about hr-other is not recalled for it. In Spanish, French, Portuguese, Indonesian and German text it is a compound word (week-end, sexta-feira, IT-Treffen), so a host name in a question in those languages is matched like any word, not enforced.
  • Apostrophes: Italian elision as French (dell'anno, l'ospedale, quest'anno, dov'è, anch'io; not before a lone s, so English dev's stays). A Turkish case ending after a name comes off in text that may be Turkish (Ankara'da, Ayşe'nin, HR24'ün, hr-vps'de are Ankara, Ayşe, HR24, hr-vps), so the name is mentioned and an identifier stays required; only Turkish endings, so Irma's, o'clock and rock'n'roll are left alone. In Dutch text the plural and possessive 's comes off (pinda's).
  • German compounds (0.11.0): in text determined as German, a compound is split by a dictionary of German lexemes from Wikidata (CC0): parts of three letters or more joined by German linking elements (-s-, -es-, -n-, -en-, -er-, -e-, a dropped -e), the analysis with the fewest parts, Wikidata's own analysis where it has one (Zahnarzttermin = Zahn + Arzt + Termin, Blumentopferde = Blumen + Topf + Erde); two analyses that cut differently leave the word whole unless one is clearly likelier (Staubecken, Wachstube stay whole). Both directions: the last part of a compound in a fact matches like the word itself (Katze finds Familienkatze), and a compound in a question finds a fact that writes its parts apart (Zahnarzttermin finds Termin beim Zahnarzt). The parts before the last count 0.6 and never recall a fact alone, and so does a last part of three letters (Tag in Alltag); a question compound whose last part alone is in a fact counts 0.6 too. Words the dictionary knows as one word stay whole (Matrose is no Rose, Mitglied no Lied, Ausland no Land); 0.6.0's suffix test stays for words it does not know (Arbeitslaptop). On Wikidata's own compounds held out of the build, 94.9 % are split, 99.9 % of the splits as Wikidata cuts them; of 204 simple words that look splittable, 4 are still split (Plattform, Tablette, Kommode, Zoophobie). Compounds whose meaning is not their parts' are never split (0.12.0: Hochzeit is no Zeit, Frühstück no Stück, Weihnachten no Nacht, Augenblick, Verstand, Reisende, Wochenende, Flugzeug …, nor their forms; Tablette is in that list too), in the table as well since its rebuild (0.17.0): the build keeps those words whole and cuts the other nouns as before, by Wikidata's analysis where it has one and else by the splitter's own judgement with stricter limits (0.12.0 had made every noun without an analysis one word; the table it would have built no longer split 15 of 33 everyday compounds, and was never shipped). The table of 0.17.0 was built without the verb forms and lacked 1,530 verbs that keep their forms whole: it cut 1,515 forms (gehörte as geh|orte, stelltest as stell|test); 0.17.1 is built with them, and verbs, adjectives and verb forms are read as in 0.16.3 again. A real ambiguity stays: Kopfschuppen (dandruff) is Kopf + Schuppen, and Schuppen is also a shed. de_compounds.tsv.gz (212 KB, rebuilt 2026-10-17), loaded on the first German word (0.15 s, 7 MB); _DE_COMPOUNDS = False turns it off.
  • German and Spanish verb forms (0.11.0): Snowball keeps ging apart from gehen, fue from ir and ser, hizo from hacer. A table of such forms from Wikidata's lexemes (the 2,000 German and 4,000 Spanish verbs a stand-in for frequency ranks first) gives a form its lemmas' stems, in text of that language only (not in a tie with English: "he went"). Such a match counts 0.6, like a root, and never recalls a fact alone: a form can belong to two verbs (fue is ir and ser), the source has errors (tuviese under twelve verbs, dropped: a form under more than three is left out; gelegt under liegen), and a name can be spelled like a form (Lucía / lucir). Forms spelled like another word are left out (Macht, vino, cuenta, hecho), and so are forms spelled like a regular form of another verb (fuhren / führen). Separable verbs are not joined (rief … an / anrufen). German text is told by its function words, and the list lacks zum, ins, wohin: a short German sentence without others stays undetermined and gets neither compounds nor verb forms. de_es_forms.tsv.gz (251 KB): German 0.03 s and 2 MB, Spanish 0.05–0.09 s and 11 MB on the first word of the language; _LEMMA_FORMS = False turns it off.
  • Dutch compounds are not taken apart (_NL_COMPOUNDS, off): on 10 questions that ask by the end of a compound and 7 that use that end in its own sense, it found 2 more right facts and recalled 4 more wrong ones (huis in ziekenhuis, weer in brandweer, kast in koelkast).
  • Words of the same root (Russian and English): Snowball removes inflection but keeps derivations apart. Russian pairs come from a dictionary (0.10.0): DerivBase.Ru, its direct derivation links as stem pairs, kept when the pair has the shape of a derivation (ночные / ночь, городской / город, учительница / учитель, дружеский / друг; not полный / пол or задача / дача) — 46,756 pairs, Apache-2.0 (licence), loaded on the first Russian word (about 0.3 s, 12 MB). English pairs come from Open English Wordnet 2025 (0.11.0): its derivation and pertainym links between single words, as stem pairs of the same shape (teacher / teaches, pay / payment, deliver / delivery, decide / decision, angry / anger, allergic / allergy; not Peter / pet, meters / met or classic / class, which 0.9.0's suffix rules joined). A link is kept only when it is the derived word's commonest sense (butter is food before it is one who butts, tower a building before one who tows, letter writing before one who lets) and no stem is another, unrelated word (pearly stems to pear) — 5,428 pairs, CC BY 4.0 on Princeton WordNet 3.1 (licence, WordNet notice), 30 KB, loaded on the first English word (0.04 s, 2.4 MB). Missing from it: dentist / dental, anxiety / anxious, runner / run, rental / rent, medical / medicine (left out by those filters); pronunciation / pronounce is in it but its spelling change is too large for the shape filter. _EN_DERIVATION = "rules" (and _RU_DERIVATION) restores 0.9.0's rules. Russian stems of six letters also match on their first five (покупа / покупк). Only in text whose language is determined (English by its function words; Cyrillic without Ukrainian or Belarusian letters), and only within that language. A root counts 0.6 of the word itself and never recalls a fact on its own: suffix rules cannot tell a derivation from a look-alike (войной / вой, полный / пол), so automatic recall needs a word of the message itself, and roots add to it. In search a root brings in facts the word's forms missed and ranks them below facts with the word. Not joined: alternating consonants and prefixes (друг / дружба, страховка / застрахован), four-letter beginnings (большое is not больница). Duplicate and conflict checks do not use roots.
  • Courtesy (0.9.0): a message whose every clause is greetings, thanks, sign-offs, reactions or "how are you", at least one of them more than padding, recalls nothing ("Hi, good morning!", "Thanks, that's all for now", "спасибо, на этом всё", "Grazie mille, a domani!", "Wielkie dzięki, do jutra!", "Heel erg bedankt, tot morgen!", and any combination of the whole-message greetings of every language). Pieces start and end at word boundaries; padding alone ("fine?", "сегодня?") is a question. Russian question words (какой, который, сколько, почему …) are left out of a query's terms, like English what / which / how, but kept in facts.
  • Names in Latin script are matched as written (and with an English 's): Jean is not mentioned by jeans. Names in other scripts carry their inflections (Маши mentions Маша, لإيرما mentions إيرما).
  • Chinese, Japanese, Korean, Thai (no spaces between words): character n-grams, bigrams for Chinese, Japanese and Korean, trigrams for Thai. Grammar weighs less: Chinese function characters (的 是 了), Japanese particles (は が を に の), Thai function words (ของ ที่ และ); a message that is only a filler (はい, 네, โอเค) recalls nothing. Chinese characters (also in Japanese) count one by one as well; kana, Hangul and Thai letters, which are sounds, do not. Traditional characters compare as Simplified ones (0.9.0): 學校 finds 学校, 醫院 finds 医院, so a fact a Taiwan or Hong Kong user wrote meets a mainland user's question and t

README truncated. Continue reading on GitHub