Backfill
Switch memory providers without Hermes forgetting you.
When you change memory providers in Hermes (built-in to Hindsight, Mnemosyne to Honcho, a cloud service to a local one), the new provider starts empty. Months of conversations are still in your session history, but the new memory has never seen them.
Backfill replays your past Hermes sessions into the memory provider you use now, so it learns from your history instead of starting from zero.
hermes backfill --dry-run # see what would be imported; changes nothing
hermes backfill # import
hermes backfill check # score recall before and after
Status: 0.1.0, tested with Holographic. In end-to-end tests on Hermes v0.23.1 (the current release, installed with
hermes plugins installfrom git) and onmain(November 2026), recall went from 0/12 to 11/12 after importing 6 past sessions in English, Russian and Chinese. In a live Hermes chat on a local model, the agent first answered "I don't know". After Backfill, a new chat answered "You use Neovim as your editor, and your favorite language is Rust" from imported memory. With Holographic's search by meaning on, questions worded differently from the original messages went from 0/12 to 11/12. Tested on 600 conversations, compressed sessions and branches. Also tested with Cognition (0/12 → 8/12), Mnemosyne (11/12 with its global scope) and Hindsight (partial import, recalled in live chat); cloud providers are not tested yet. See Tested and Known limits.
The problem
People switch memory providers often. The question that keeps coming up on r/hermesagent is some form of "has anyone migrated away from [provider] without losing useful history?" Today the answers are:
- start over with an empty memory, or
- ask the agent by hand to "populate anything important from session history", one prompt at a time.
Hermes already moves your settings when a provider changes from a built-in to a catalog plugin. It does not move what your old memory knew into a different provider. A few providers ship a one-way importer for one specific source (Mnemosyne has import-hindsight, for example). Backfill works from the one record every setup has, Hermes's own session history, into whichever provider you use now.
What it does
- Reads your past sessions from Hermes's own session database, read-only.
- Loads the memory provider that is active in your
config.yaml. - Replays each past conversation into it, turn by turn, through the same path a live chat uses.
- Remembers which sessions it has imported, so running it again never imports a session twice.
- Optionally measures recall before and after, so you can see the difference as a number.
It works with any memory provider that follows the standard Hermes MemoryProvider interface: Hindsight, Honcho, Mem0, Mnemosyne, Holographic, Supermemory and the rest of the catalog. It needs no provider-specific code.
How it works
Backfill doesn't use a special import format. It plays your history back to the provider as if those conversations were happening again.
state.db (read-only) your active memory provider
│ ▲
│ session 1: [user, assistant, user, ...] │
▼ │
pair turns ──► MemoryManager.sync_all(user, assistant) (once per turn)
──► MemoryManager.on_session_end(messages) (once per session)
──► record session id as done
Step by step:
| Step | What Backfill calls | Notes |
|---|---|---|
| Read history | hermes_state.SessionDB(read_only=True), list_sessions_rich(), get_compression_chain(), get_messages_as_conversation() | Oldest conversation first. A conversation Hermes compressed into several sessions is read once, through Hermes's own de-duplicated lineage view (include_ancestors=True), because each continuation repeats the end of the one before. Compaction summaries are skipped. The database is never written. |
| Load provider | Provider named in memory.provider, loaded by Hermes's own loader | The same provider instance type your agent uses. |
| Start it | MemoryManager.add_provider() then initialize_all(..., agent_context="primary") | Providers ignore writes from non-primary contexts, so this matters. |
| Start each session | MemoryManager.on_session_switch(session_id, reset=True) | Providers file what they store under the original session, with fresh per-session buffers. |
| Replay turns | MemoryManager.sync_all(user, assistant, session_id=...) | Hermes's built-in secret redaction runs here, exactly as in a live chat. |
| Close session | MemoryManager.on_session_end(messages) | Lets providers that extract facts at session end do so. |
| Finish | flush_pending(), then shutdown_all() | Waits for queued writes before exiting. |
The provider decides what to keep. Backfill feeds it the conversations and the provider's own logic stores, extracts or summarizes them, the same way it would have if it had been active at the time.
Commands
| Command | What it does |
|---|---|
| hermes backfill --dry-run | Counts sessions and turns that would be imported, names the target provider, and warns if the provider makes LLM calls. Changes nothing. |
| hermes backfill | Runs the import. Asks for confirmation first, and refuses to start while the Hermes gateway is running. |
| hermes backfill status | Shows the target provider, sessions imported, sessions remaining and the last run. |
| hermes backfill check | Runs the recall check (below). |
| /backfill | In chat, shows the same status. The import itself runs from the terminal only, because it can take a long time. |
Options for hermes backfill:
| Option | Default | Meaning |
|---|---|---|
| --since YYYY-MM-DD | all history | Only sessions started on or after this date. |
| --until YYYY-MM-DD | today | Only sessions started before this date. Use the day you switched providers to avoid importing sessions the provider already saw. |
| --limit N | no limit | Import at most N sessions this run. Run again to continue. |
| --yes | off | Skip the confirmation prompt (required when not running in a terminal). |
| --include-source SOURCE | none | Also import sessions from a source that is skipped by default, such as cron. Repeatable. |
Cron runs and subagent sessions are skipped by default. Only your own conversations are imported.
Options for hermes backfill check:
| Option | Default | Meaning |
|---|---|---|
| --questions FILE | none | Use your own questions: a JSON list of {"question": ..., "answer": ...}. No model calls. |
| --count N | 20 | How many questions to write from your history when no file is given. |
| --new | off | Write fresh questions instead of reusing the saved ones. |
| --label NAME | before or after | Name this run in the saved results. |
| --verbose | off | For each miss, show what the provider returned. |
Recall check
hermes backfill check measures whether the import helped:
- It picks a sample of your past sessions (20 by default) and asks your Hermes model to write one question per session, five sessions per call. The question is worded the way you might ask it months later, not with your original words, so the check measures real recall rather than word matching. The answer must be words you actually wrote, or the question is dropped.
- It asks the memory provider to recall each question and checks whether the answer comes back.
- It saves the questions and the score. The next run reuses the same questions, so running it once before
hermes backfilland once after gives a fair before/after result.
Writing questions uses your Hermes model through Backfill's own model slot, auxiliary.backfill_check. It uses the same model as your agent, with reasoning turned off by default: writing a question needs no reasoning, and with reasoning off a local 4B model took 18 to 27 seconds for six questions instead of 4 to 14 minutes. Change the slot like any auxiliary task in hermes model or config.yaml. To skip the model entirely, pass your own questions with --questions FILE. The check reads memory only and never writes to it.
Safety
- Read-only history.
state.dbis opened read-only and never changed. - Dry run first.
--dry-runshows exactly what will happen before anything is sent. - Secrets are redacted. Turns pass through Hermes's own provider redaction, the same as live chat.
- No double imports. Imported session IDs are recorded in Backfill's own plugin state. Re-running skips them.
- Each exchange is sent once. Backfill keeps a short fingerprint (a 16-character hash, not the text) of every turn it sends. A turn seen again, such as the copied history at the start of a
/branchor a turn already imported in an earlier run, is skipped. - Resumable. If a run stops partway through, the next run continues where it left off.
- A session counts as imported only when the provider confirms it. Backfill waits for Hermes's write queue and, for providers that keep their own queue (Hindsight), for that queue too. It also watches the provider's warnings while it imports. If the provider reports a failure while handling a session, Backfill stops before marking it. If the provider drops queued writes while shutting down ("abandoning 12 pending retain(s)"), Backfill un-marks that run, so the next run sends those sessions again. A provider that accepts writes it cannot store therefore never leaves history silently missing.
- Live sessions are not sent twice. Backfill records when its first import into a provider started. Sessions that began after that ran with the provider active, so the provider already saw them, and later runs skip them.
- Stop the gateway first. If the Hermes gateway is running, it may be writing to the same memory store, so Backfill refuses to start and tells you to stop it. Two writers never clash. A plain
hermes chatalso starts a gateway in the background, which exits after about 10 idle minutes;hermes gateway stopends it at once. - Provider hints. The dry run warns about settings that would make an import do nothing, recall poorly or cost money. For Holographic it checks
auto_extract(off means nothing is learned) and search by meaning (off means imported facts are found only by matching words), and prints the exact commands.
Cost
Backfill itself makes no LLM calls during import. Some memory providers do:
| Provider type | Examples | What an import costs |
|---|---|---|
| Local store, no extraction model | Holographic (with auto_extract: true) | Nothing beyond disk space. |
| Provider extracts facts with an LLM | Mem0, Hindsight, Honcho (depending on setup) | One or more provider LLM calls per turn, billed by the provider or your model account. |
The dry run tells you how many turns will be sent, so you can try --limit on a few sessions first and check your provider's usage before importing everything.
Known limits
- Dates. The Hermes memory interface does not carry a timestamp per turn, so imported memories are stored as of the import date, not the original conversation date. Providers that reason about time (for example "what did I say last month") may misplace them.
- Duplicates with the same provider. If the active provider was already running during some of your past sessions, importing those sessions again creates duplicates. Use
--untilwith the date you switched. - Built-in memory files are not changed.
MEMORY.mdandUSER.mdstay as they are. Backfill only writes to the external provider. - Provider behaviour varies. Each provider decides what to keep from a replayed conversation. Some keep everything, some extract only facts, and some may treat old turns differently than live ones.
- Recall depends on the provider's search. By default Holographic matches words: "which code editor do I use?" does not find "I use Neovim for all my coding". In Backfill's test, questions worded differently from the original messages scored 0/12 by words alone and 11/12 with Holographic's search by meaning on (the miss is the skipped cron session). Turn it on before importing, so each fact gets its vector as it is stored; the dry run prints the commands. Two practical points from testing with a local embedding server: keep the model loaded (
keep_alive -1in Ollama), because Holographic waits at most a few seconds per call and a cold model times out; and on a store with fewer than 20 facts, setembedding_min_score(0.4 worked), because below that size Holographic offers a fact by meaning only above that score.hermes backfill check --verboseshows what came back for each miss. - One question about several things is harder. With a 4B model, "which editor, which database and what wake-up time?" in one message was answered only partly, even with search by meaning on. The same facts asked one at a time were answered correctly every time. Larger models do better here.
- Two private Hermes imports. Loading a provider by name uses
plugins.memory.load_memory_provider, and skipping context-compaction summaries usesagent.context_compressor.is_compaction_summary_message. Neither is on the public plugin API list, sohermes plugins validatereports a warning for each, not a failure. Everything else uses public modules (hermes_state,agent.memory_manager,hermes_cli.config,gateway.status). - Providers that do not learn from conversations gain little. Some providers store only notes the agent writes on purpose. Birkin Mnemosyne, for example, keeps such notes plus an opt-in handoff of each session's last exchange, so a replay cannot teach it your history. The dry run says so for known cases.
- Provider scope settings decide what a new chat can see. Mnemosyne, by default, recalls a memory only inside the conversation it came from, so imported history (like any memory from another chat) does not reach a new chat until you set
memory.mnemosyne.default_scope global. Mnemosyne also consolidates working memories only once they are about 3.5 days old, and imported memories carry the import date (see Dates). - The provider decides what it extracts. Cognition, for example, recognises English phrasing only, so the Russian and Chinese facts in the test were not stored.
- LLM-backed providers are slow on small local models. Hindsight runs several model calls per stored turn. With a 4B local model one session took several minutes, so a large import needs a hosted model or patience; Backfill resumes where it stopped. Hindsight's local mode also refuses a home folder other users can read (its settings file must be owner-only).
- Not yet tested: cloud providers (Mem0, Honcho, Supermemory), a full Hindsight import, and running on macOS or Windows. The code passed Hermes's own Windows check (
scripts/check-windows-footguns.py), and Hermes sets the console to UTF-8 on every platform.
Tested
tests/run_e2e.sh runs against a real Hermes install in a throwaway HERMES_HOME with Holographic (auto_extract: true):
| Check | Result |
|---|---|
| hermes plugins validate | passed, security scan safe, 2 private-import warnings |
| Recall before import | 0/12 |
| Dry run | 6 sessions, 12 turns, 1 cron session skipped |
| --limit 2, then a second run | 2 sessions, then the remaining 4 (resume works) |
| Third run | "Nothing to import" (no double imports) |
| Recall after import | 11/12 (the miss is the skipped cron session) |
| state.db checksum before and after | identical (read-only) |
The test history mixes English, Russian and Chinese, and every language was recalled. The hand-written questions reuse words from the original messages, which suits Holographic's keyword search, so 11/12 is a best case.
tests/run_chat_test.sh runs a live chat against a free local model (qwen3.5:4b on Ollama, thinking off):
| Check | Result |
|---|---|
| New chat before import | "I don't know your code editor choice, Atlas database selection, or usual wake-up time." |
| New chat after import | "You use Neovim as your editor, and your favorite language is Rust." The agent called Holographic's fact_store tool and got back the imported facts. |
| Import while a chat's gateway is running | refused with a clear message; works after hermes gateway stop |
| A chat after the import, then a dry run | that chat is skipped as already seen live |
| /backfill inside a chat | shows the import status |
| Repeat on the final code (2026-11-11), 5 fresh-chat answers after import | 4 named both Neovim and Rust; 1 named only Rust. The import is the same every time; the small 4B model is not. |
| check --new (questions written by the local model) | 6 questions written in 18 to 27 s; recall 2/6 to 4/5 by words alone, 3/3 with search by meaning |
Search by meaning (tests/questions_paraphrased.json: 12 questions that share no keyword with the stored facts, some in English about Russian and Chinese facts), with qwen3-embedding:0.6b on Ollama:
| Check | Words only | Search by meaning on |
|---|---|---|
| Original wording (questions.json) | 11/12 | 11/12 |
| Different wording (questions_paraphrased.json) | 0/12 | 11/12 |
| Facts with a vector after import | none | 11/11 |
| Live chat, "Which code editor do I use?" (2 runs each) | "Vim.", "Visual Studio Code." (guesses) | "Neovim.", "Neovim." |
Other providers, with the same 7 seeded sessions and no provider-specific code in Backfill:
| Provider | How it learns | Recall before → after | Live chat after import |
|---|---|---|---|
| Holographic | extracts facts from each turn | 0/12 → 11/12 | "Neovim … Rust" |
| Cognition | extracts facts only at session end (tests the session-end replay) | 0/12 → 8/12 (all 8 English facts; it does not extract Russian or Chinese) | "You use Neovim and your favorite language is Rust.", "PostgreSQL." |
| Mnemosyne (mnemosyne-oss, the most-discussed provider on r/hermesagent) | stores every turn; recalls within the conversation it came from unless default_scope: global | default scope: 0/12 → 0/12 (all 12 turns stored, but kept inside their original sessions). With memory.mnemosyne.default_scope global: 0/12 → 11/12. Reworded questions: 0/12 (its recall needs shared words). The dry run now explains the scope setting. | partial: "Neovim (or maybe Vim)", then a guess for the language; with global scope, the chat's own questions are stored too and crowd out the facts |
| Hindsight (local embedded mode, extraction by the same 4B local model) | extracts facts with an LLM on its own server, through its own write queue | 0/12 → 3/12 from a partial import, and 3/12 on reworded questions too (it searches by meaning). The import was not finished: on the 4B local model each session took several minutes of extraction and consolidation. | "Neovim and Rust." |
| Birkin Mnemosyne | notes the agent writes; no learning from turns | 0/12 → 0/12, as designed; the dry run now explains why | not run |
Backfill tells the provider when each past session starts (on_session_switch), so stored facts keep their original session: Cognition filed them as cog-past-001, cog-past-002, … instead of under the import run.
tests/run_lineage_test.sh covers conversations Hermes compressed into several sessions, an explicit /branch and tool calls, built in the same storage shapes as Hermes's own tests:
| Check | Result |
|---|---|
| A conversation compressed twice, plus a branch: 8 stored user turns, 6 distinct | 6 turns sent; the compressed conversation counts as one, the copied tail and summaries are skipped |
| The branch imported in a later run | its copied turn is still skipped |
| Turns with tool calls | the user's words and the assistant's final answer are sent |
tests/seed_stress.py builds 600 conversations (720 sessions, every third turn a tool call, one in ten compressed into three sessions):
| Check | Result |
|---|---|
| Dry run | 1.7 s, 121 MB peak |
| Import, 4,051 turns into Holographic | 20.6 s, 166 MB peak |
| Facts stored | 600 of 600, no duplicates |
| Second run | "Nothing to import" |
| Backfill's own state | 112 KB |
Disclosure (catalog rule 13)
- Reads: this profile's
state.db(read-only) andconfig.yaml(to find the active memory provider). - Sends: your past conversation turns go to your active memory provider, the same data it receives during live chat. If that provider is a cloud service, the turns leave your machine. The dry run and the confirmation prompt name the provider before anything is sent.
- Writes: only its own plugin state: which sessions are imported, a 16-character hash of each sent turn (never its text), when the first import started, and recall-check questions and scores.
- LLM use:
hermes backfill checkwrites test questions with your Hermes model through Backfill's own auxiliary slotauxiliary.backfill_check(same model as your agent unless you change it; reasoning off by default). The import itself makes no LLM calls of its own. - Network, credentials, telemetry: Backfill makes no network calls of its own, reads no credentials and sends no telemetry. Any network traffic comes from the memory provider you chose.
- Dependencies: Python standard library only.
Install
hermes plugins install https://github.com/wth-amna/hermes-backfill
hermes plugins enable backfill
License
MIT. See LICENSE.
README from the reviewed commit b99250c; it updates when the author re-pins.

wth-amna