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

  1. Read the wrapped inner error; if the cursor table is missing or corrupted, rebuild it (or restore the database from backup).
  2. 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).
  3. Check disk space and filesystem write permissions for the .beads database directory.
  4. 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.
  5. 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

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


AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30). Data as JSON: /api/errors/c5f32496a7144ee5. Report an issue: GitHub.