JuliusBrussee/caveman · error
cyclic supersession history at
Error message
cyclic supersession history at %s
What it means
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.
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.
Example fix
-- before: A and B point at each other, History cannot order them UPDATE memories SET supersedes='B' WHERE id='A'; UPDATE memories SET supersedes='A' WHERE id='B'; -- after: break the cycle, keep only the true predecessor edge UPDATE memories SET supersedes=NULL WHERE id='B';
Defensive patterns
Strategy: validation
Validate before calling
// Go: detect a cycle in the supersedes chain before calling History
func hasCycle(db *sql.DB, id string) bool {
seen := map[string]bool{}
cur := id
for cur != "" {
if seen[cur] { return true }
seen[cur] = true
var prev sql.NullString
if db.QueryRow(`SELECT supersedes FROM memories WHERE id=?`, cur).Scan(&prev) != nil {
return false
}
if !prev.Valid { break }
cur = prev.String
}
return false
} Type guard
null
Try / catch
hist, err := store.History(id)
if err != nil && strings.HasPrefix(err.Error(), "cyclic supersession history at ") {
cyclicID := strings.TrimPrefix(err.Error(), "cyclic supersession history at ")
// break the cycle via targeted SQL or Forget(), then retry
return repairLineage(cyclicID)
} Prevention
- 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.
When it happens
Trigger: 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().
Common situations: 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.
Understand the failure class
Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.
Related errors
- broken supersession history at
- repair forgotten successor
- budget is below CCR storage minimum
- cave_ccr_budget_exceeded
- cave_sandbox_subagent_cycle
AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20).
Data as JSON: /api/errors/e5742b09cc9304bc.
Report an issue: GitHub.
Appendix: source
Thrown at mem/store.go:294
// id. Broken/cyclic lineage is rejected rather than returning partial history.
func (s *Store) History(id string) ([]Memory, error) {
if strings.TrimSpace(id) == "" {
return nil, fmt.Errorf("history requires memory id")
}
current, err := s.memoryByID(id)
if err != nil {
return nil, err
}
seen := map[string]bool{current.ID: true}
var before []Memory
cursor := current
for cursor.Supersedes != "" {
prev, err := s.memoryByID(cursor.Supersedes)
if err != nil {
return nil, fmt.Errorf("broken supersession history at %s: %w", cursor.ID, err)
}
if seen[prev.ID] {
return nil, fmt.Errorf("cyclic supersession history at %s", prev.ID)
}
seen[prev.ID] = true
before = append(before, prev)
cursor = prev
}
history := make([]Memory, 0, len(before)+1)
for i := len(before) - 1; i >= 0; i-- {
history = append(history, before[i])
}
history = append(history, current)
cursor = current
for cursor.SupersededBy != "" {
next, err := s.memoryByID(cursor.SupersededBy)
if err != nil {
return nil, fmt.Errorf("broken supersession history at %s: %w", cursor.ID, err)
}
if seen[next.ID] {
return nil, fmt.Errorf("cyclic supersession history at %s", next.ID)View on GitHub (pinned to 3ee70a1026)