gastownhall/beads · error

cannot move unversioned record to persistence mode %q

Error message

cannot move unversioned record to persistence mode %q

What it means

An unversioned record is frozen in persistent mode: once StorageClass is 'unversioned', it cannot be moved to ephemeral or no_history (wisp modes). Normalization rejects any mode other than persistent for such records, as documented — an unversioned record may remain persistent but cannot move to a wisp mode.

Source

Thrown at internal/types/types.go:975

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 {
			storageClass = ""
		}
		return true, false, storageClass, nil

View on GitHub (pinned to 71377f2769)

Solutions

  1. Keep the record in persistent mode, or recreate the record as a new ephemeral/no_history issue and close the unversioned one
  2. Check StorageClass == StorageClassUnversioned before requesting a mode change and skip/branch accordingly
  3. Change the storage class policy for the record first if the workflow truly requires wisp conversion (if supported upstream)

Example fix

// before
NormalizePersistenceMode(issue, PersistenceModeEphemeral) // issue.StorageClass == unversioned
// after
if issue.StorageClass != StorageClassUnversioned {
    NormalizePersistenceMode(issue, PersistenceModeEphemeral)
}
Defensive patterns

Strategy: validation

Validate before calling

if issue.StorageClass == StorageClassUnversioned && mode != PersistenceModePersistent {
    return fmt.Errorf("issue %s is unversioned and cannot move to %s", issue.ID, mode)
}

Type guard

func canChangeMode(i Issue, m PersistenceMode) bool {
    return i.StorageClass != StorageClassUnversioned || m == PersistenceModePersistent
}

Try / catch

res, err := NormalizePersistenceMode(issue, mode)
if err != nil && strings.Contains(err.Error(), "cannot move unversioned record") {
    // keep persistent or recreate as new wisp issue
}

Prevention

When it happens

Trigger: Calling NormalizePersistenceMode on an Issue whose current.StorageClass == StorageClassUnversioned with mode != PersistenceModePersistent — e.g. attempting to demote it to ephemeral or no_history.

Common situations: Scripted cleanup jobs that sweep records into wisp modes without checking storage class; users trying to demote an unversioned issue via flags; automation assuming all records can become wisps.

Related errors


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