Remnic MemoryProvider for Hermes Agent. Provides automatic memory recall on every LLM turn and automatic observation of every response via the Hermes MemoryProvider protocol. The deepest available integration — memory is structural, not optional.
Hermes integration follows the same boundary as every other host: Remnic core owns memory semantics, while the Hermes package stays a thin adapter over Hermes' real plugin and MemoryProvider contracts.
Canonical upstream references:
- Hermes repository: https://github.com/NousResearch/hermes-agent
- Hermes docs/site: https://hermes-agent.nousresearch.com
- Which Hermes plugin slot Remnic uses
- Why MemoryProvider
- Prerequisites
- Installation
- Configuration reference
- Environment variable overrides
- Token bootstrap
- How the provider works
- Lifecycle parity audit
- Tools registered
- Profile and session isolation
- Error handling philosophy
- Engram compat window
- Troubleshooting
- Migration notes (Engram era)
- Uninstall
Remnic registers as a Hermes memory provider (plugin.yaml declares kind: exclusive, the Hermes manifest kind used for provider plugins selected through memory.provider). This is the only slot Remnic occupies and the only slot it needs.
The manifest declares Hermes-supported capability metadata with provides_tools and provides_hooks. It does not declare or register a context engine.
Remnic does not, and should not, register as a Hermes context_engine. Hermes' context_engine slot replaces the built-in ContextCompressor — it controls how Hermes compresses its own outgoing conversation history before sending to the LLM. That is a different concern from external memory recall. Remnic delivers all of the following through the memory_provider hook chain (pre_llm_call → recall envelope), with no context_engine registration involved:
- Recalled memories from the Remnic store
- Lossless Context Management (LCM) compressed-history sections, when the Remnic daemon has
lcmEnabled: true - Entity context, identity anchors, continuity loops, and any other recall-side enrichment served by the daemon
If a static analysis tool, AI reviewer, or third-party guide tells you "Remnic needs register_context_engine in Hermes to enable LCM," that guidance is incorrect. LCM lives on the Remnic daemon. It is delivered to Hermes via the recall response. The memory_provider hook is the correct and sufficient integration point.
A Remnic-backed ContextEngine (one that uses Remnic's LCM to compress Hermes' local history) is a possible future additive feature. It is not required for any of the capabilities Remnic exposes today.
MCP gives Hermes tools it can call, but the agent must decide to call them. The MemoryProvider protocol hooks into Hermes at the framework level so memory operations happen regardless of what the agent chooses to do.
| Aspect | MCP Only | MemoryProvider |
|---|---|---|
| Recall | Agent must call remnic_recall |
Automatic on every turn |
| Observe | Agent must call remnic_store |
Automatic after every response |
| Latency | Tool call overhead per turn | Pre-fetched, non-blocking |
| Reliability | Agent may omit the call | Structural — cannot be skipped |
The plugin also registers the remnic_* tools for cases where the agent should control recall or storage explicitly — for example, pinning a specific fact mid-session. The two approaches are complementary.
- Remnic daemon running on
127.0.0.1:4318(configurable). See the Remnic repository for installation. - Hermes Agent v0.7.0 or later — the MemoryProvider protocol was introduced in v0.7.0.
- Python 3.10 or later.
pip install --upgrade remnic-hermes
remnic connectors install hermesremnic connectors install hermes generates an auth token, writes ~/.remnic/tokens.json, adds the remnic: block to your Hermes config.yaml, and runs a daemon health check. It does not start the daemon — if unreachable, it prints remnic daemon start as the next step. Restart Hermes after running it.
pip install --upgrade remnic-hermesThen add the config block manually — see Configuration reference.
cd packages/plugin-hermes
pip install -e ".[dev]"The plugin entry point is register(ctx) in remnic_hermes/__init__.py. It reads configuration from ctx.config["remnic"], falling back to ctx.config["engram"] if the remnic key is absent. The extracted dict is passed directly to RemnicMemoryProvider.
In Hermes config.yaml, the config block sits at the top level under a remnic: key (or engram: for legacy configs), alongside the plugins: list:
plugins:
- remnic_hermes
remnic:
host: "127.0.0.1"
port: 4318
token: ""
session_key: ""
timeout: 30.0| Field | Type | Default | Description |
|---|---|---|---|
host |
string | "127.0.0.1" |
Hostname or IP of the Remnic daemon. Overridden by REMNIC_HOST env var. |
port |
integer | 4318 |
TCP port of the Remnic daemon. Overridden by REMNIC_PORT env var. |
token |
string | "" |
Auth token for the daemon. If empty, auto-loaded from the token store (see Token bootstrap). |
session_key |
string | "" |
Session identifier passed on every recall/observe call. If empty, auto-generated as hermes-<12 random hex chars> at startup. |
timeout |
float | 30.0 |
HTTP request timeout in seconds applied to all daemon calls. |
No other fields are read. Fields documented elsewhere (such as namespace, recall_top_k, recall_mode, or token_env) do not exist in this implementation.
Environment variables are consulted only when the corresponding field is absent from the config block. Inline config values win.
| Variable | Overrides | Notes |
|---|---|---|
REMNIC_HOST |
remnic.host |
Primary |
REMNIC_PORT |
remnic.port |
Primary |
ENGRAM_HOST |
remnic.host |
Legacy fallback; checked when REMNIC_HOST is unset |
ENGRAM_PORT |
remnic.port |
Legacy fallback; checked when REMNIC_PORT is unset |
Precedence (highest to lowest): inline config field → REMNIC_* env var → ENGRAM_* env var → compiled default.
The auth token is not read from an environment variable. It comes from the inline token: field or the token store file.
remnic connectors install hermes handles the full flow:
- Validates the Hermes profile and config directory.
- Generates a per-connector auth token scoped to Hermes.
- Adds the
remnic:block to Hermesconfig.yaml(with rollback on failure). - Commits the token to
~/.remnic/tokens.json. - Writes the connector config to
~/.config/engram/.engram-connectors/connectors/hermes.json. - Runs a daemon health check — reports whether the daemon is reachable but does not start it. If unreachable, install still succeeds and prints
remnic daemon startas the next step.
To install into a non-default Hermes profile:
remnic connectors install hermes --config profile=Research
Write ~/.remnic/tokens.json in the following format:
{
"tokens": [
{ "connector": "hermes", "token": "remnic_hm_...", "createdAt": "2026-01-01T00:00:00Z" }
]
}The token loader searches for connector: "hermes" first, then connector: "openclaw". It also checks ~/.engram/tokens.json as a legacy fallback.
- Inline
token:field inconfig.yaml. connector: "hermes"entry in~/.remnic/tokens.json.connector: "openclaw"entry in~/.remnic/tokens.json.connector: "hermes"entry in~/.engram/tokens.json(legacy fallback).connector: "openclaw"entry in~/.engram/tokens.json(legacy fallback).- Empty string — daemon calls will return 401 until a token is configured.
Called when the plugin loads. Creates an httpx.AsyncClient pointed at http://<host>:<port>/engram/v1 and issues a GET /health request. A failed health check is swallowed and treated as non-fatal — the daemon may become available later in the session. If the client is not initialized (daemon was never reachable), all subsequent hook methods return early without errors.
Note: the HTTP base path currently uses /engram/v1 because the Remnic daemon exposes a legacy surface during the v1.x compat window. This will change to /remnic/v1 once the daemon ships the dual-path rollout.
Called before every LLM request. Behavior:
- Scans
messagesin reverse to find the last message withrole: "user". - Skips recall entirely if the user message is absent or fewer than 3 words (whitespace-split). This avoids triggering recall on very short acknowledgments like "ok" or "thanks".
- Issues
POST /recallwith the user message as the query,sessionKey, andtopK: 8. The plugin leaves recall mode unset so the daemon default can include LCM compressed-history sections whenlcmEnabled: true. - If the response has a non-empty
contextfield andcount > 0, returns a<remnic-memory count="N">block that Hermes injects into the system prompt. - Exceptions are swallowed; returns
""on any error so the LLM call proceeds normally.
Called after every agent response. Takes the full session transcript and sends the last 2 messages (user + assistant) to POST /observe. This provides near-real-time observation without the cost of replaying the entire transcript on every turn.
Exceptions are swallowed.
Called when the session ends. Receives a session dict; reads session["messages"] and sends the full transcript to POST /observe. This is the deep extraction pass — the daemon analyses the complete conversation for structured memory candidates.
Exceptions are swallowed.
Called by Hermes when its active session id changes without tearing down the provider. If remnic.session_key is explicitly configured, Remnic preserves that stable key. If session_key is omitted and the plugin generated an ephemeral key, Remnic updates it to the new Hermes session id so recalls and observations remain scoped to the current Hermes conversation after /new, /reset, or similar session-boundary operations.
Closes the httpx.AsyncClient. Safe to call when the client was never initialized.
Audit date: 2026-04-30. Upstream Hermes reference used: NousResearch/hermes-agent commit 1d8068d (2026-04-30T12:57:02-07:00).
Remnic remains a Hermes memory provider plugin. In plugin.yaml, kind: exclusive marks the package as an exclusive provider selected by memory.provider, provides_tools enumerates the Remnic and legacy Engram tool surfaces, and provides_hooks declares the wired on_session_reset hook. The Hermes context_engine slot is still intentionally unused because it replaces Hermes' local ContextCompressor; it is not the right slot for recall, observation, heartbeat, dreams, or reset handling.
| OpenClaw surface | Hermes surface | Status | Remnic behavior |
|---|---|---|---|
agent_heartbeat |
Hermes cron scheduler (cron/scheduler.py) and agent/tool jobs |
Equivalent but different | Remnic does not register a plugin tick hook. Hermes scheduled jobs can call Remnic maintenance tools such as remnic_memory_summarize_hourly, remnic_conversation_index_update, remnic_dreams_run, and remnic_compounding_weekly_synthesize. |
before_reset |
Plugin hooks on_session_finalize and on_session_reset, plus MemoryProvider on_session_end / on_session_switch |
Wired for session scoping | Remnic registers an on_session_reset hook when Hermes exposes ctx.register_hook, and the provider implements on_session_switch so generated session keys follow the new Hermes session id. Stable configured session_key values are preserved. |
commands.list / registerCommand |
ctx.register_command for in-session slash commands and ctx.register_cli_command for hermes <subcommand> commands |
Available, not wired | Remnic exposes explicit capabilities as agent tools rather than Hermes slash/CLI commands today. The command surfaces exist upstream and can be used later for operator-style commands if there is a concrete UX need. |
dreaming slot |
No dedicated dreaming plugin slot; Hermes uses cron/background jobs and context-engine lifecycle for compression only | Equivalent but different | Remnic keeps dream/consolidation semantics inside the daemon. Hermes can invoke them through remnic_dreams_status and remnic_dreams_run; recurring background execution should be modeled as a Hermes cron job, not as a context_engine. |
No upstream Hermes feature request is needed from this audit: each OpenClaw lifecycle surface has either a direct Hermes plugin hook or a supported Hermes scheduling/command equivalent. The only non-equivalent detail is naming: Hermes does not have an OpenClaw-style dreaming slot, but its cron/background-task model is the correct host-native way to schedule Remnic dream work.
| Tool name | Parameters | Description |
|---|---|---|
remnic_recall |
query: string |
Recall memories from Remnic matching a natural language query |
remnic_store |
content: string |
Store a memory in Remnic for future recall |
remnic_search |
query: string |
Full-text search across all Remnic memories |
remnic_lcm_search |
query: string, sessionKey?: string, namespace?: string, limit?: integer |
Search the daemon-side LCM conversation archive |
remnic_recall_explain |
sessionKey?: string, namespace?: string |
Return the last recall snapshot |
remnic_recall_tier_explain |
sessionKey?: string, namespace?: string |
Return tier attribution for the last direct-answer recall |
remnic_recall_xray |
query: string, sessionKey?: string, namespace?: string, budget?: integer, `disclosure?: chunk |
section |
remnic_memory_last_recall |
sessionKey?: string |
Fetch the memory IDs injected in the last recall |
remnic_memory_intent_debug |
namespace?: string |
Inspect the latest intent/planner snapshot |
remnic_memory_qmd_debug |
namespace?: string |
Inspect the latest QMD recall snapshot |
remnic_memory_graph_explain |
namespace?: string |
Inspect graph recall expansion from the last recall |
remnic_memory_feedback_last_recall |
memoryId: string, `vote: up |
down, note?: string` |
remnic_set_coding_context |
sessionKey: string, `codingContext?: object |
null, projectTag?: string` |
remnic_memory_get |
memoryId: string, namespace?: string |
Fetch one stored memory by id |
remnic_memory_store |
content: string, sessionKey?: string, category?: string, confidence?: number, namespace?: string, tags?: string[], entityRef?: string, ttl?: string, sourceReason?: string |
Store a memory with the daemon's richer memory-store schema |
remnic_memory_timeline |
memoryId: string, namespace?: string, limit?: number |
Fetch the timeline for one stored memory |
remnic_memory_profile |
namespace?: string |
Read the user profile surface |
remnic_memory_entities |
namespace?: string |
List tracked entities |
remnic_memory_questions |
namespace?: string |
List open memory questions |
remnic_memory_identity |
namespace?: string |
Read identity memory state |
remnic_memory_promote |
memoryId: string, namespace?: string, sessionKey?: string |
Promote a memory candidate or stored memory |
remnic_memory_outcome |
memoryId: string, `outcome: success |
failure, namespace?: string, sessionKey?: string, timestamp?: string` |
remnic_entity_get |
name: string, namespace?: string |
Fetch one tracked entity by name |
remnic_memory_capture |
content: string, namespace?: string, category?: string, tags?: string[], entityRef?: string, confidence?: number, ttl?: string, sourceReason?: string |
Capture an explicit memory note |
remnic_memory_action_apply |
action: string, category?: string, content?: string, `outcome?: applied |
skipped |
remnic_continuity_audit_generate |
`period?: weekly | monthly, key?: string` |
remnic_continuity_incident_open |
symptom: string, namespace?: string, triggerWindow?: string, suspectedCause?: string |
Open a continuity incident |
remnic_continuity_incident_close |
id: string, fixApplied: string, verificationResult: string, namespace?: string, preventiveRule?: string |
Close a continuity incident with verification |
remnic_continuity_incident_list |
`state?: open | closed |
remnic_continuity_loop_add_or_update |
id: string, cadence: string, purpose: string, status: string, killCondition: string |
Add or update a continuity improvement loop |
remnic_continuity_loop_review |
id: string, namespace?: string, status?: string, notes?: string, reviewedAt?: string |
Review an existing continuity improvement loop |
remnic_identity_anchor_get |
namespace?: string |
Read the identity continuity anchor |
remnic_identity_anchor_update |
namespace?: string, identityTraits?: string, communicationPreferences?: string, operatingPrinciples?: string, continuityNotes?: string |
Conservatively merge identity anchor sections |
remnic_review_queue_list |
runId?: string, namespace?: string |
Fetch the latest review queue artifact bundle |
remnic_review_list |
filter?: string, namespace?: string, limit?: number |
List contradiction review items |
remnic_review_resolve |
pairId: string, verb: string |
Resolve a contradiction review pair |
remnic_suggestion_submit |
content: string, schemaVersion?: number, idempotencyKey?: string, dryRun?: boolean, sessionKey?: string, category?: string, confidence?: number, namespace?: string, tags?: string[], entityRef?: string, ttl?: string, sourceReason?: string |
Queue a suggested memory for review |
remnic_work_task |
action: string, id?: string, title?: string, description?: string, status?: string, priority?: string, owner?: string, assignee?: string, projectId?: string, tags?: string[], dueAt?: string |
Manage work-layer tasks |
remnic_work_project |
action: string, id?: string, name?: string, description?: string, status?: string, owner?: string, tags?: string[], taskId?: string, projectId?: string |
Manage work-layer projects |
remnic_work_board |
action: string, projectId?: string, snapshotJson?: string, linkToMemory?: boolean |
Export or import work-layer board snapshots and markdown |
remnic_shared_context_write_output |
agentId: string, title: string, content: string |
Write agent work product into shared context |
remnic_shared_feedback_record |
agent: string, decision: string, reason: string |
Record shared feedback for peer modeling |
remnic_shared_priorities_append |
agentId: string, text: string |
Append priorities notes for curator merge |
remnic_shared_context_cross_signals_run |
date?: string |
Generate shared-context cross-signal artifacts |
remnic_shared_context_curate_daily |
date?: string |
Generate the daily shared-context roundtable |
remnic_compounding_weekly_synthesize |
weekId?: string |
Generate weekly compounding outputs |
remnic_compounding_promote_candidate |
weekId: string, candidateId: string |
Promote a compounding candidate into durable memory |
remnic_compression_guidelines_optimize |
dryRun?: boolean, eventLimit?: number |
Run compression-guideline policy optimization |
remnic_compression_guidelines_activate |
expectedContentHash?: string, expectedGuidelineVersion?: number |
Activate a staged compression-guideline draft |
remnic_memory_governance_run |
namespace?: string, `mode?: shadow |
apply, recentDays?: number, maxMemories?: number, batchSize?: number` |
remnic_procedure_mining_run |
namespace?: string |
Run procedural memory mining |
remnic_procedural_stats |
namespace?: string |
Read procedural memory stats |
remnic_contradiction_scan_run |
namespace?: string |
Run an on-demand contradiction scan |
remnic_memory_summarize_hourly |
none | Generate hourly conversation summaries |
remnic_conversation_index_update |
sessionKey?: string, hours?: number, embed?: boolean |
Update the conversation index |
remnic_day_summary |
memories?: string, sessionKey?: string, namespace?: string |
Generate a structured end-of-day summary |
remnic_briefing |
since?: string, focus?: string, namespace?: string, `format?: markdown |
json, maxFollowups?: number` |
remnic_context_checkpoint |
sessionKey: string, context: string, namespace?: string |
Save a structured context checkpoint for a session |
remnic_profiling_report |
`format?: ascii | json, limit?: number` |
Each tool handler returns the raw JSON response from the daemon or {"error": "Not connected to Remnic"} when the client is not initialized. Direct memory tools use the daemon's REST endpoints where available; debug, explain, and MCP-native memory surfaces are forwarded through the daemon MCP endpoint.
The remnic_* tools give the agent explicit control for cases where automatic recall is insufficient — for example, storing a specific fact the agent has derived mid-session, searching the LCM archive directly, inspecting why a recall result appeared, opening a continuity incident, curating stored memories, saving a checkpoint, or generating a profiling report.
Hermes profiles live under ~/.hermes/profiles/<name>/ and each loads its own config.yaml. You can use different session_key values to keep memory contexts distinct across profiles:
# ~/.hermes/profiles/research/config.yaml
plugins:
- remnic_hermes
remnic:
host: "127.0.0.1"
port: 4318
session_key: "research"# ~/.hermes/profiles/coding/config.yaml
plugins:
- remnic_hermes
remnic:
host: "127.0.0.1"
port: 4318
session_key: "coding"The session_key is passed on every /recall and /observe call, so the daemon can scope retrieval to sessions with matching keys. If session_key is omitted, the provider generates a random key (hermes-<12hex>) at startup; this means recall will only find memories from the same process lifetime unless you set a stable key.
To share memories across all profiles, omit session_key in both configs and rely on the Remnic daemon's global index.
Every MemoryProvider hook (initialize, pre_llm_call, sync_turn, extract_memories) wraps its daemon call in a bare except Exception: pass block. This is intentional: Remnic being unavailable must never break the agent. The agent continues normally; it just loses memory context for that turn or session.
This design means:
- The daemon can be restarted mid-session without crashing Hermes.
- Misconfigured tokens produce silent auth failures rather than agent crashes.
- Network blips are non-fatal.
If you need to diagnose silent failures, check daemon health directly:
remnic daemon status
curl -s http://127.0.0.1:4318/engram/v1/healthDuring the Engram to Remnic rebrand, the plugin registers legacy aliases for every Remnic tool:
| Tool name | Status | Notes |
|---|---|---|
remnic_recall |
Current | Use for new integrations |
remnic_store |
Current | Use for new integrations |
remnic_search |
Current | Use for new integrations |
remnic_lcm_search |
Current | Use for new integrations |
engram_recall |
Legacy alias | Routes to the same handler as remnic_recall |
engram_store |
Legacy alias | Routes to the same handler as remnic_store |
engram_search |
Legacy alias | Routes to the same handler as remnic_search |
engram_lcm_search |
Legacy alias | Routes to the same handler as remnic_lcm_search |
The legacy tool schemas deliberately describe themselves as "Engram" tools (e.g., "Recall memories from Engram..."). This is intentional: when a language model surfaces the engram_* names, the description must agree with the name so the model does not confuse the two tool sets. Do not update these descriptions to say "Remnic".
The Python class aliases EngramMemoryProvider, EngramClient, and EngramHermesConfig are preserved for import-path compatibility and will be removed in a future major release.
The engram: config block is also still accepted as a fallback. If your config.yaml has engram: instead of remnic:, everything works without changes.
The auth token is missing or invalid. Re-run the connector install to regenerate it:
remnic connectors install hermes
cat ~/.remnic/tokens.json # verify a hermes entry existsremnic daemon status
remnic daemon install # installs and starts the launchd/systemd serviceVerify the HTTP surface is responding:
curl -s http://127.0.0.1:4318/engram/v1/healthThe package is not installed in the Python environment Hermes uses:
which python && pip show remnic-hermes
hermes --versionInstall into the correct environment: <path-to-hermes-python> -m pip install --upgrade remnic-hermes.
- Confirm the daemon is healthy:
remnic daemon status. - Confirm the query is at least 3 words —
pre_llm_callskips recall for shorter messages. - Confirm the token is valid: a 401 is swallowed silently, so daemon health does not catch it.
- Use the explicit tool to test the round-trip: call
remnic_recallwith a query. If it returns{"error": "Not connected to Remnic"},initializenever completed successfully.
If session_key is not set, a new random key is generated each startup. Set a stable session_key in the config if you want cross-session recall to scope correctly:
remnic:
session_key: "my-agent"Or leave it blank to rely on the Remnic daemon's global search (the daemon indexes all sessions, but sessionKey may affect ranking).
If you are upgrading from a configuration that used the engram-hermes package or an engram: config block:
pip install --upgrade remnic-hermesreplacesengram-hermes. Uninstall the old package first:pip uninstall engram-hermes.- Your
config.yamlengram:block continues to work without changes. You can rename it toremnic:at any time — both are accepted. - Tool calls to
engram_recall,engram_store, andengram_searchcontinue to work. No Hermes system prompt or tool-list changes are required. - Python imports of
EngramMemoryProvider,EngramClient, andEngramHermesConfigcontinue to resolve. - When you are ready to fully migrate: rename
engram:toremnic:inconfig.yamland update any explicit tool references toremnic_*.
pip uninstall remnic-hermes
remnic connectors remove hermesremnic connectors remove hermes revokes the token and removes the remnic: block from Hermes config.yaml.