{"record":{"id":"839e0e091a0f0169","repo":"NousResearch/hermes-agent","slug":"memory-node-id-is-stale-refresh-the-graph","errorCode":null,"errorMessage":"memory node id is stale — refresh the graph","messagePattern":"memory node id is stale — refresh the graph","errorType":"exception","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"agent/learning_mutations.py","lineNumber":59,"sourceCode":"    try:\n        return parts[1], int(parts[2])\n    except ValueError as exc:\n        raise ValueError(f\"bad memory node id: {node_id!r}\") from exc\n\n\ndef _memory_local_index(source: str, global_index: int) -> int:\n    \"\"\"Global card index → position within the source's own file.\n\n    ``_memory_cards`` emits all ``MEMORY.md`` cards before ``USER.md`` cards, so\n    a profile card's local index is its global index minus the memory count.\n    \"\"\"\n    from agent.learning_graph import _memory_cards\n\n    cards = _memory_cards()\n    if not 0 <= global_index < len(cards):\n        raise IndexError(f\"memory index {global_index} out of range\")\n    if cards[global_index].get(\"source\") != source:\n        raise ValueError(\"memory node id is stale — refresh the graph\")\n    if source == \"memory\":\n        return global_index\n    return global_index - sum(1 for c in cards if c.get(\"source\") == \"memory\")\n\n\ndef _locate_memory(source: str, gidx: int) -> tuple[Path, list[str], int]:\n    \"\"\"Resolve a memory card to its file, all §-delimited entries, and local index.\n\n    Entries come from ``MemoryStore._read_file`` — the same parser the memory\n    tool uses — so journey indices stay aligned with what the graph renders.\n    \"\"\"\n    from tools.memory_tool import MemoryStore\n\n    path = _memories_dir() / _MEMORY_FILES[source]\n    if not path.exists():\n        raise ValueError(f\"{path.name} not found\")\n    chunks = MemoryStore._read_file(path)\n    local = _memory_local_index(source, gidx)","sourceCodeStart":41,"sourceCodeEnd":77,"githubUrl":"https://github.com/NousResearch/hermes-agent/blob/c896c09c42910c584c4c7d2325b58c14713ea42c/agent/learning_mutations.py#L41-L77","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":null,"handlingStrategy":"retry","validationCode":"from agent.learning_graph import _memory_cards\nfrom agent.learning_mutations import _parse_memory_id\n\ndef id_matches_live_graph(node_id: str) -> bool:\n    source, gidx = _parse_memory_id(node_id)\n    cards = _memory_cards()\n    return 0 <= gidx < len(cards) and cards[gidx].get(\"source\") == source","typeGuard":null,"tryCatchPattern":"try:\n    mutate(node_id)\nexcept ValueError as e:\n    if \"stale\" in str(e):\n        node_id = rederive_id_after_graph_refresh()\n        mutate(node_id)  # one retry with a fresh id\n    else:\n        raise","preventionTips":["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."],"tags":["memory","learning-graph","stale-state"],"backgroundTag":null,"analyzedSha":"c896c09c42910c584c4c7d2325b58c14713ea42c","analyzedAt":"2026-08-14T17:18:01.089Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}