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

  1. Check the filesystem is writable and has space (`df -h`, `touch <dir>/test`), fix, and retry the forget.
  2. Check for competing writers/locks with `lsof <db>`; serialize deletes or add a busy timeout to the DSN.
  3. Run `PRAGMA integrity_check;` — on corruption, restore from backup and rebuild lineage via remember/supersede.
  4. 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

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 int

View on GitHub (pinned to 3ee70a1026)