gastownhall/beads · error

node %q: ephemeral and no_history are mutually exclusive

Error message

node %q: ephemeral and no_history are mutually exclusive

What it means

During `bd graph apply`, each plan node's storage settings are validated. A node cannot be both ephemeral and no_history: ephemeral records live on the wisp plane with their own retention rules, so requesting both is contradictory. The error is returned while reconciling node flags before any issue is created, so the whole apply aborts transactionally.

Source

Thrown at cmd/bd/graph_apply.go:896

// unset. "ephemeral" is the spelled-out spelling of ephemeral: true (C1.4) —
// it routes the node to the wisp plane and leaves the marker cell empty
// (wisp-plane rows derive their class, C1.2). A durable class
// (versioned/unversioned) combined with an effective wisp-plane node is
// reconciled by flag-over-config precedence: an explicit node storage_class is
// rejected rather than silently erased, while a per-type config default yields
// to the effective plane; versioned normalizes to the unset marker only after
// that check.
func graphApplyNodeStorageClass(node GraphApplyNode, opts GraphApplyOptions) (ephemeral, noHistory bool, class types.StorageClass, err error) {
	ephemeral = opts.Ephemeral
	if node.Ephemeral != nil {
		ephemeral = *node.Ephemeral
	}
	noHistory = opts.NoHistory
	if node.NoHistory != nil {
		noHistory = *node.NoHistory
	}
	if ephemeral && noHistory {
		return false, false, "", fmt.Errorf("node %q: ephemeral and no_history are mutually exclusive", node.Key)
	}
	issueType := types.IssueType(node.Type)
	if issueType == "" {
		issueType = types.TypeTask
	}
	class, err = resolveStorageClass(node.StorageClass, issueType.Normalize())
	if err != nil {
		return false, false, "", fmt.Errorf("node %q: %w", node.Key, err)
	}
	if class == types.StorageClassEphemeral {
		if noHistory {
			return false, false, "", fmt.Errorf("node %q: storage_class ephemeral and no_history are mutually exclusive", node.Key)
		}
		if node.Ephemeral != nil && !*node.Ephemeral {
			return false, false, "", fmt.Errorf("node %q: storage_class ephemeral conflicts with ephemeral: false", node.Key)
		}
		ephemeral = true
		class = ""

View on GitHub (pinned to 71377f2769)

Solutions

  1. Remove `no_history: true` from the node, keeping `ephemeral: true` if wisp-plane behavior is desired.
  2. If the node should be durable, remove `ephemeral: true` and keep `no_history: true`.
  3. Check upstream plan generation for code that sets both flags from overlapping options.

Example fix

// before
{"key": "bd-123", "ephemeral": true, "no_history": true}
// after
{"key": "bd-123", "ephemeral": true}
Defensive patterns

Strategy: validation

Validate before calling

for _, n := range plan.Nodes {
  if n.Ephemeral != nil && *n.Ephemeral && n.NoHistory != nil && *n.NoHistory {
    return fmt.Errorf("node %q: cannot set both ephemeral and no_history", n.Key)
  }
}

Type guard

func isExclusiveRetention(n Node) bool {
  return n.Ephemeral != nil && *n.Ephemeral && n.NoHistory != nil && *n.NoHistory
}

Try / catch

if err := bd.GraphApply(ctx, plan); err != nil {
  if strings.Contains(err.Error(), "mutually exclusive") {
    // sanitize plan: drop no_history from ephemeral nodes, retry once
  }
}

Prevention

When it happens

Trigger: Applying a graph plan where a node sets both `ephemeral: true` and `no_history: true` (or the --ephemeral/--no-history options imply both for the same node).

Common situations: Hand-written or AI-generated plan JSON stacking retention flags; template nodes that copied both flags; misunderstanding that no_history is not the way to make a record ephemeral.

Related errors


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