gastownhall/beads · error

storage class %q conflicts with ephemeral/no_history

Error message

storage class %q conflicts with ephemeral/no_history

What it means

A record on the wisp plane (Ephemeral or NoHistory set) may only carry the 'ephemeral' storage class (or none). Any other storage class contradicts the wisp flags, so normalization rejects it.

Source

Thrown at internal/types/types.go:969

// 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
	}

	switch mode {
	case PersistenceModePersistent:
		storageClass := current.StorageClass
		if storageClass == StorageClassEphemeral {
			storageClass = ""
		}
		return false, false, storageClass, nil

View on GitHub (pinned to 71377f2769)

Solutions

  1. Clear the storage class (or set it to StorageClassEphemeral) when moving an issue to the ephemeral/no_history plane
  2. Run the issue through NormalizePersistenceMode before writing so conflicts are resolved in one place
  3. Fix the update path that toggles wisp flags without updating StorageClass

Example fix

// before
issue.Ephemeral = true; issue.StorageClass = StorageClassUnversioned
// after
issue.Ephemeral = true; issue.StorageClass = StorageClassEphemeral
Defensive patterns

Strategy: validation

Validate before calling

if (issue.Ephemeral || issue.NoHistory) && issue.StorageClass != "" && issue.StorageClass != StorageClassEphemeral {
    return fmt.Errorf("wisp issue %s carries incompatible class %q", issue.ID, issue.StorageClass)
}

Type guard

func wispClassConflict(i Issue) bool {
    return (i.Ephemeral || i.NoHistory) && i.StorageClass != "" && i.StorageClass != StorageClassEphemeral
}

Try / catch

res, err := NormalizePersistenceMode(issue, mode)
if err != nil && strings.Contains(err.Error(), "conflicts with ephemeral/no_history") {
    issue.StorageClass = StorageClassEphemeral
    res, err = NormalizePersistenceMode(issue, mode)
}

Prevention

When it happens

Trigger: Calling NormalizePersistenceMode with current.Ephemeral || current.NoHistory true while current.StorageClass is a non-empty class other than StorageClassEphemeral (e.g. unversioned or versioned).

Common situations: Promoting/demoting a record between planes while a stale storage class from its previous life remains set; code that sets ephemeral but forgets to clear storage class; merging records from different planes.

Related errors


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