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.StorageClassView on GitHub (pinned to 71377f2769)
Solutions
- Set StorageClass to one of the valid constants (versioned/ephemeral/unversioned or empty where allowed)
- Parse user input through ParseStorageClass instead of direct casting
- 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
- Assign StorageClass only via constants or ParseStorageClass
- Validate exported/imported records before writing them back
- Check for retired class names when upgrading between versions
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
- invalid key %q: expected storage-class.<issue-type> (e.g. st
- config storage-class.%s: %w
- node %q: storage_class ephemeral and no_history are mutually
- applyGraph: node %q storage class (ephemeral=%t, no_history=
- %w: invalid persistence mode %q
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/e04201c6055e4842.
Report an issue: GitHub.