Skip to content

feat(memory): add Contexto memory provider plugin - #23217

Closed
amiller wants to merge 4 commits into
NousResearch:mainfrom
amiller:feat/contexto-memory-plugin
Closed

feat(memory): add Contexto memory provider plugin#23217
amiller wants to merge 4 commits into
NousResearch:mainfrom
amiller:feat/contexto-memory-plugin

Conversation

@amiller

@amiller amiller commented May 10, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds Contexto (the OSS self-hosted memory engine) as a MemoryProvider plugin. Per-turn ingest of (user, assistant) pairs into the cognitive store (episodic / semantic / procedural sectors), semantic recall before each turn, plus a contexto_search tool exposed to the model. Sits alongside the existing mem0, honcho, supermemory, etc. plugins — same lifecycle, same interface contract.

Draft because it depends on two changes still in flight upstream at ekailabs/contexto:

  • ekailabs/contexto#143 — Python client at clients/python/ (the pip dependency this plugin imports)
  • ekailabs/contexto#145 — extractor user-identity fix (without it, the plugin still works but emits unanchored User subjects in extracted triples)

The pip_dependencies line in plugin.yaml currently points at the contexto fork (amiller/contexto); will switch to ekailabs/contexto:main once #143 lands.

What's in the plugin

File Purpose
plugins/memory/contexto/__init__.py ContextoMemoryProvider impl
plugins/memory/contexto/plugin.yaml name/version/desc + pip dep + activation metadata
plugins/memory/contexto/README.md install + activation walkthrough
plugins/memory/contexto/tests/test_plugin.py regression tests (require live selfhost; skip if unreachable)

