Skip to content

Hermes Agent memory provider on Upstash Redis (DX-3182) - #80

Merged
CahidArda merged 3 commits into
mainfrom
dx-3182-hermes-memory-provider-main
Oct 28, 2026
Merged

CahidArda merged 3 commits into
mainfrom
dx-3182-hermes-memory-provider-main

Conversation

@CahidArda

Copy link
Copy Markdown
Collaborator

Resolves the rest of DX-3182. Brings the Hermes Agent memory provider from #76 to main.

Why this PR

#76 was stacked on #75. #75 was squash-merged into main, and #76 was merged 91 seconds later into #75's branch, which by then was already merged. So the provider never reached main, which still has only the scaffold.

What's in it

Tested on this branch

  • pnpm py:lint and node scripts/sync-version.mjs --check pass.
  • 46 pytest tests pass against the test Redis.
  • python/hermes-memory/e2e/hermes_e2e.py passes in a real Hermes (v0.20.2, gpt-5-mini):
    • session 2 recalls the facts with no tool calls;
    • the control user's Upstash searches return nothing;
    • the transcripts are searchable;
    • the TS SDK recalls the memory Hermes saved.

Still open from #76's review (not in this PR)

  • If a user id with : is refused, initialize() has already created the stores, so the provider stays active as user default.
  • /undo keeps an undone turn when the user message and the reply both repeat exactly.

Before the release

The PyPI pending trusted publisher for upstash-hermes-memory must exist before the Version Packages PR is merged: owner upstash, repo agentkit, workflow release.yml, no environment.

CahidArda and others added 2 commits October 28, 2026 10:03
* feat(hermes-memory): Upstash Redis memory provider for Hermes Agent (DX-3182)

Python ports of AgentMemory and ChatHistory that write the TS SDK's keys, JSON documents and
Redis Search indexes byte for byte, and UpstashMemoryProvider on top of them: per-turn recall,
per-session searchable transcripts, mirroring of the built-in memory tool, and four
upstash_* tools. Registered through the hermes_agent.memory_providers entry point.

Adds unit and live-Redis tests, and e2e/hermes_e2e.py, which drives a real Hermes session in a
temporary HERMES_HOME and checks what lands in Redis (and that the TS SDK recalls it).

* fix(hermes-memory): address the review of #76 (DX-3182)

- Search recovers when another client drops the shared index: an empty result is checked
  against SEARCH.COUNT (-1 = missing), and the index is recreated and the query retried once.
- Metadata may not set text, userId or createdAt, so it cannot move a memory to another user.
- Memory ids may contain ':' like in the TS SDK; user and session ids still may not.
- The provider creates the index before its first write and waits for indexing after saving a
  fact, so a fact saved in one turn is recalled in the next or in a session started right after.
- Tests create indexes before seeding and wait for indexing before asserting, instead of polling.
- e2e: Hermes's built-in stores are off; the sessions run back to back; session 2 must answer
  with no tool calls (read from state.db); a control session as another user must not know.

* fix(hermes-memory): one-to-one key encoding, per-profile prefixes, /undo (DX-3182)

- User and session ids are percent-encoded (':' -> %3A, '%' -> %25) instead of mapping ':' to
  '_', so 'alice:work' and 'alice_work' no longer share a partition. Ids with neither character
  are unchanged and still match the TS SDK's keys.
- UPSTASH_HERMES_MEMORY_PREFIX / _CHAT_PREFIX are read through get_secret like the credentials,
  so a multiplexed gateway uses each profile's own .env.
- on_session_switch(rewound=True) marks the session; the next sync_turn (or on_session_end) cuts
  the saved transcript back to the latest user message Hermes still has, matching repeated
  messages by occurrence, before appending. Undone turns no longer stay searchable.

* fix(hermes-memory): keep TS ids literal, match undo by turn after compaction (DX-3182)

- User/session ids are used as they are, like the TS SDK; a `:` is refused
  (user id) or skips the transcript (session id) instead of being encoded,
  since any encoding could equal another literal TS id.
