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

  1. Identify the cycle: `sqlite3 <db> "SELECT id, supersedes FROM memories"` and trace pointers from the reported id until it repeats.
  2. 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.
  3. Prefer deleting the inconsistent records via Forget() (which repairs neighbors) and re-superseding them correctly.
  4. 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

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


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)