gastownhall/beads · error
ephemeral and no_history are mutually exclusive
Error message
ephemeral and no_history are mutually exclusive
What it means
An issue cannot be both ephemeral and no_history at the same time; these wisp-plane flags are mutually exclusive. NormalizePersistenceMode rejects the combination up front because the resulting storage semantics are contradictory.
Source
Thrown at internal/types/types.go:962
// IsValid reports whether a persistence mode is a known explicit value.
// Empty is invalid because it would be indistinguishable from an omitted mode.
func (m PersistenceMode) IsValid() bool {
return slices.Contains(validPersistenceModes, m)
}
// NormalizePersistenceMode maps a requested mode from current to its exact
// persistence fields without mutating an issue. The returned values are
// Ephemeral, NoHistory, and StorageClass, respectively. Plane and retention
// changes preserve the create-selected class. Promotion clears an explicit
// ephemeral class marker to select normalized versioned storage. An
// unversioned record may remain persistent but cannot move to a wisp mode.
func NormalizePersistenceMode(current Issue, mode PersistenceMode) (bool, bool, StorageClass, error) {
if !mode.IsValid() {
return false, false, "", fmt.Errorf("invalid persistence mode %q", mode)
}
if current.Ephemeral && current.NoHistory {
return false, false, "", fmt.Errorf("ephemeral and no_history are mutually exclusive")
}
if !current.StorageClass.IsValid() {
return false, false, "", fmt.Errorf("invalid storage class %q", current.StorageClass)
}
wispPlane := current.Ephemeral || current.NoHistory
if wispPlane && current.StorageClass != "" && current.StorageClass != StorageClassEphemeral {
return false, false, "", fmt.Errorf("storage class %q conflicts with ephemeral/no_history", current.StorageClass)
}
if !wispPlane && current.StorageClass == StorageClassEphemeral {
return false, false, "", fmt.Errorf("storage class ephemeral requires ephemeral or no_history")
}
if current.StorageClass == StorageClassUnversioned && mode != PersistenceModePersistent {
return false, false, "", fmt.Errorf("cannot move unversioned record to persistence mode %q", mode)
}
if persistenceMode(current) == mode {
return current.Ephemeral, current.NoHistory, current.StorageClass, nil
}
View on GitHub (pinned to 71377f2769)
Solutions
- Clear one of the two flags on the Issue before normalizing (keep ephemeral OR no_history, not both)
- Re-fetch the issue and apply a single persistence change
- Fix the update path so a persistence change sets one mode, replacing rather than OR-ing flags
Example fix
// before issue.Ephemeral = true; issue.NoHistory = true // after issue.Ephemeral = true; issue.NoHistory = false
Defensive patterns
Strategy: validation
Validate before calling
if issue.Ephemeral && issue.NoHistory {
return fmt.Errorf("issue %s has both ephemeral and no_history set", issue.ID)
} Type guard
func hasConflictingWispFlags(i Issue) bool {
return i.Ephemeral && i.NoHistory
} Try / catch
res, err := NormalizePersistenceMode(issue, mode)
if err != nil && strings.Contains(err.Error(), "mutually exclusive") {
// clear one flag and retry once
} Prevention
- Apply persistence changes through a single update path that replaces, not ORs, flags
- Re-fetch the issue before mutating persistence fields
- Add an invariant test that no stored issue has both flags set
When it happens
Trigger: Calling NormalizePersistenceMode with current.Ephemeral == true and current.NoHistory == true, e.g. an Issue built by code that sets both booleans, or a record mutated by two overlapping updates.
Common situations: Applying two independent updates (one enabling ephemeral, one enabling no_history) without reading the record in between; hand-edited JSONL export; copying flags from two different commands.
Related errors
- storage class %q conflicts with ephemeral/no_history
- storage class ephemeral requires ephemeral or no_history
- cannot move unversioned record to persistence mode %q
- persist sync.remote to config.yaml: %w
- failed to persist sync.remote to config.yaml: %w
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/37b8782806e457e5.
Report an issue: GitHub.