gastownhall/beads · error

invalid storage class %q

Error message

invalid storage class %q

What it means

NormalizePersistenceMode validates the issue's current StorageClass against the canonical valid list before applying a mode change. An unknown or corrupted class value aborts the operation to avoid writing inconsistent storage metadata.

Source

Thrown at internal/types/types.go:965

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
	}

	switch mode {
	case PersistenceModePersistent:
		storageClass := current.StorageClass

View on GitHub (pinned to 71377f2769)

Solutions

  1. Set StorageClass to one of the valid constants (versioned/ephemeral/unversioned or empty where allowed)
  2. Parse user input through ParseStorageClass instead of direct casting
  3. Repair or re-export the record if its stored class is corrupt/legacy

Example fix

// before
issue.StorageClass = StorageClass("versioed")
// after
issue.StorageClass = StorageClassVersioned
Defensive patterns

Strategy: validation

Validate before calling

if issue.StorageClass != "" && !issue.StorageClass.IsValid() {
    return fmt.Errorf("issue %s has invalid storage class %q", issue.ID, issue.StorageClass)
}

Type guard

func hasValidStorageClass(i Issue) bool {
    return i.StorageClass == "" || i.StorageClass.IsValid()
}

Try / catch

res, err := NormalizePersistenceMode(issue, mode)
if err != nil && strings.Contains(err.Error(), "invalid storage class") {
    // repair or reset StorageClass before retrying
}

Prevention

When it happens

Trigger: Calling NormalizePersistenceMode when current.StorageClass holds a value not in validStorageClasses (fails StorageClass.IsValid()) — e.g. "" when a class was required, a typo like "versioed", or data written by an incompatible version.

Common situations: Hand-edited or externally migrated JSONL records; database rows from an older schema using a retired class name; code assigning raw strings to StorageClass instead of constants.

Related errors


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