gastownhall/beads · warning

legacy SQLite issue %s is a tombstone

Error message

legacy SQLite issue %s is a tombstone

What it means

checkRequiredScalars rejects tombstone rows: the legacy DB contains an issue whose status is 'tombstone'. Tombstones are deletion markers from the legacy sync model and are intentionally not migrated into the current schema; this row is skipped-by-error during validation.

Source

Thrown at internal/migration/legacysqlite/reader.go:556

func (x legacyExtras) applyCanonicalTimestamps(issue *types.Issue) error {
	var err error
	if issue.CreatedAt, err = canonicalCurrentDatetime(issue.CreatedAt); err != nil {
		return fmt.Errorf("legacy SQLite issue %s created_at: %w", issue.ID, err)
	}
	if issue.UpdatedAt, err = canonicalCurrentDatetime(issue.UpdatedAt); err != nil {
		return fmt.Errorf("legacy SQLite issue %s updated_at: %w", issue.ID, err)
	}
	return nil
}

// checkRequiredScalars enforces the non-empty ID, non-tombstone status, and
// present created_at/updated_at invariants every legacy issue must satisfy.
func checkRequiredScalars(issue *types.Issue) error {
	if issue.ID == "" {
		return fmt.Errorf("legacy SQLite issue has empty ID")
	}
	if issue.Status == "tombstone" {
		return fmt.Errorf("legacy SQLite issue %s is a tombstone", issue.ID)
	}
	if issue.CreatedAt.IsZero() || issue.UpdatedAt.IsZero() {
		return fmt.Errorf("legacy SQLite issue %s has invalid created_at or updated_at", issue.ID)
	}
	return nil
}

// checkRemovedFields rejects legacy rows that populate columns the current
// schema no longer supports, then validates the tri-state boolean columns.
func (x legacyExtras) checkRemovedFields(issue *types.Issue) error {
	if nonempty(x.closedBy, x.deletedBy, x.deleteReason, x.originalType, x.hookBead, x.roleBead, x.agentState, x.lastActivity, x.roleType, x.rig) || x.deletedAt.Valid || x.crystallizes.Int64 != 0 || x.quality.Valid || (issue.SourceRepo != "" && issue.SourceRepo != ".") {
		return fmt.Errorf("legacy SQLite issue %s uses unsupported removed fields", issue.ID)
	}
	for _, b := range []struct {
		name string
		v    sql.NullInt64
	}{{"ephemeral", x.ephemeral}, {"pinned", x.pinned}, {"is_template", x.template}} {
		if b.v.Valid && b.v.Int64 != 0 && b.v.Int64 != 1 {

View on GitHub (pinned to 71377f2769)

Solutions

  1. This is expected behavior — the tombstone should not be migrated; verify the corresponding entity was intentionally deleted
  2. If the tombstone is stale and the live row was restored elsewhere, delete the tombstone row: sqlite3 legacy.db "DELETE FROM issues WHERE id='<id>' AND status='tombstone'"
  3. If deletion was unintended, recreate the issue content before migrating
  4. Re-run the migration

Example fix

// before
status = 'tombstone' (row blocks migration validation)
// after
DELETE FROM issues WHERE id='<id>' AND status='tombstone';
Defensive patterns

Strategy: validation

Validate before calling

-- identify tombstones before migrating
SELECT id FROM issues WHERE status = 'tombstone';

Type guard

def is_migratable(row) -> bool:
    return row.get('status') != 'tombstone'

Prevention

When it happens

Trigger: Migration reads a legacy issue row with status exactly 'tombstone' — i.e. an issue that was deleted in the legacy system but whose marker row remains in the issues table.

Common situations: Legacy databases that used tombstone rows for sync/replication deletion propagation; this is normal data in old multi-replica setups, not corruption.

Related errors


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