Behavior

  • Targets self-hosted Contexto only (http://localhost:4010 by default, override via CONTEXTO_BASE_URL). Not compatible with the hosted api.getcontexto.com mindmap surface — different endpoints, different abstractions.
  • Agent slug = agent_identity (the active hermes profile name, e.g. default, coder). One slug per profile, so all sessions of a profile share recall — same scoping convention as the other memory plugins.
  • user_id fallback — when the gateway doesn't supply a platform user_id (CLI sessions), uses agent_identity as the contexto userId. Without this, the contexto extractor produces bare User / I triple subjects instead of resolvable identities.
  • Honors agent_context — only the primary agent ingests. Subagents and cron contexts skip writes (their tool-output-heavy turns would corrupt recall semantics and reliably blow extraction timeouts on Gemini Flash). Subagent results still reach memory via on_delegation(task, result) as a single curated parent turn.
  • Daemon-thread errors logged, not printed — the plugin runs ingest and prefetch on background threads; failures route through logger.warning (so they land in agent.log) instead of dumping raw tracebacks to stderr (which polluted the chat UI in earlier iterations).
  • Tools exposed: one tool, contexto_search, for explicit recall queries from the model.

Testing

tests/test_plugin.py covers:

  • Subagent / cron / flush contexts skip ingest, prefetch, and agent registration (no threads spawned, no HTTP)
  • Primary context ingests normally
  • on_delegation captures (task, result) pairs as a single curated parent turn, recallable via search
  • Daemon-thread failures route through the logger, never leak to stderr

All 6 tests pass against a running selfhost (skip if unreachable, so they're CI-safe even without infra).

Activation

# config.yaml
memory:
  provider: contexto
# .env (optional, defaults to localhost:4010)
CONTEXTO_BASE_URL=http://localhost:4010

Or via the wizard: hermes memory setup → pick contexto from the list (auto-detected from plugins/memory/contexto/).

Commit history

  • 34d15b1c1 initial plugin
  • e0735d2ad honor agent_context + capture subagent results via on_delegation
  • 92d8a0712 route ingest/prefetch failures through logger.warning instead of stderr
  • c4c7b9288 default user_id to agent_slug when no platform user_id is set

Test plan

🤖 Generated with Claude Code

amiller and others added 4 commits May 9, 2026 16:47
Wires the self-hosted Contexto memory engine (port 4010 by default)
into the MemoryProvider interface. Per-turn ingest of (user, assistant)
pairs to /v1/ingest, background-threaded prefetch via /v1/search before
each turn, and a contexto_search tool for explicit lookups.

Agent slug is one-per-hermes-profile (derived from kwargs.agent_identity);
userId is per-platform-user (kwargs.user_id, gateway-supplied). The
plugin idempotently registers the agent slug on initialize.

Pip dep points at the Python client in the contexto repo's
clients/python/ subdirectory via git URL.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…_delegation

Two changes that go together:

1. Honor the MemoryProvider ABC contract: skip writes when agent_context
   is non-primary (subagent, cron, flush). Subagent turns are
   tool-output-heavy — 5-10x larger than primary turns — so ingesting
   them under the parent's slug both corrupts recall semantics and
   reliably blows the extraction timeout against Gemini Flash. Without
   this guard, every delegated task triggered a 120s+ Gemini extract
   that often timed out.

2. Implement on_delegation so the parent agent captures (task, result)
   pairs as a single curated turn under its own slug. Subagent raw
   turns are still silenced, but the work they did still reaches memory
   in a clean, recallable form ("[delegated subtask] ... / [delegation
   result] ..."). This means delegating "research X" doesn't lose the
   findings — they're remembered as a parent observation.

Adds tests/test_plugin.py covering all branches: subagent + cron + flush
silenced, primary still ingests, on_delegation captures and is recallable.
Tests skip if the selfhost isn't reachable.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Daemon-thread errors print raw tracebacks to stderr by default. For the
contexto plugin running inside an interactive hermes session, that means
every transient ingest failure (Gemini extract timeout, network blip,
container restart) prints a 30-line traceback right into the user's
chat UI mid-stream. Worse, those tracebacks NEVER made it to agent.log,
so failures were both maximally visible to the user and invisible to
me when debugging.

Wrap _sync and queue_prefetch._run in try/except + logger.warning. The
error type and message (including the response body, thanks to
_raise_with_body in the client) still surface — they just land in
agent.log under "contexto sync failed" / "contexto prefetch failed"
instead of stderr. Same pattern mem0 and supermemory use.

This is logging, not swallowing: the agent's response is non-blocking
on memory writes, the error info is preserved, and the user can grep
agent.log to see what's actually happening with their ingests.

Adds a regression test that captures stderr + caplog and verifies
failures route through the logger only.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Hermes CLI sessions don't have a platform user_id (gateway-supplied), so
the plugin was passing user_id=None to Contexto's /v1/ingest. Without a
userId, the (now-merged-upstream) extractor user-identity anchoring
doesn't kick in — the model produces bare "User" / "I" subjects in
extracted triples, which is exactly the contamination root cause we
filed ekailabs/contexto#144 for.

Fall back to agent_slug as user_id when the platform doesn't supply
one. The agent slug is the hermes profile name (e.g. "default",
"coder"), which is the closest stable identity we have for a CLI user
talking to themselves. Triples will then anchor to that identity:

  Before (CLI session, user_id=None):
    User → has favorite color → teal
    User → is working on → matthammer
  After:
    default → has favorite color → teal
    default → is working on → matthammer

Gateway sessions (Telegram/Discord/etc) keep their per-platform user_id
unchanged — the fallback only applies when kwargs.get("user_id") is
falsy.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@alt-glitch alt-glitch added type/feature New feature or request P3 Low — cosmetic, nice to have comp/plugins Plugin system and bundled plugins tool/memory Memory tool and memory providers labels May 11, 2026
@kshitijk4poor

Copy link
Copy Markdown
Collaborator

Thanks for the contribution!

Per the updated CONTRIBUTING.md, new memory providers are no longer accepted as in-tree additions to plugins/memory/:

Memory Providers: CLOSED to new in-tree additions
PRs adding to plugins/memory/ will be closed. Publish as standalone plugin into ~/.hermes/plugins/ or via pip entry point. Must implement MemoryProvider ABC (sync_turn, prefetch, shutdown, optional post_setup).

Closing this in line with that policy. The path forward is to publish it as a standalone plugin so users can install it directly without touching the Hermes source tree. Once it's published, a small docs PR adding it to the Community plugins section of the README is welcome.

Sorry for the bump — appreciate the time you put into this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/plugins Plugin system and bundled plugins P3 Low — cosmetic, nice to have tool/memory Memory tool and memory providers type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants