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 nilView on GitHub (pinned to 27d5a3981a)
Solutions
- 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
- 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
- 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
- Route all supersede edits for a memory through a single writer (one process or a mutex)
- Always derive oldID from a fresh read in the same session, never from persisted/stale state
- Treat this error as a retry signal with re-read, not a blind retry of the same call
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
- prefix replacement put: %w
- ccr: typed object id collision
- cave_memory_too_large
- native runtime: socket already active
- ErrMemoryTooLarge
AI-assisted analysis of JuliusBrussee/caveman@27d5a3981a (2026-08-15).
Data as JSON: /api/errors/8ee3c708ce93c239.
Report an issue: GitHub.