JuliusBrussee/caveman · error
repair forgotten predecessor
Error message
repair forgotten predecessor: %w
What it means
When the forgotten memory has a predecessor (supersedes is set), Forget() updates that predecessor's superseded_by pointer (usually to NULL) inside the same transaction. If that UPDATE fails, the whole forget rolls back and this wrapped error is returned. It signals a write failure on the SQLite file, not a data-logic problem.
Solutions
- Check writability: ensure the DB file, its directory, and -wal/-shm files are writable by the current user; fix permissions and retry.
- Free disk space / raise quota (`df -h`) if the disk is full, then retry Forget.
- Look for competing writers holding the write lock (`lsof <db>`) and stop them; add `_busy_timeout` to the DSN for transient contention.
- Run `PRAGMA integrity_check;`; if pages are corrupt, restore from backup and replay via remember/supersede.
Example fix
// before: DB on a read-only backup mount -> repair UPDATE fails
s, _ := mem.Open("/mnt/backup-ro/cavemem.db")
s.Forget(id) // repair forgotten predecessor: attempt to write a readonly database
// after: open a writable copy
s, _ := mem.Open("/data/cavemem.db") Defensive patterns
Strategy: retry
Validate before calling
// Go: ensure the DB volume is writable and has headroom before Forget
func writableCheck(path string) error {
if err := unix.Access(filepath.Dir(path), unix.W_OK); err != nil {
return fmt.Errorf("db directory not writable: %w", err)
}
return nil
} Type guard
null
Try / catch
ok, err := store.Forget(id)
if err != nil && strings.Contains(err.Error(), "repair forgotten predecessor") {
// check read-only mount / disk full / lock, fix, then retry (Forget is idempotent)
if errors.Is(err, sqlite.ErrBusy) { /* retry with backoff */ }
return err
} Prevention
- Never open the store from a read-only mount or backup copy that must stay pristine.
- Keep the DB, its directory, and -wal/-shm files owned and writable by the service user.
- Alert on disk usage thresholds so the volume never fills mid-transaction.
- Set a busy timeout in the DSN to tolerate concurrent writers.
When it happens
Trigger: Calling Forget(id) on a memory that has a predecessor while the database is read-only, the disk is full, the file is locked by another writer at UPDATE time, or the DB is corrupted on the affected pages.
Common situations: Running cavemem against a DB on a read-only mount or read-only backup copy, disk quota exhausted, concurrent long write transactions holding the lock, or WAL/journal files not writable.
Related errors
AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20).
Data as JSON: /api/errors/5bbd420d3d6790cb.
Report an issue: GitHub.
Appendix: source
Thrown at mem/store.go:345
return false, fmt.Errorf("forget begin: %w", err)
}
defer tx.Rollback()
var supersedes, supersededBy sql.NullString
if err := tx.QueryRow(
`SELECT supersedes, superseded_by FROM memories WHERE id = ?`, id,
).Scan(&supersedes, &supersededBy); err != nil {
if err == sql.ErrNoRows {
return false, nil
}
return false, fmt.Errorf("read forgotten memory: %w", err)
}
if supersedes.Valid {
if _, err := tx.Exec(
`UPDATE memories SET superseded_by = ? WHERE id = ?`,
nullableLineageID(supersededBy), supersedes.String,
); err != nil {
return false, fmt.Errorf("repair forgotten predecessor: %w", err)
}
}
if supersededBy.Valid {
if _, err := tx.Exec(
`UPDATE memories SET supersedes = ? WHERE id = ?`,
nullableLineageID(supersedes), supersededBy.String,
); err != nil {
return false, fmt.Errorf("repair forgotten successor: %w", err)
}
}
if _, err := tx.Exec(`DELETE FROM memories WHERE id = ?`, id); err != nil {
return false, fmt.Errorf("forget: %w", err)
}
if err := tx.Commit(); err != nil {
return false, fmt.Errorf("forget commit: %w", err)
}
return true, nil
}View on GitHub (pinned to 3ee70a1026)