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
- This is expected behavior — the tombstone should not be migrated; verify the corresponding entity was intentionally deleted
- 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'"
- If deletion was unintended, recreate the issue content before migrating
- 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
- Review tombstone rows and confirm intended deletions before migrating
- Purge resolved tombstones from the legacy DB as routine hygiene
- Understand the legacy sync model so tombstones are not mistaken for data loss
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
- sealed legacy SQLite database does not match source fingerpr
- sealed legacy SQLite WAL does not match source fingerprint
- legacy SQLite source changed while sealing
- legacy SQLite source %q must not be a symlink
- legacy SQLite source %q must be a regular file
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/f90c94eec46fdf1a.
Report an issue: GitHub.