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

  1. 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
  2. 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
  3. 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

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


AI-assisted analysis of JuliusBrussee/caveman@27d5a3981a (2026-08-15). Data as JSON: /api/errors/e5742b09cc9304bc. Report an issue: GitHub.