Skip to content

Map Hermes session id to a UUIDv5 (non-UUID session_id collapses all sessions into the server default) #5

Description

@kumarabd

Summary

The provider passes Hermes' native session identifier (e.g. 20260628_134834_d90c1c66) straight through to ferrosa-memory as session_id. ferrosa-memory requires session_id to be a UUID; a non-UUID value is rejected/substituted with the server's configured default session. Net effect: every Hermes session collapses into one shared default session in ferrosa-memory, so there is no per-session isolation. Additionally, prefetch omits session_id entirely, so reads and writes can target different sessions once writes carry a real session id.

Evidence

ferrosa-memory logs (server side) on every turn:

WARN ferrosa_memory_core::dispatch: substituted configured default for caller-provided session_id
  provided="20260628_134834_d90c1c66" default=b4c62491-fb35-4fbb-b670-5f29dd2d5adf

ferrosa-memory only treats a session_id as valid when it parses as a UUID; otherwise it falls back to the configured default (and warns):

// ferrosa-memory: crates/ferrosa-memory-core/src/dispatch.rs (resolve_session_id)
Some(Value::String(s)) => {
    s.is_empty() || s.eq_ignore_ascii_case("default") || uuid::Uuid::parse_str(s).is_err()
}
// ... non-UUID + configured default => substitute default + warn

In this plugin, the raw Hermes id is sent as the session id, and prefetch sends no session id at all:

# plugin/__init__.py
def initialize(self, session_id: str, **kwargs) -> None:
    self._session_id = session_id           # Hermes' native id, not a UUID
    ...

def prefetch(self, query: str, *, session_id: str = "") -> str:
    result = self._client.call("hybrid_search", {
        "query": query,
        "limit": 5,
    })                                       # no session_id -> server default

def sync_turn(self, user_content, assistant_content, *, session_id=""):
    self._client.call("smart_ingest", {
        ...
        "session_id": session_id or self._session_id,  # non-UUID -> coerced to default
    })

Why this matters

  • No session isolation: all sessions write into the single server default session; cross-session features (per-session consolidation, scenes, workspace profiles) can't distinguish sessions.
  • Read/write divergence risk: once writes carry a per-session UUID but prefetch still omits it, recall reads from the default session while writes go elsewhere.
  • ferrosa-memory is intentionally generic (multi-agent) and keeps a UUID session_id contract down to CQL partition keys; the harness-specific id→UUID mapping belongs in this adapter, not in ferrosa-memory.

Proposed fix

Map Hermes' native session id to a deterministic, stateless UUIDv5 in the adapter, and thread it through all calls (prefetch, sync_turn, on_pre_compress, on_session_end, on_memory_write):

import uuid
# Fixed namespace constant for the ferrosa-hermes adapter.
FERROSA_HERMES_NS = uuid.UUID("………")  # generated once, committed

def _to_ferrosa_session(hermes_session_id: str) -> str:
    return str(uuid.uuid5(FERROSA_HERMES_NS, hermes_session_id))

UUIDv5 is:

  • Deterministic — same Hermes session → same ferrosa session every time, no lookup/state.
  • Stateless — works across multiple Hermes instances and multiple ferrosa-memory replicas with no shared mapping table (important for horizontal scaling).
  • Collision-resistant — namespaced SHA-1 of the id.

prefetch must pass the same mapped session_id so reads and writes agree.

Acceptance criteria

  • Hermes session id is mapped to a UUIDv5 in initialize (and any per-call override is mapped too).
  • The mapped UUID is sent on every ferrosa-memory call, including prefetch/hybrid_search.
  • ferrosa-memory no longer logs the substituted configured default for caller-provided session_id warning for Hermes traffic.
  • Distinct Hermes sessions produce distinct ferrosa-memory sessions (verifiable via get_stats / session scoping).
  • TDD: a test asserting the mapping is deterministic and that prefetch/sync_turn use the same mapped id.

Environment

  • ferrosa-hermes: current main
  • ferrosa-memory: 0.23.x line, shared HTTP mode (UUID session_id contract)
  • Harness: Hermes agent (Docker Compose), provider ferrosa

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions