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
Summary
The provider passes Hermes' native session identifier (e.g.
20260628_134834_d90c1c66) straight through to ferrosa-memory assession_id. ferrosa-memory requiressession_idto 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,prefetchomitssession_identirely, so reads and writes can target different sessions once writes carry a real session id.Evidence
ferrosa-memory logs (server side) on every turn:
ferrosa-memory only treats a
session_idas valid when it parses as a UUID; otherwise it falls back to the configured default (and warns):In this plugin, the raw Hermes id is sent as the session id, and
prefetchsends no session id at all:Why this matters
prefetchstill omits it, recall reads from the default session while writes go elsewhere.session_idcontract 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):UUIDv5 is:
prefetchmust pass the same mappedsession_idso reads and writes agree.Acceptance criteria
initialize(and any per-call override is mapped too).prefetch/hybrid_search.substituted configured default for caller-provided session_idwarning for Hermes traffic.get_stats/ session scoping).prefetch/sync_turnuse the same mapped id.Environment
mainsession_idcontract)ferrosa