JuliusBrussee/caveman · error

forget begin

Error message

forget begin: %w

What it means

Forget() opens a SQLite transaction before reading and repairing lineage pointers. If db.Begin() fails, the deletion never starts and this wrapped error is returned. Begin failures almost always mean the underlying SQLite connection or database file is unusable (locked, closed, or I/O error).

Solutions

  1. Check for other processes holding the DB: `fuser <db-file>` or `lsof <db-file>`; close or kill stale cavemem/sqlite3 writers and retry.
  2. Verify disk space (`df -h`) and that the file/directory is writable; free space or fix permissions, then retry.
  3. If the store was closed in-process, reopen it with the store constructor before calling Forget.
  4. Enable a busy timeout in the SQLite DSN (e.g. `?_busy_timeout=5000`) so short-lived lock contention resolves instead of failing.

Example fix

// before: default DSN, concurrent writers cause immediate 'database is locked'
s, err := mem.Open("cavemem.db")
// after: give SQLite a busy timeout
s, err := mem.Open("file:cavemem.db?_busy_timeout=5000")
Defensive patterns

Strategy: retry

Validate before calling

// Go: check the store is usable before calling Forget
func storeWritable(path string) error {
	if fi, err := os.Stat(path); err != nil {
		return err
	} else if fi.Mode()&0200 == 0 {
		return fmt.Errorf("db file not writable: %s", path)
	}
	return nil
}

Type guard

null

Try / catch

var ok bool
var err error
for i := 0; i < 3; i++ {
	ok, err = store.Forget(id)
	if err == nil || !strings.Contains(err.Error(), "forget begin") {
		break
	}
	time.Sleep(100 * time.Millisecond << i) // wait out transient lock
}

Prevention

When it happens

Trigger: Calling Forget(id) (API, cavemem_forget MCP tool, or `cavemem forget` CLI) while the database file is locked by another long-running writer, the store was already Closed(), or the DB file/directory is unreadable or on a full disk.

Common situations: Two cavemem processes (CLI + MCP server) contending on the same database, a hung external sqlite3 shell holding a write lock, read-only filesystem or full disk, or calling Forget after Close.

Understand the failure class

Background: Database query failed: Internal Server Error 500s wrapping SQL, Prisma, and connection failures — what to check first — this error's family across 16 libraries.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20). Data as JSON: /api/errors/1c1bf116447eab3e. Report an issue: GitHub.

Appendix: source

Thrown at mem/store.go:327

			return nil, fmt.Errorf("broken supersession history at %s: %w", cursor.ID, err)
		}
		if seen[next.ID] {
			return nil, fmt.Errorf("cyclic supersession history at %s", next.ID)
		}
		seen[next.ID] = true
		history = append(history, next)
		cursor = next
	}
	return history, nil
}

// Forget deletes a memory by id and atomically repairs neighboring lineage
// pointers. Expired predecessors stay expired: deleting a current version means
// forgetting that fact, never silently resurrecting an older one.
func (s *Store) Forget(id string) (bool, error) {
	tx, err := s.db.Begin()
	if err != nil {
		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)

View on GitHub (pinned to 3ee70a1026)