JuliusBrussee/caveman · error
cyclic supersession history at %s
Error message
cyclic supersession history at %s
What it means
During History's backward walk over the Supersedes chain, a previously visited memory id was reached again, which means the lineage graph contains a cycle (A supersedes B, B supersedes A, or a self-loop). History refuses to return partial or infinite history and errors out at the offending id instead of looping forever.
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 27d5a3981a)
Solutions
- Inspect the cycle: SELECT id, supersedes, superseded_by FROM memories WHERE id IN (...) following the supersedes chain from the id you passed to find the loop
- Repair the bad pointer(s) — set the offending supersedes to the true predecessor (or NULL for the root) — the supersede transaction normally makes cycles impossible, so a cycle indicates manual/direct writes
- If reproduction is easy, check whether any code path writes supersedes/superseded_by outside Store.Supersede and remove it
Example fix
-- before (corrupt row creating a cycle) -- A.supersedes = B, B.supersedes = A -- after: break the loop at the older memory UPDATE memories SET supersedes = NULL WHERE id = 'B';
Defensive patterns
Strategy: validation
Try / catch
hist, err := store.History(id)
if err != nil {
if strings.Contains(err.Error(), "cyclic supersession history") {
// log the id, quarantine the chain, alert — data repair needed
}
return err
} Prevention
- Never write supersedes/superseded_by outside Store.Supersede's transaction
- After manual DB surgery or restore, run a History walk over affected ids to verify linearity
- Keep SQLite backups before any direct pointer edits so a bad repair is reversible
When it happens
Trigger: Corrupted lineage from manual SQLite edits, a partial bug in an older supersede implementation, or direct writes that set supersedes pointers inconsistently. The `seen` map trips when cursor.Supersedes resolves to an id already on the path — including the starting memory (self-reference).
Common situations: Hand-crafted rows inserted with sqlite3 for migration/testing that accidentally cross-link; concurrent supersede bugs in custom forks; restoring a backup on top of an existing table producing duplicate ids with stale pointers.
Related errors
- cave_memory_too_large
- memory %s changed during supersede
- ErrMemoryTooLarge
- history requires memory id
- memory %s not found
AI-assisted analysis of JuliusBrussee/caveman@27d5a3981a (2026-08-15).
Data as JSON: /api/errors/e5742b09cc9304bc.
Report an issue: GitHub.