gastownhall/beads · error
failed to close wisp root %s: %w
Error message
failed to close wisp root %s: %w
What it means
When the squashed molecule root is ephemeral (a wisp), squash auto-closes it to complete the molecule lifecycle. If w.CloseIssue fails, the error is wrapped as 'failed to close wisp root %s: %w', aborting the transaction and rolling back the squash.
Source
Thrown at cmd/bd/mol_squash.go:326
if err := w.AddDependency(ctx, dep, actorName); err != nil {
return nil, fmt.Errorf("failed to link digest to root: %w", err)
}
// Delete ephemeral children within the same transaction
if !keepChildren {
for _, id := range childIDs {
if err := w.DeleteIssue(ctx, id, actorName); err != nil {
return nil, fmt.Errorf("failed to delete child %s: %w", id, err)
}
result.DeletedCount++
}
}
// Auto-close the root if it's a wisp — squash completes the molecule lifecycle
if root.Ephemeral {
reason := fmt.Sprintf("Squashed: %d steps → digest %s", len(children), result.DigestID)
if err := w.CloseIssue(ctx, root.ID, reason, actorName); err != nil {
return nil, fmt.Errorf("failed to close wisp root %s: %w", root.ID, err)
}
// Clear ephemeral so the closed root stops being re-emitted by every wisp-table export cycle.
if err := w.UpdateIssue(ctx, root.ID, map[string]interface{}{"wisp": false}, actorName); err != nil {
return nil, fmt.Errorf("failed to clear ephemeral flag on root %s: %w", root.ID, err)
}
result.WispSquash = true
}
return result, nil
}
func init() {
molSquashCmd.Flags().Bool("dry-run", false, "Preview what would be squashed")
molSquashCmd.Flags().Bool("keep-children", false, "Don't delete ephemeral children after squash")
molSquashCmd.Flags().String("summary", "", "Agent-provided summary (bypasses auto-generation)")
molCmd.AddCommand(molSquashCmd)
}View on GitHub (pinned to 71377f2769)
Solutions
- Check the wrapped cause; confirm the root still exists and is not already closed
- Resolve concurrent writers (another bd session or active sync) and retry
- If status validation rejects the close, adjust the root's status/workflow before squashing
- Retry the squash after storage is healthy; the digest creation will redo atomically
Defensive patterns
Strategy: try-catch
Validate before calling
// Check root is open and still exists before squash
root, err := s.GetIssue(ctx, rootID)
if err != nil || root.Status == "closed" {
return fmt.Errorf("root %s missing or already closed", rootID)
} Type guard
func canClose(i *types.Issue) bool { return i != nil && i.Status != "closed" } Try / catch
err := squashMolecule(ctx, s, root, children, keep, summary, actor)
if err != nil {
if strings.Contains(err.Error(), "failed to close wisp root") {
log.Printf("wisp close failed; transaction rolled back: %v", errors.Unwrap(err))
}
return err
} Prevention
- Verify the root is not already closed/deleted before squashing
- Pause sync operations during molecule mutations
- Handle status-transition validation rules in custom workflows
When it happens
Trigger: Squashing a wisp molecule when closing the root fails: root deleted concurrently, status transition rejected by validation, or storage write error/lock.
Common situations: Concurrent process already closed or deleted the root; custom status workflows rejecting the close transition; DB lock contention during sync.
Related errors
- failed to clear ephemeral flag on root %s: %w
- determining wisp status for %s: %w
- failed to create digest issue: %w
- failed to link digest to root: %w
- failed to delete child %s: %w
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/31e3d002c25f9ca6.
Report an issue: GitHub.