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

  1. Refresh the graph and re-derive the node id — the error message is explicit: 'refresh the graph'.
  2. Treat node ids as single-use within a turn: render the graph, mutate, re-render before the next mutation.
  3. 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

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


AI-assisted analysis of NousResearch/hermes-agent@c896c09c42 (2026-08-14). Data as JSON: /api/errors/839e0e091a0f0169. Report an issue: GitHub.