JuliusBrussee/caveman · error
forget
Error message
forget: %w
What it means
After repairing both neighbor pointers, Forget() deletes the target row with `DELETE FROM memories WHERE id = ?`. If that statement fails, the transaction (including all pointer repairs) is rolled back and this wrapped error is returned. This is the final write of the forget; failures point to storage-level problems.
Solutions
- Check the filesystem is writable and has space (`df -h`, `touch <dir>/test`), fix, and retry the forget.
- Check for competing writers/locks with `lsof <db>`; serialize deletes or add a busy timeout to the DSN.
- Run `PRAGMA integrity_check;` — on corruption, restore from backup and rebuild lineage via remember/supersede.
- Retry Forget after conditions change; note Forget is idempotent (missing id returns false, nil), so a safe re-run is cheap.
Example fix
// before: transient lock causes DELETE to fail; immediate hard fail
ok, err := s.Forget(id)
// after: open store with busy timeout so SQLite waits out short locks
s, err := mem.Open("file:cavemem.db?_busy_timeout=5000")
// then re-run Forget; it is safe to retry
ok, err := s.Forget(id) Defensive patterns
Strategy: retry
Validate before calling
// Go: idempotency makes retry safe; pre-check storage writability first
func canRetryForget(store *mem.Store, id string) bool {
_, err := store.History(id) // chain errors mean the row state, not storage, is the issue
return err == nil || !strings.Contains(err.Error(), "broken supersession history")
} Type guard
null
Try / catch
for attempt := 0; attempt < 3; attempt++ {
ok, err := store.Forget(id)
if err == nil {
_ = ok
break
}
if !strings.Contains(err.Error(), "forget:") {
break // different failure, don't blind-retry
}
time.Sleep(time.Duration(attempt+1) * 200 * time.Millisecond)
} Prevention
- Treat Forget as idempotent and safe to re-run — build retry logic around that guarantee.
- Avoid external jobs that flip the DB file or volume read-only while cavemem runs.
- Set WAL journal mode plus a busy timeout for resilient concurrent access.
- Watch disk space and inode health on the store volume.
When it happens
Trigger: Calling Forget(id) on an existing memory when the database has become unwritable between the earlier reads and the DELETE: read-only filesystem flipped to read-only, disk full, lock contention, or page corruption. (A row that vanished earlier surfaces as (false, nil), not this error.)
Common situations: Long-running MCP server whose volume filled mid-operation, an admin chmod-ing the DB read-only while the server runs, or concurrent writers exhausting SQLite's lock handling.
Related errors
AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20).
Data as JSON: /api/errors/7467698e2a5fbaa0.
Report an issue: GitHub.
Appendix: source
Thrown at mem/store.go:357
}
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
}
func nullableLineageID(value sql.NullString) any {
if value.Valid {
return value.String
}
return nil
}
// Count returns the number of durable memory blocks without loading their
// contents. Callers omit this number on error rather than presenting zero.
func (s *Store) Count() (int, error) {
var count intView on GitHub (pinned to 3ee70a1026)