gastownhall/beads · error

node %q: storage_class ephemeral and no_history are mutually

Error message

node %q: storage_class ephemeral and no_history are mutually exclusive

What it means

If a node resolves to storage class `ephemeral`, it is placed on the wisp plane, which by definition has no durable history. Requesting no_history alongside that is redundant-conflicting, so validation rejects it. This is the storage_class-based path of the same exclusivity rule as error 710.

Source

Thrown at cmd/bd/graph_apply.go:908

	}
	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 = ""
	}
	// Reconcile the requested durable class with the effective wisp plane
	// (flag > config, Protocol v0.1 §C1.3): an explicit node storage_class
	// contradicts an ephemeral/no_history node and is rejected, so the durable
	// intent is preserved rather than silently collapsed into an
	// effective-ephemeral record; a per-type config default yields to the
	// effective plane. versioned normalizes to the unset marker only after the
	// check (C2.4).
	var conflict bool
	class, conflict = reconcileStorageClassPlane(class, node.StorageClass != "", ephemeral || noHistory)
	if conflict {
		return false, false, "", fmt.Errorf("node %q: storage_class %s conflicts with ephemeral/no_history: wisp-plane records are storage class ephemeral", node.Key, class)

View on GitHub (pinned to 71377f2769)

Solutions

  1. Drop `no_history` from the node (or unset the --no-history option) since storage_class ephemeral already implies it.
  2. Change storage_class to a non-ephemeral value if you need no_history with durable storage.
  3. Review type-default storage_class configuration that may implicitly make nodes ephemeral.

Example fix

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

Strategy: validation

Validate before calling

for _, n := range plan.Nodes {
  if n.StorageClass == "ephemeral" && n.NoHistory != nil && *n.NoHistory {
    return fmt.Errorf("node %q: drop no_history when storage_class is ephemeral", n.Key)
  }
}

Type guard

func ephemeralWithNoHistory(n Node) bool {
  return n.StorageClass == "ephemeral" && n.NoHistory != nil && *n.NoHistory
}

Try / catch

if err := bd.GraphApply(ctx, plan); err != nil {
  if strings.Contains(err.Error(), "mutually exclusive") {
    plan = stripNoHistoryFromEphemeralNodes(plan); return bd.GraphApply(ctx, plan)
  }
}

Prevention

When it happens

Trigger: Applying a graph plan where a node has `storage_class: "ephemeral"` and `no_history: true`, or inherits ephemeral storage class while the no_history flag/option is set.

Common situations: Combining per-node storage_class with global --no-history; type-default configs mapping a type to ephemeral while the plan sets no_history; copy-pasted retention settings.

Related errors


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