JuliusBrussee/caveman · error

memory %s changed during supersede

Error message

memory %s changed during supersede

What it means

Raised by Store.Supersede when the UPDATE that expires the old memory (`SET valid_until/superseded_by WHERE id = ? AND valid_until IS NULL`) affects zero rows. This is an optimistic-concurrency guard: the memory you are replacing is no longer the current version, because a concurrent Supersede, Forget, or expiry already changed it. The transaction had already inserted the replacement row, but the mismatch aborts the commit path.

Source

Thrown at mem/store.go:254

	if _, err := tx.Exec(
		`INSERT INTO memories
		   (id, text, created_at, valid_from, supersedes)
		 VALUES (?, ?, ?, ?, ?)`,
		newID, newText, now, now, oldID,
	); err != nil {
		return Memory{}, fmt.Errorf("insert replacement memory: %w", err)
	}
	res, err := tx.Exec(
		`UPDATE memories
		    SET valid_until = ?, superseded_by = ?
		  WHERE id = ? AND valid_until IS NULL`,
		now, newID, oldID,
	)
	if err != nil {
		return Memory{}, fmt.Errorf("expire old memory: %w", err)
	}
	if n, _ := res.RowsAffected(); n != 1 {
		return Memory{}, fmt.Errorf("memory %s changed during supersede", oldID)
	}
	if err := tx.Commit(); err != nil {
		return Memory{}, fmt.Errorf("supersede commit: %w", err)
	}
	return Memory{
		ID:         newID,
		Text:       newText,
		CreatedAt:  now,
		ValidFrom:  now,
		Supersedes: oldID,
	}, nil
}

func validateMemorySize(text string) error {
	if len(text) > MaxMemoryBytes {
		return fmt.Errorf("%w: memory is %d bytes, over the %d-byte cap", ErrMemoryTooLarge, len(text), MaxMemoryBytes)
	}
	return nil

View on GitHub (pinned to 27d5a3981a)

Solutions

  1. Re-read the memory and retry Supersede against the new current version (the winner's replacement), or re-fetch the chain if your edit is still needed
  2. If the error repeats with no other writer, inspect the row: SELECT valid_until, superseded_by FROM memories WHERE id = ? — a non-NULL valid_until means it is already historical; supersede its replacement instead
  3. Serialize supersede edits for the same memory id through a single Store/process or a mutex so only one writer races

Example fix

// before
newMem, err := store.Supersede(oldID, updatedText) // races with another writer

// after
newMem, err := store.Supersede(oldID, updatedText)
if err != nil && strings.Contains(err.Error(), "changed during supersede") {
    // another writer won; re-read the current version and re-apply
    current, rerr := store.History(oldID) // or fetch latest via memoryByID chain
    if rerr == nil {
        latest := current[len(current)-1]
        newMem, err = store.Supersede(latest.ID, updatedText)
    }
}
Defensive patterns

Strategy: retry

Validate before calling

cur, err := store.History(oldID)
if err != nil { return err }
latest := cur[len(cur)-1]
if latest.ValidUntil != nil || latest.SupersededBy != "" {
    return fmt.Errorf("memory %s already superseded by %s; supersede the current version", oldID, latest.SupersededBy)
}

Type guard

func isCurrent(m mem.Memory) bool {
    return m.ValidUntil == nil && m.SupersededBy == ""
}

Try / catch

newMem, err := store.Supersede(oldID, text)
if err != nil {
    if strings.Contains(err.Error(), "changed during supersede") {
        // re-resolve the current version and retry once with fresh state
    }
    return err
}

Prevention

When it happens

Trigger: Two goroutines/processes call Supersede(oldID, ...) on the same current memory; the second one's conditional UPDATE matches no row (valid_until is already set). Also hit when the old memory was Forget-deleted or the row expired between your read and your write, or when the id passed is already a superseded (historical) version.

Common situations: Parallel agents editing the same memory through separate Store handles (e.g. CLI and MCP server against the same SQLite file), retry logic that re-runs a supersede whose first attempt actually succeeded, or holding an id from an earlier session after another tool superseded it.

Related errors


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