{"record":{"id":"2fbb634527fe422b","repo":"headroomlabs-ai/headroom","slug":"memories-old-memory-id-and-new-memory-id-do-no-2fbb63","errorCode":null,"errorMessage":"Memories {old_memory_id} and {new_memory_id} do not form a reciprocal supersession edge","messagePattern":"Memories (.+?) and (.+?) do not form a reciprocal supersession edge","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"headroom/memory/adapters/sqlite.py","lineNumber":735,"sourceCode":"        if old_memory_id == new_memory_id:\n            raise ValueError(\"A memory cannot supersede itself\")\n\n        with self._get_conn() as conn:\n            conn.execute(\"BEGIN IMMEDIATE\")\n            rows = conn.execute(\n                \"SELECT * FROM memories WHERE id IN (?, ?)\",\n                (old_memory_id, new_memory_id),\n            ).fetchall()\n            memories = {row[\"id\"]: self._row_to_memory(row) for row in rows}\n            old_memory = memories.get(old_memory_id)\n            new_memory = memories.get(new_memory_id)\n\n            if old_memory is None:\n                raise ValueError(f\"Memory {old_memory_id} not found\")\n            if new_memory is None:\n                raise ValueError(f\"Memory {new_memory_id} not found\")\n            if old_memory.superseded_by != new_memory_id or new_memory.supersedes != old_memory_id:\n                raise ValueError(\n                    f\"Memories {old_memory_id} and {new_memory_id} do not form \"\n                    \"a reciprocal supersession edge\"\n                )\n\n            conn.execute(\n                \"UPDATE memories SET valid_until = NULL, superseded_by = NULL WHERE id = ?\",\n                (old_memory_id,),\n            )\n            conn.execute(\n                \"UPDATE memories SET supersedes = NULL WHERE id = ?\",\n                (new_memory_id,),\n            )\n\n        old_memory.valid_until = None\n        old_memory.superseded_by = None\n        new_memory.supersedes = None\n        return old_memory, new_memory\n","sourceCodeStart":717,"sourceCodeEnd":753,"githubUrl":"https://github.com/headroomlabs-ai/headroom/blob/322425c43bffde1ed0b64fecf3cf5951565dd82b/headroom/memory/adapters/sqlite.py#L717-L753","documentation":"Raised by detach_supersession when both memories exist but they do not form a reciprocal supersession edge — i.e. old.superseded_by != new_id or new.supersedes != old_id. The detach operation only removes verified two-way edges and never infers one-sided links, so any asymmetry is rejected.","triggerScenarios":"Passing two memories that are adjacent in a chain but whose forward/back pointers disagree (e.g. old.superseded_by points to a different memory than new.supersedes); data corruption from partial updates; manually edited rows.","commonSituations":"Crash between the two UPDATEs in supersede leaving a half-written edge; a memory being superseded twice so pointers moved to a third node; repair scripts assuming adjacency equals an edge.","solutions":["Inspect both rows: SELECT superseded_by, supersedes for the pair and confirm they point at each other before detaching.","If the edge is one-sided, repair the dangling column(s) explicitly rather than calling detach_supersession.","For chains where the old memory was superseded by a different memory, call detach with the actual current partner IDs."],"exampleFix":"// before\nawait store.detach_supersession(old_id, new_id)  # assumed edge\n\n// after\nold = await store.get(old_id); new = await store.get(new_id)\nif old.superseded_by == new_id and new.supersedes == old_id:\n    await store.detach_supersession(old_id, new_id)\nelse:\n    logger.warning(\"no reciprocal edge; manual repair needed\")","handlingStrategy":"validation","validationCode":"old = await store.get(old_id); new = await store.get(new_id)\nif not (old and new and old.superseded_by == new_id and new.supersedes == old_id):\n    logger.warning(\"not a reciprocal edge; manual repair required\")\n    return\nawait store.detach_supersession(old_id, new_id)","typeGuard":"def is_reciprocal(old: Memory, new: Memory) -> bool:\n    return old.superseded_by == new.id and new.supersedes == old.id","tryCatchPattern":"try:\n    await store.detach_supersession(old_id, new_id)\nexcept ValueError as e:\n    if \"reciprocal\" in str(e):\n        # inspect rows and repair dangling pointers manually\n        ...","preventionTips":["Only use detach_supersession on edges created by supersede().","Audit half-written edges after any crash during supersede; the two UPDATEs are transactional but external edits may not be."],"tags":["sqlite","supersession","data-integrity","validation"],"backgroundTag":null,"analyzedSha":"322425c43bffde1ed0b64fecf3cf5951565dd82b","analyzedAt":"2026-08-15T01:03:05.481Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}