NousResearch/hermes-agent · error · ValueError
memory node id is stale — refresh the graph
Error message
memory node id is stale — refresh the graph
What it means
ValueError from _memory_local_index: the node id parsed and the index is in range, but the card at that global index belongs to a different source than the id claims (e.g. the id says 'user' but the card at that position is from MEMORY.md). The graph emits all 'memory' cards before 'user' cards, so positional drift means the id predates a change in card ordering/count.
Source
Thrown at agent/learning_mutations.py:59
try:
return parts[1], int(parts[2])
except ValueError as exc:
raise ValueError(f"bad memory node id: {node_id!r}") from exc
def _memory_local_index(source: str, global_index: int) -> int:
"""Global card index → position within the source's own file.
``_memory_cards`` emits all ``MEMORY.md`` cards before ``USER.md`` cards, so
a profile card's local index is its global index minus the memory count.
"""
from agent.learning_graph import _memory_cards
cards = _memory_cards()
if not 0 <= global_index < len(cards):
raise IndexError(f"memory index {global_index} out of range")
if cards[global_index].get("source") != source:
raise ValueError("memory node id is stale — refresh the graph")
if source == "memory":
return global_index
return global_index - sum(1 for c in cards if c.get("source") == "memory")
def _locate_memory(source: str, gidx: int) -> tuple[Path, list[str], int]:
"""Resolve a memory card to its file, all §-delimited entries, and local index.
Entries come from ``MemoryStore._read_file`` — the same parser the memory
tool uses — so journey indices stay aligned with what the graph renders.
"""
from tools.memory_tool import MemoryStore
path = _memories_dir() / _MEMORY_FILES[source]
if not path.exists():
raise ValueError(f"{path.name} not found")
chunks = MemoryStore._read_file(path)
local = _memory_local_index(source, gidx)View on GitHub (pinned to c896c09c42)
Solutions
- Refresh the graph and re-derive the node id — the error message is explicit: 'refresh the graph'.
- Treat node ids as single-use within a turn: render the graph, mutate, re-render before the next mutation.
- Avoid interleaving direct MemoryStore writes with graph-derived ids in the same workflow.
Defensive patterns
Strategy: retry
Validate before calling
from agent.learning_graph import _memory_cards
from agent.learning_mutations import _parse_memory_id
def id_matches_live_graph(node_id: str) -> bool:
source, gidx = _parse_memory_id(node_id)
cards = _memory_cards()
return 0 <= gidx < len(cards) and cards[gidx].get("source") == source Try / catch
try:
mutate(node_id)
except ValueError as e:
if "stale" in str(e):
node_id = rederive_id_after_graph_refresh()
mutate(node_id) # one retry with a fresh id
else:
raise Prevention
- Validate id-vs-graph consistency (source at that index) right before the call.
- Do not interleave direct MemoryStore writes with graph-derived ids.
- Re-render the graph whenever MEMORY.md changes — index drift affects every user-source id.
When it happens
Trigger: MEMORY.md gained or lost cards after the id was minted, shifting every USER.md card's global index; the id 'memory:user:5' now points at a memory-source card, triggering the mismatch check cards[gidx]['source'] != source.
Common situations: Agent holds node ids from an earlier turn while the user (or memory tool) edited MEMORY.md in between; long-running sessions with background memory writes.
Related errors
- memory index {global_index} out of range
- bad memory node id: {node_id!r}
- Session transcript exceeds the Desktop safe-load limit; use
- {path.name} not found
AI-assisted analysis of NousResearch/hermes-agent@c896c09c42 (2026-08-14).
Data as JSON: /api/errors/839e0e091a0f0169.
Report an issue: GitHub.