itpartypattaya/hermes-plugin-holographic
Holographic memory provider for Hermes Agent: local SQLite fact store, HRR retrieval, 18 languages. Maintained by itpartypattaya.
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
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:
hermes plugins install holographic(add--forceif 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.- In
config.yaml: removeholographicfromplugins.disabled, and setmemory.provider: holographic. - 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Маша,deploysfindsdeploy,kucingnyafindskucing; 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_turnsturns. Facts are framed as data (#id [trust] text), one line each.prefetch_max_tokenscaps 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) andvalid_from(the date it started to hold, when the message says). A name inentitiesthat 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 underentities_not_linkedby its position in the list (index, from 0) with the reason, not the name itself (a refused name may beKEY=secret); the fact is stored all the same.entitiesgiven 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 listssimilarfacts,possible_conflicts(same subject, a different claim) andpast_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…_KEYor…_TOKENand 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, andthe token counter.py moduleandToken file: tokens.jsontoo; 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 sotoken-service:1.4). The cost, chosen:password-hunter2.txtis 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,listandhistorystill 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_errorwhile 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 withupdate). - entities lists names with their aliases, fact counts and standing (0.16.0):
establishedwhen someone named it (entities=[...],alias,rename) or two or more facts name it,provisionalwhen the rules below guessed it from one fact and nobody confirmed it;statuslists 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;entitystays 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 underentity_candidates. Spelling variants are one entity (Hermes Agent=hermes-agent), andX aka YmakesYan 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 Yfind 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 undersplit, with thealiascall that makes them one again;alias,renameanddelete_entityby it are refused, and so isrenameof 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 laterentities=["Bob Brown"]file the second person's facts under her, andrenameordelete_entityby "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 underfacts_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 saysfrom_droppedwhen X went with its last fact.entitiesshows such names astext_aliases,reviewlists them underalias_candidateshowever old (up to itslimit),statsgives 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 underaliases, not refused, andsplitseparates it. A name declared with a text already stored (anaddof the same text, acorrectof the date only, achangeinto 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直连可用" givesDocker Hub), short quoted terms ("Phoenix",«Сбер Бизнес»,“华为”,「华为」,《平凡的世界》, straight quotes beside Chinese characters too) and both sides ofX 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 instatsand 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(defaultmin_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;
searchandlistread them only withas_of=YYYY-MM-DD("what held on that date"). The answer separatesresults(known to hold then) fromundetermined(may have held: a start or an end that is not known to the day). The store invents no dates: a fact withoutvalid_fromis 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, withsecure_deleteon) 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 asmerged: the copy leaves search, recall andlist; its versions, sources and confirmations stay under its own id (history {keep}shows them undermerged_from,history {copy}saysmerged_into), and its last text answersexistswith 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_idis inhistory), otherwisechanged_sinceand 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;forgetof the keep forgets its copies too. The first merge raises the history schema (see Database and downgrades). - Older prompts keep working:
add supersedes=Xischange X(the id now staysX),update content=iscorrect,removeisforget.
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,changeandcorrecttakeorigin:said(the person said it),confirmed(the agent's conclusion, and the person agreed) orinferred(the agent's own conclusion). It is kept with the version, shown byhistory,list,exportandsearch, and counted instats. Auto-extracted sentences aresaid(they are the user's own words); facts stored before 0.15.0 and mirrored memory entries have no origin. Acorrectthat names no origin keeps the version's (fixing a typo does not make a fact nobody's); achangeis 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 nooriginuntil a new session starts: its facts land without one. A new fact or a changed version stored with no origin has none (unknowninstats), and the tool description says so; anaddof a text already stored keeps that fact's origin and trust. In the database the origin is kept with the version, infact_versions.src_originof the current row (status = 'current'), not onfacts. Stored times are UTC (fact_versions.recorded_at,facts.created_at/updated_at); for a window in local days usefact_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 atdefault_trustas before; achangestarts over at its new origin's trust, and so does a new text throughcorrectorupdate(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, insearchand 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 withorigin: saidin 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 asconfirmed; achangedrops the confirmations, which were of the old claim, and so does a new text throughcorrectorupdate. 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.reviewlists 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_mapand pointmemory_map.plugin_dir/memory_map.storeat 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_storeaction that builds it are not in this release.
Languages
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, Spanishlá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ğunisçocuk,domówisdom), and a word typed without its marks still meets one typed with them. Turkish also gets the noun without thesof a possessive that Snowball leaves (kedisi→kedi); Dutch keeps age-that Snowball takes off (gezin, family, is notzin, 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 (Italiancome,dove,per; Turkishkim,ben; Polishbo; Dutchdoor,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" keepskim. Italianmia(my) is not on the list: it is a common name. - Folding: Latin accents and ligatures (
estafindsestá,coeurfindscœur), Arabic vowel marks and letter variants, the Indic nukta, and native digits (2027finds২০২৭,HR24findsHR২৪). Folding also merges what only an accent tells apart (Spanishaño/ano), as the FTS5 index already did. Turkishıand Polishłcompare asiandl(İstanbul,ISTANBULandistanbulmeet,KIZIMmeetskızım,szkolameetsszkoł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
бэкапfindsbackup,телеграмеfindsTelegram,впнfindsVPN,визыfindsvisa, 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…;_LOANWORDSintext.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 meetstokenorvisa. A Latin spelling joins its group only in English or in text of no known language (0.14.6): Frenchchatis a cat, Germanüberfolds touber, Spanishbotesare 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 abouthr-otheris 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 lones, so Englishdev'sstays). A Turkish case ending after a name comes off in text that may be Turkish (Ankara'da,Ayşe'nin,HR24'ün,hr-vps'deare Ankara, Ayşe, HR24, hr-vps), so the name is mentioned and an identifier stays required; only Turkish endings, soIrma's,o'clockandrock'n'rollare left alone. In Dutch text the plural and possessive'scomes 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,Wachstubestay whole). Both directions: the last part of a compound in a fact matches like the word itself (KatzefindsFamilienkatze), and a compound in a question finds a fact that writes its parts apart (ZahnarztterminfindsTermin 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 (TaginAlltag); a question compound whose last part alone is in a fact counts 0.6 too. Words the dictionary knows as one word stay whole (Matroseis noRose,MitgliednoLied,AuslandnoLand); 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:Hochzeitis noZeit,FrühstücknoStück,WeihnachtennoNacht,Augenblick,Verstand,Reisende,Wochenende,Flugzeug…, nor their forms;Tabletteis 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örteasgeh|orte,stelltestasstell|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) isKopf+Schuppen, andSchuppenis 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 = Falseturns it off. - German and Spanish verb forms (0.11.0): Snowball keeps
gingapart fromgehen,fuefromirandser,hizofromhacer. 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 (fueisirandser), the source has errors (tuvieseunder twelve verbs, dropped: a form under more than three is left out;gelegtunderliegen), 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 lackszum,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 = Falseturns 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 (huisinziekenhuis,weerinbrandweer,kastinkoelkast). - 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; notPeter/pet,meters/metorclassic/class, which 0.9.0's suffix rules joined). A link is kept only when it is the derived word's commonest sense (butteris food before it is one who butts,towera building before one who tows,letterwriting before one who lets) and no stem is another, unrelated word (pearlystems topear) — 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/pronounceis 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. Insearcha 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 Englishwhat/which/how, but kept in facts. - Names in Latin script are matched as written (and with an English
's):Jeanis not mentioned byjeans. 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