gastownhall/beads · error
recording %s in %s: %w
Error message
recording %s in %s: %w
What it means
This error is returned when the library fails to record a successfully-applied migration into its cursor table via `INSERT IGNORE INTO <cursorTable> (version, content_hash)` (internal/storage/schema/schema.go:1677). The cursor row is what marks a migration version as applied; without it, migrations would re-run on the next pass. The wrap text names the migration file and the cursor table involved.
Source
Thrown at internal/storage/schema/schema.go:1677
dirtyBeforeStep, err = dirtyTables(ctx, db, true)
if err != nil {
return count, fmt.Errorf("snapshotting dirty tables before %s: %w", mf.name, err)
}
}
if err := src.preMigrationRepair(ctx, db, mf.version); err != nil {
return count, fmt.Errorf("pre-repair for migration %s: %w", mf.name, err)
}
fmt.Fprintf(stderr, "Applying migration %04d: %s…\n", mf.version, humanMigrationName(mf.name))
start := time.Now()
if err := execMigrationBody(ctx, db, string(data)); err != nil {
return count, fmt.Errorf("migration %s: %w", mf.name, err)
}
sum := sha256.Sum256(data)
contentHash := hex.EncodeToString(sum[:])
if _, err := db.ExecContext(ctx, "INSERT IGNORE INTO "+src.cursorTable+" (version, content_hash) VALUES (?, ?)", mf.version, contentHash); err != nil {
return count, fmt.Errorf("recording %s in %s: %w", mf.name, src.cursorTable, err)
}
count++
// commitEachStep's DOLT_ADD/DOLT_COMMIT is the expensive, fallible
// part of this step on the production embedded path. The "done" line
// (and its timing) must land after that commit succeeds, not before
// it: printing "done" and then hitting a commit error would show an
// operator a false completion, and timing that stopped before the
// commit would understate the step's real cost. A failed commit
// returns before either print statement below runs.
if commitEachStep {
if err := commitMigrationStep(ctx, db, src.cursorTable, mf.name, dirtyBeforeStep); err != nil {
return count, fmt.Errorf("committing migration %s: %w", mf.name, err)
}
}
fmt.Fprintf(stderr, " done (%.1fs)\n", time.Since(start).Seconds())
if migrateStepFaultHook != nil {View on GitHub (pinned to 71377f2769)
Solutions
- Read the wrapped inner error; if the cursor table is missing or corrupted, rebuild it (or restore the database from backup).
- Check for concurrent processes: ensure only one `bd` instance is running against the database (beads uses a lock/lockfile; remove a stale lock if safe).
- Check disk space and filesystem write permissions for the .beads database directory.
- Re-run the migration pass; because the migration succeeded but the cursor row did not, the same migration will be retried — verify its SQL is idempotent or clean up its effects first if the inner error indicates a write failure that partially applied.
- If context timeouts recur, increase the timeout budget or avoid killing the process mid-migration.
Example fix
// before: stale lock from a killed process blocks the cursor insert bd doctor # reports database lock held // after: remove the stale lock with a single writer, then retry rm -f .beads/db.lock bd migrate up
Defensive patterns
Strategy: try-catch
Validate before calling
// Before migrating: single-writer check and cursor table sanity
if _, err := os.Stat(".beads/db.lock"); err == nil {
return fmt.Errorf("another bd process may hold the database lock")
}
var n int
if err := db.QueryRowContext(ctx, "SELECT COUNT(*) FROM "+cursorTable).Scan(&n); err != nil {
return fmt.Errorf("cursor table %s unreadable: %w", cursorTable, err)
} Type guard
var timeout bool
if errors.Is(err, context.DeadlineExceeded) || errors.Is(err, context.Canceled) {
timeout = true // cursor insert was cut off by context cancellation
}
var dbErr *database.DBError
if errors.As(err, &dbErr) && dbErr.Code == mysql.ErrNoSuchTable {
// cursor table missing: rebuild/restore instead of retrying the insert
} Try / catch
if err := migrateUp(ctx, db); err != nil {
if strings.Contains(err.Error(), "recording ") {
// migration body likely applied but cursor row missing:
// the same migration will re-run next pass; verify idempotency
// and repair cursor state via bd doctor before retrying
}
return err
} Prevention
- Run only one bd process per database at a time.
- Monitor disk space on the volume holding .beads.
- Never manually delete or edit the schema cursor table.
- Increase context timeouts for migration runs on large rigs.
- Back up before upgrades so a torn step can be restored.
When it happens
Trigger: The ExecContext INSERT into the schema cursor table fails right after the migration SQL succeeded: cursor table missing or corrupted, a constraint/index conflict not tolerated by INSERT IGNORE, permissions/lock errors on the table, or the connection/context was cancelled between the migration body and the insert.
Common situations: A corrupted or manually-deleted .beads database missing the cursor table; a context cancellation/timeout expiring exactly at the insert; disk-full or read-only filesystem making the write fail; concurrent `bd` processes contending for the same database lock.
Related errors
- reading %s version: %w
- failed to migrate credential keys: %w
- failed to update encrypted password for peer %s: %w
- failed to initialize schema: %w
- failed to rebuild pool after migration: %w
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/c5f32496a7144ee5.
Report an issue: GitHub.