{"record":{"id":"e5742b09cc9304bc","repo":"JuliusBrussee/caveman","slug":"cyclic-supersession-history-at-s","errorCode":null,"errorMessage":"cyclic supersession history at %s","messagePattern":"cyclic supersession history at (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"mem/store.go","lineNumber":294,"sourceCode":"// id. Broken/cyclic lineage is rejected rather than returning partial history.\nfunc (s *Store) History(id string) ([]Memory, error) {\n\tif strings.TrimSpace(id) == \"\" {\n\t\treturn nil, fmt.Errorf(\"history requires memory id\")\n\t}\n\tcurrent, err := s.memoryByID(id)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tseen := map[string]bool{current.ID: true}\n\tvar before []Memory\n\tcursor := current\n\tfor cursor.Supersedes != \"\" {\n\t\tprev, err := s.memoryByID(cursor.Supersedes)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"broken supersession history at %s: %w\", cursor.ID, err)\n\t\t}\n\t\tif seen[prev.ID] {\n\t\t\treturn nil, fmt.Errorf(\"cyclic supersession history at %s\", prev.ID)\n\t\t}\n\t\tseen[prev.ID] = true\n\t\tbefore = append(before, prev)\n\t\tcursor = prev\n\t}\n\thistory := make([]Memory, 0, len(before)+1)\n\tfor i := len(before) - 1; i >= 0; i-- {\n\t\thistory = append(history, before[i])\n\t}\n\thistory = append(history, current)\n\tcursor = current\n\tfor cursor.SupersededBy != \"\" {\n\t\tnext, err := s.memoryByID(cursor.SupersededBy)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"broken supersession history at %s: %w\", cursor.ID, err)\n\t\t}\n\t\tif seen[next.ID] {\n\t\t\treturn nil, fmt.Errorf(\"cyclic supersession history at %s\", next.ID)","sourceCodeStart":276,"sourceCodeEnd":312,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/3ee70a102609e550bd2e68004bf5990a9341c851/mem/store.go#L276-L312","documentation":"History() tracks every memory id visited with a seen set while walking the Supersedes chain backwards. If it reaches a record it has already seen, the chain contains a cycle and no well-defined oldest-to-newest ordering exists, so it returns this error naming the duplicated memory. The store rejects cyclic lineage rather than looping forever or emitting a partial chain.","triggerScenarios":"Calling History(id) when lineage pointers form a loop — e.g. two memories whose supersedes fields point at each other (A supersedes B and B supersedes A), typically produced by manual SQL edits or a buggy external writer bypassing Supersede().","commonSituations":"Hand-editing the SQLite database, a script that back-filled supersedes pointers incorrectly, or data restored from two conflicting backups whose pointer sets were merged.","solutions":["Identify the cycle: `sqlite3 <db> \"SELECT id, supersedes FROM memories\"` and trace pointers from the reported id until it repeats.","Break the cycle by clearing one wrong pointer: `UPDATE memories SET supersedes = NULL WHERE id = '<one-loop-member>'` — choose the edge that is factually wrong.","Prefer deleting the inconsistent records via Forget() (which repairs neighbors) and re-superseding them correctly.","After fixing, re-run History to confirm the chain resolves."],"exampleFix":"-- before: A and B point at each other, History cannot order them\nUPDATE memories SET supersedes='B' WHERE id='A';\nUPDATE memories SET supersedes='A' WHERE id='B';\n-- after: break the cycle, keep only the true predecessor edge\nUPDATE memories SET supersedes=NULL WHERE id='B';","handlingStrategy":"validation","validationCode":"// Go: detect a cycle in the supersedes chain before calling History\nfunc hasCycle(db *sql.DB, id string) bool {\n\tseen := map[string]bool{}\n\tcur := id\n\tfor cur != \"\" {\n\t\tif seen[cur] { return true }\n\t\tseen[cur] = true\n\t\tvar prev sql.NullString\n\t\tif db.QueryRow(`SELECT supersedes FROM memories WHERE id=?`, cur).Scan(&prev) != nil {\n\t\t\treturn false\n\t\t}\n\t\tif !prev.Valid { break }\n\t\tcur = prev.String\n\t}\n\treturn false\n}","typeGuard":"null","tryCatchPattern":"hist, err := store.History(id)\nif err != nil && strings.HasPrefix(err.Error(), \"cyclic supersession history at \") {\n\tcyclicID := strings.TrimPrefix(err.Error(), \"cyclic supersession history at \")\n\t// break the cycle via targeted SQL or Forget(), then retry\n\treturn repairLineage(cyclicID)\n}","preventionTips":["Treat supersedes/superseded_by as store-owned invariants: only Supersede() and Forget() may write them.","When importing or merging data, run a cycle check over lineage pointers before committing.","Avoid manual `UPDATE memories SET supersedes=...` statements; they are the usual cycle source.","Add a test/data audit that walks every chain and asserts no id repeats."],"tags":["sqlite","data-integrity","cycle","lineage"],"backgroundTag":"invalid-state-transition","analyzedSha":"3ee70a102609e550bd2e68004bf5990a9341c851","analyzedAt":"2026-09-20T15:53:39.229Z","contentChangedAt":"2026-09-20T15:53:39.229Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}