gastownhall/beads · error

node %q: storage_class %s conflicts with ephemeral/no_histor

Error message

node %q: storage_class %s conflicts with ephemeral/no_history: wisp-plane records are storage class ephemeral

What it means

After flag reconciliation, `reconcileStorageClassPlane` checks the requested durable storage_class against the effective wisp plane (node is ephemeral or no_history). Wisp-plane records are inherently storage class ephemeral, so a non-empty explicit storage_class that conflicts is rejected per Protocol v0.1 §C1.3/C2.4.

Source

Thrown at cmd/bd/graph_apply.go:926

			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)
	}
	return ephemeral, noHistory, class, nil
}

func executeGraphApply(ctx context.Context, plan *GraphApplyPlan, opts GraphApplyOptions) (*GraphApplyResult, error) {
	if err := opts.Validate(); err != nil {
		return nil, err
	}

	keyToID := make(map[string]string, len(plan.Nodes))
	owner := getOwner()

	commitMsg := plan.CommitMessage
	if commitMsg == "" {
		commitMsg = fmt.Sprintf("bd: graph-apply %d nodes", len(plan.Nodes))
	}

	if err := store.RunInTransaction(ctx, commitMsg, func(tx storage.Transaction) error {

View on GitHub (pinned to 71377f2769)

Solutions

  1. Remove the explicit storage_class from wisp-plane nodes (ephemeral/no_history) and let the plane imply ephemeral.
  2. Drop ephemeral/no_history from the node if the durable storage_class is what you want.
  3. Align the plan generator with Protocol v0.1 §C1.3 so wisp-plane nodes never carry a durable storage_class.

Example fix

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

Strategy: validation

Validate before calling

for _, n := range plan.Nodes {
  wisp := (n.Ephemeral != nil && *n.Ephemeral) || (n.NoHistory != nil && *n.NoHistory)
  if wisp && n.StorageClass != "" && n.StorageClass != "ephemeral" {
    return fmt.Errorf("node %q: wisp-plane nodes must not carry durable storage_class", n.Key)
  }
}

Type guard

func wispPlaneWithDurableClass(n Node) bool {
  wisp := (n.Ephemeral != nil && *n.Ephemeral) || (n.NoHistory != nil && *n.NoHistory)
  return wisp && n.StorageClass != "" && n.StorageClass != "ephemeral"
}

Try / catch

if err := bd.GraphApply(ctx, plan); err != nil {
  if strings.Contains(err.Error(), "wisp-plane records are storage class ephemeral") {
    plan = clearStorageClassOnWispNodes(plan); return bd.GraphApply(ctx, plan)
  }
}

Prevention

When it happens

Trigger: Applying a node with `ephemeral: true` and/or `no_history: true` while also carrying an explicit non-ephemeral `storage_class` (e.g. `versioned`).

Common situations: Per-type config defaults injecting storage_class while the plan sets no_history; combining template nodes (storage_class set) with ephemeral batching flags; protocol version mismatches between plan generator and bd.

Related errors


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