A Hermes memory plugin that gives agents a structured, queryable fact store backed by SQLite. Built to solve two problems: agents losing coherent working knowledge after context compaction, and the need to convey context across sessions transparently — extending factual recollection without hitting the cold tier and blowing out the context window with large files.
The agent's memory has three tiers:
| Tier | Storage | What lives there | Fed by |
|---|---|---|---|
| Cold | OKF bundles on NFS (/shared/agents/common/) |
Infrastructure facts — hosts, services, clusters, protocols. Curated, versioned, shared between agents. | Manual authoring + PR review |
| Warm | memory_store.db (this plugin) |
Structured facts ingested from OKF + runtime learning. FTS5+Jaccard retrieval with trust scoring. | okf_ingest.py (cold→warm sync), agent's fact_store tool calls |
| Hot | Agent context window | Prefetched facts injected into the system prompt via prefetch(). Compaction recovery re-injects bootstrap infrastructure facts. |
Plugin prefetch() + on_session_switch hook |
The cold tier is canonical. The warm tier is the working copy agents query at
runtime. The hot tier is what the agent actually sees. okf_ingest.py bridges
cold→warm; the plugin bridges warm→hot.
The plugin registers as a MemoryProvider and exposes two tools to the agent:
fact_store— CRUD + retrieval over a structured fact storefact_feedback— rate a fact as helpful/unhelpful, adjusts trust score
Facts are stored in SQLite with:
- Full-text search (FTS5) for keyword retrieval
- Entity extraction and resolution (facts are linked to the entities they mention)
- Trust scores that drift up/down based on feedback
Retrieval is FTS5 + Jaccard similarity. The probe/related/reason/contradict
actions all operate over the same FTS5+Jaccard pipeline (probe/related search
by entity name, reason joins entity names into a keyword query, contradict
uses Jaccard entity overlap + Jaccard content divergence).
On every turn, prefetch() runs hybrid retrieval (FTS5 + Jaccard) against the
user's message and injects relevant facts into the context block.
When Hermes compresses context, the plugin detects the compression
session-switch event and re-injects the top-N high-trust infrastructure
category facts into the start of the next system prompt. This bootstraps the
agent back to knowing where it lives and what it manages — the host it runs on,
the clusters it monitors, the protocols it follows — without manual
intervention.
Facts tagged with source_session: are treated as latent context from other
concurrent sessions. These surface in prefetch() under a separate "Other
Active Conversations" block and are suppressed if the current venue is more
public than the session they came from.
OKF bundles
are directories of YAML-frontmatter markdown files describing infrastructure
concepts — hosts, clusters, services, protocols. The cold tier lives on NFS at
/shared/agents/common/. okf_ingest.py walks a bundle and upserts facts into
the warm store.
python3 src/okf_ingest.py /shared/agents/common/infrastructure/
# dry run
python3 src/okf_ingest.py /shared/agents/common/infrastructure/ --dry-run --verbose
# explicit db path
python3 src/okf_ingest.py /path/to/bundle --db /path/to/memory_store.dbIngestion is idempotent — files are tracked by path+timestamp and skipped if unchanged. Trust is assigned from OKF v0.2 frontmatter signals:
| Trust | Source |
|---|---|
| 0.9 | verified.by: human:... or bootstrap: true |
| 0.7 | verified.by: agent:... |
| 0.5 | No verified block |
Deprecated files (status: deprecated) are purged from the store on next
ingest. Stale files (stale_after in the past) are skipped and purged if
previously ingested. Orphaned state rows (fact deleted but okf_ingestion_state
row survived) are detected and repaired by re-ingesting instead of skipping
(romar#19).
Facts published by one agent are automatically ingested by the other via a
write-side/poll-side pipeline over the NATS JetStream agent-memory stream
(subject agents.memory.shared.>):
agent A NATS JetStream agent B
nats-publish ──→ agent-memory stream ──→ nats-listener (memory_poll_loop)
│
├─ dedup (content hash)
├─ normalize
└─ append to memory_spool.jsonl
│
pre_llm_call hook (handler.py)
│
└─ upsert into memory_store.db
The listener polls the stream every 10 seconds alongside the coordination poll
loop. Each fact is deduplicated by content hash before write, and the spool
file auto-trims oldest 25% when it exceeds 10MB. The pre_llm_call hook reads
the spool and upserts into the warm store before every turn.
Trust scores work the same as OKF ingestion — 0.7 for infrastructure facts,
0.9 for bootstrap — and land below the default min_trust_threshold (0.3) so
they're eligible for retrieval immediately. The hot tier picks them up on the
next prefetch().
This is the runtime path for shared knowledge. OKF bundles handle curated, versioned facts; the broadcast pipeline handles facts discovered at runtime and pushed peer-to-peer.
The pre_llm_call hook that drains the spool into the store is a shell
hook, and Hermes skips un-allowlisted shell hooks silently. If
hooks_auto_accept is false (the default) and the hook command is not
approve-allowlisted, the gateway logs a WARNING ... not allowlisted — skipped
each turn but the fact store freezes with no error: no new facts land, and
nothing is written or read. This is exactly what happened to the Clomp host
(romar#186): the store went ~7 days without a write, and the symptom surfaced
only as "no fact_store recall" — not as a crash.
Before relying on the broadcast pipeline, verify the hook is actually running:
grep -c 'not allowlisted' ~/.hermes/logs/gateway.log # any count > 0 means it's skipping
# and confirm the spool is being drained:
wc -l ~/.hermes/memory_spool.jsonl # 0 lines = nothing flowingFix is one of: hooks_auto_accept: true, an explicit allowlist entry for
python3 ~/.hermes/hooks/ingest-memory-spool/handler.py, or approving the hook
at the next TTY prompt. The spool-health check in scripts/fact-store-inventory.py
flags a spool with old lines for the same reason.
Drop the src/plugins/memory/hermes_memory/ directory into your Hermes plugins
path and register it in config.yaml by its module name:
plugins:
hermes-memory-store:
db_path: $HERMES_HOME/memory_store.db
auto_extract: false
default_trust: 0.5
min_trust_threshold: 0.3
bootstrap_inject_limit: 15
bootstrap_min_trust: 0.7
bootstrap_shadow: false
okf_bundle_path: /shared/agents/common/infrastructure/The plugin key hermes-memory-store matches the module's registration name
(__init__.py → register() → provider name hermes-memory). The directory
name hermes_memory is the Python module path.
| Action | Purpose |
|---|---|
add |
Store a fact. Requires content. Optional category, tags. |
search |
Keyword lookup across content and tags. |
probe |
All facts about a specific entity (keyword search on the entity name). |
related |
Facts connected to an entity (keyword search on the entity name). |
reason |
Facts matching a list of entities — joins entity names into a keyword query. |
contradict |
Find fact pairs making conflicting claims about the same entities (Jaccard entity overlap + content divergence). |
update |
Modify content, tags, category, or adjust trust by delta. |
remove |
Delete a fact by ID. |
list |
Browse facts by category/trust, sorted by trust descending. |
Categories: user_pref, project, tool, general, infrastructure.
# What do we know about the rune host?
fact_store(action="probe", entity="rune")
# Who works on backend and what do they do?
fact_store(action="reason", entities=["peppi", "backend"])
# Find anything about deploy processes
fact_store(action="search", query="deploy process")
| Key | Default | Description |
|---|---|---|
db_path |
$HERMES_HOME/memory_store.db |
SQLite path. Supports $HERMES_HOME and ~ expansion. |
auto_extract |
false |
Auto-extract facts from conversation at session end. |
default_trust |
0.5 |
Starting trust score for new facts. |
min_trust_threshold |
0.3 |
Prefetch ignores facts below this score. |
okf_bundle_path |
/shared/agents/common/infrastructure/ |
Default bundle for OKF ingestion. |
bootstrap_inject_limit |
15 |
Max facts re-injected after compaction. |
bootstrap_min_trust |
0.7 |
Minimum trust for compaction re-injection. |
bootstrap_shadow |
false |
Log what would inject without injecting. |
src/
okf_ingest.py # OKF parser + ingestor (cold→warm)
plugins/memory/
__init__.py # Plugin registry
hermes_memory/
__init__.py # MemoryProvider plugin + fact_store tool
store.py # SQLite schema, CRUD, entity resolution
retrieval.py # FTS5 + Jaccard retrieval
tests/
test_okf_ingest.py # 30 tests (parsing, trust, deprecation,
# staleness, orphan repair, CLI)
docs/
problem.md # Original design brief
rune-review-2026-06-22.md # Architecture review
- romar#19 — orphaned OKF state rows permanently blocked re-ingestion (fixed)
- romar#1 — architecture paper audit that surfaced the dual-delivery-path design (listener + plugin)
- Agent Inbox Pattern postmortem
— the
nats-listener.pydeliver→inbox path this plugin complements