- /undo finds the cut by the latest saved turn whose user text and reply
  match the latest turn left in Hermes's history, so in-place compaction
  no longer cuts retained turns.

---------

Co-authored-by: Upstash Box <box@upstash.com>
…ME (DX-3182)

Hermes's built-in session_search reads every session in state.db, whoever it belonged to, so in a
shared home the control user could find sessions 1 and 2 there and the check failed even though
the provider's Redis searches returned nothing for that user.
Copilot AI balanced review requested due to automatic review settings October 28, 2026 09:19
@linear-code

linear-code Bot commented Oct 28, 2026

Copy link
Copy Markdown

DX-3182

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

An explicit zero TTL leaves transcripts stored indefinitely, and forgetting reports success before the search index reflects deletion, allowing removed memories to remain retrievable.

2 open issues
What changed in this PR

Brings the Hermes memory provider from #76 onto main, replacing the Python scaffold with Redis-backed memory and chat history shared with the TypeScript SDK.

Changes:

  • Adds Python memory primitives and a Hermes provider for automatic recall, transcript storage, and memory tools.
  • Adds contract, lifecycle, packaging, and end-to-end checks, with an isolated home for the control user.
  • Documents setup and queues the first 0.1.0 release through Changesets.
File Description
README.md Replaces scaffold status with the provider’s capabilities.
python/​hermes-memory/​tests/​test_redis.py Tests live Redis operations and shared data compatibility.
python/​hermes-memory/​tests/​test_provider.py Tests provider tools, lifecycle, isolation, and transcript rewind.
python/​hermes-memory/​tests/​test_package.py Checks public exports, registration, and wheel contents.
python/​hermes-memory/​tests/​test_layout.py Checks Redis layout compatibility and telemetry.
python/​hermes-memory/​tests/​conftest.py Adds unique test prefixes and key cleanup.
python/​hermes-memory/​src/​upstash_hermes_memory/​provider.py Implements Hermes recall, tools, and transcript lifecycle.
python/​hermes-memory/​src/​upstash_hermes_memory/​memory.py Adds the Python AgentMemory implementation.
python/​hermes-memory/​src/​upstash_hermes_memory/​chat_history.py Adds transcript storage and search.
python/​hermes-memory/​src/​upstash_hermes_memory/​_telemetry.py Adds the package’s Redis telemetry tag.
python/​hermes-memory/​src/​upstash_hermes_memory/​_search.py Handles search index provisioning and recovery.
python/​hermes-memory/​src/​upstash_hermes_memory/​__init__.py Exports primitives and registers the provider.
python/​hermes-memory/​README.md Documents setup, storage layout, and verification.
python/​hermes-memory/​pyproject.toml Declares the Hermes discovery entry point.
python/​hermes-memory/​e2e/​hermes_e2e.py Checks real Hermes recall, isolation, and SDK interoperability.
CLAUDE.md Updates provider documentation and testing guidance.
.changeset/​hermes-memory-provider.md Queues the initial minor release.

Review effort: Balanced

…182)

save_chat skipped EXPIRE for a TTL of 0 (a falsy check), so the transcript never expired. The TS
ChatHistory applies any TTL that is defined, 0 included (expire now); the port now does the same.
Copilot AI balanced review requested due to automatic review settings October 28, 2026 09:30
@CahidArda
CahidArda merged commit 6269e3b into main Oct 28, 2026
2 checks passed

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Python’s save logic preserves an existing title when passed an explicit empty string, preventing callers from clearing it and diverging from TypeScript behavior.

No open issues 🎉

1 previously missed issue

In code that hasn't changed since last review

Medium severity Treat an empty title as an explicit request to clear it

python/​hermes-memory/​src/​upstash_hermes_memory/​chat_history.py:117

Passing title="" after a titled save preserves the old title here, whereas TypeScript ChatHistory.saveChat treats an explicitly empty title as clearing it (packages/sdk/src/chat-history.ts:225). A Python caller therefore cannot clear an existing title and writes a different document for the same operation. Fall back to the existing title only when title is None.

2 resolved since last review

Review effort: Balanced

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants