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
- Remove `no_history: true` from the node, keeping `ephemeral: true` if wisp-plane behavior is desired.
- If the node should be durable, remove `ephemeral: true` and keep `no_history: true`.
- 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
- Pick one retention mechanism per node: ephemeral OR no_history, never both.
- Validate plan JSON against the graph schema before apply.
- Keep plan generators free of overlapping retention flags.
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
- node %q: storage_class ephemeral and no_history are mutually
- node %q: storage_class ephemeral conflicts with ephemeral: f
- edge %d %s->%s duplicates a parent-child relationship with d
- got %d close reasons for %d issue IDs; provide exactly one s
- cannot specify both --reason-file and --reason/--resolution/
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/ff0c380c6b5c41ea.
Report an issue: GitHub.