gastownhall/beads · error

storage class ephemeral requires ephemeral or no_history

Error message

storage class ephemeral requires ephemeral or no_history

What it means

The ephemeral storage class only makes sense for records on the wisp plane. A persistent, versioned record (Ephemeral and NoHistory false) cannot claim storage class 'ephemeral', so normalization rejects the state.

Source

Thrown at internal/types/types.go:972

// 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
	case PersistenceModeEphemeral:
		storageClass := current.StorageClass
		if storageClass == StorageClassVersioned {

View on GitHub (pinned to 71377f2769)

Solutions

  1. Clear StorageClass (letting it default to versioned) when promoting the record out of the wisp plane
  2. Set StorageClass to a persistent-appropriate class (versioned/unversioned) along with clearing the wisp flags
  3. Always apply plane changes through NormalizePersistenceMode rather than setting fields directly

Example fix

// before
issue.Ephemeral = false; issue.StorageClass = StorageClassEphemeral
// after
issue.Ephemeral = false; issue.StorageClass = ""
Defensive patterns

Strategy: validation

Validate before calling

if !issue.Ephemeral && !issue.NoHistory && issue.StorageClass == StorageClassEphemeral {
    return fmt.Errorf("persistent issue %s has ephemeral storage class", issue.ID)
}

Type guard

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

Try / catch

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

Prevention

When it happens

Trigger: Calling NormalizePersistenceMode with current.Ephemeral == false, current.NoHistory == false, and current.StorageClass == StorageClassEphemeral — e.g. a record demoted to ephemeral earlier and then flipped back to persistent without clearing the class.

Common situations: Promoting a wisp back to a persistent record while its ephemeral marker was left behind; manual edits to exported data; code paths that clear Ephemeral but not StorageClass.

Related errors


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