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

  1. Check the wrapped cause; confirm the root still exists and is not already closed
  2. Resolve concurrent writers (another bd session or active sync) and retry
  3. If status validation rejects the close, adjust the root's status/workflow before squashing
  4. 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

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


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