gastownhall/beads · error
delete: classify planes: %w
Error message
delete: classify planes: %w
What it means
DeleteInTx begins by classifying each requested ID as a regular issue or a wisp via WispIDSetInTx, so the delete can address the right plane. Failure of that probe is wrapped with 'delete: classify planes'. This runs before existence checking, so the typo-reporting order can work.
Source
Thrown at internal/storage/issueops/delete_role.go:38
// writes that must see one snapshot.
//
// It assumes a request already refused by workapi.ValidateDeleteRequest and
// already normalized by workapi.NormalizeDeleteIDs. The accessors do both
// BEFORE opening a transaction, so a malformed request costs no database work.
//
// THE REWRITE IS INSIDE THE TRANSACTION. A route that deleted the rows in one
// transaction and rewrote the neighbors' text afterwards left, on a failure
// between the two, a workspace whose rows were gone and whose descriptions
// still cited them.
func DeleteInTx(ctx context.Context, tx *sql.Tx, req publicops.DeleteRequest) (publicops.DeleteResult, error) {
ids := req.IDs
result := publicops.DeleteResult{DryRun: req.DryRun}
// The existence probe comes FIRST, so `bd delete typo real` reports the
// typo rather than whatever the graph says about the id that resolved.
wispSet, err := WispIDSetInTx(ctx, tx, ids)
if err != nil {
return publicops.DeleteResult{}, fmt.Errorf("delete: classify planes: %w", err)
}
found, err := GetIssuesByIDsInTx(ctx, tx, ids, wispSet)
if err != nil {
return publicops.DeleteResult{}, fmt.Errorf("delete: resolve ids: %w", err)
}
present := make(map[string]bool, len(found))
for _, issue := range found {
if issue != nil {
present[issue.ID] = true
}
}
var missing []string
for _, id := range ids {
if !present[id] {
missing = append(missing, id)
}
}
if len(missing) > 0 {View on GitHub (pinned to 71377f2769)
Solutions
- Inspect the wrapped WispIDSetInTx error; if 'no such table: wisps', run schema migration (bd doctor/migrate)
- Retry the delete once lock contention clears or the connection is restored
- Confirm the database file/server matches the binary version before deleting
Example fix
// before err := deleteOp(ctx, store, ids) // fails: classify planes: no such table: wisps // after — migrate schema first // bd doctor && bd migrate err := deleteOp(ctx, store, ids)
Defensive patterns
Strategy: try-catch
Validate before calling
// verify wisps table exists before delete
var n int
if err := db.QueryRow("SELECT COUNT(*) FROM sqlite_master WHERE name='wisps'").Scan(&n); err != nil || n == 0 {
return fmt.Errorf("wisps table missing; run bd doctor / migrate")
} Try / catch
_, err := DeleteInTx(ctx, tx, req)
if err != nil && strings.Contains(err.Error(), "delete: classify planes:") {
if strings.Contains(err.Error(), "no such table") { migrate(db); return retry(ctx, req) }
return err
} Prevention
- Run bd doctor/migrate after upgrading the binary
- Confirm database version matches binary version
- Ensure the DB is not locked by another writer at delete time
When it happens
Trigger: bd delete (DeleteInTx) where the WispIDSetInTx query against the wisps table fails — wisps table missing (old schema with wisp mode enabled), SQL error, lock contention, or context cancellation at the very start of the transaction.
Common situations: Upgraded binary expecting a wisps table against an unmigrated database; concurrent writer locking the DB at delete time; connection drop to remote Dolt immediately after the transaction opens.
Understand the failure class
Background: "query failed", "%w: SQL error" — wrapped database query errors in Go libraries explained — this error's family across 3 libraries.
Related errors
- db: DependencySQLRepository.WispSourceIDs: %w
- delete: drop wisp deps: %w
- delete: drop wisp rows: %w
- hydrate neighbors (wisps): %w
- delete wisp %s: %w
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/42dd069574bcd82f.
Report an issue: GitHub.