JuliusBrussee/caveman · error

ccr: unknown lifecycle

Error message

ccr: unknown lifecycle %q

What it means

prepareObject (engine/ccr/store.go:122) validates the Object.Lifecycle field before persisting a CCR object via putObject/PutObject. The store only accepts the four enumerated lifecycle values: Hot, Warm, Cold, and LifecycleArchived. Empty strings are defaulted to Hot, but any other non-empty value is rejected with this error. This is a fail-closed enum guard ensuring tiering metadata in the SQLite recovery store is always well-defined.

Solutions

  1. Set Lifecycle to one of the exported constants: Hot, Warm, Cold, or LifecycleArchived (or leave it empty to default to Hot).
  2. Normalize the incoming value before calling PutObject — trim whitespace and match case exactly against the constants.
  3. If migrating data from another enum vocabulary, map old values to the four supported lifecycle constants before storage.
  4. Check for typos or custom values (e.g. "archive" instead of LifecycleArchived) in the code path that builds the Object.

Example fix

// before
obj := ccr.Object{Type: ccr.ObjectEvidenceBundle, SessionID: "s1", Lifecycle: ccr.Lifecycle("hot")}
store.PutObject(ctx, obj) // error: ccr: unknown lifecycle "hot"

// after
obj := ccr.Object{Type: ccr.ObjectEvidenceBundle, SessionID: "s1", Lifecycle: ccr.Hot}
store.PutObject(ctx, obj)
Defensive patterns

Strategy: validation

Validate before calling

func validLifecycle(l ccr.Lifecycle) bool {
	return l == "" || l == ccr.Hot || l == ccr.Warm || l == ccr.Cold || l == ccr.LifecycleArchived
}
if !validLifecycle(obj.Lifecycle) {
	return fmt.Errorf("refusing to store object with lifecycle %q", obj.Lifecycle)
}

Type guard

func isValidLifecycle(v ccr.Lifecycle) bool {
	switch v {
	case "", ccr.Hot, ccr.Warm, ccr.Cold, ccr.LifecycleArchived:
		return true
	}
	return false
}

Try / catch

obj, err := store.PutObject(ctx, o)
if err != nil {
	var unknown *fmt.Errorf
	if strings.Contains(err.Error(), "unknown lifecycle") {
		log.Fatalf("invalid lifecycle %q: use Hot|Warm|Cold|LifecycleArchived", o.Lifecycle)
	}
	return err
}

Prevention

When it happens

Trigger: Calling PutObject (directly or via putObject) with obj.Lifecycle set to a value that is not exactly Hot, Warm, Cold, or LifecycleArchived — e.g. a hand-rolled string cast like Lifecycle("hot"), Lifecycle("archive"), a typo such as Lifecycle("Hott"), or a lifecycle value imported from another subsystem's vocabulary.

Common situations: Developers constructing Object structs manually instead of through store helpers, migrating records whose lifecycle was persisted with an older or different enum spelling, or deserializing lifecycle from config/JSON where case differs ("Hot" vs "hot"). Also occurs when a custom tier value from a fork or newer version is written into an older store build.

Understand the failure class

Background: Invalid enum value errors: "Unknown type", "Invalid scope", "must be one of" — when a string is not on the library's allowed list — this error's family across 23 libraries.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20). Data as JSON: /api/errors/66d6362fd9355b0a. Report an issue: GitHub.

Appendix: source

Thrown at engine/ccr/store.go:122

func prepareObject(obj Object) (Object, error) {
	if _, ok := objectTypes[obj.Type]; !ok {
		return Object{}, fmt.Errorf("ccr: unknown object type %q", obj.Type)
	}
	if strings.TrimSpace(obj.SessionID) == "" {
		return Object{}, errors.New("ccr: typed object session_id is required")
	}
	if obj.Currentness == "" {
		obj.Currentness = Current
	}
	if obj.Currentness != Current && obj.Currentness != Stale && obj.Currentness != Archived {
		return Object{}, fmt.Errorf("ccr: unknown currentness %q", obj.Currentness)
	}
	if obj.Lifecycle == "" {
		obj.Lifecycle = Hot
	}
	if obj.Lifecycle != Hot && obj.Lifecycle != Warm && obj.Lifecycle != Cold && obj.Lifecycle != LifecycleArchived {
		return Object{}, fmt.Errorf("ccr: unknown lifecycle %q", obj.Lifecycle)
	}
	if obj.CreatedAt.IsZero() {
		obj.CreatedAt = time.Now().UTC()
	} else {
		obj.CreatedAt = obj.CreatedAt.UTC()
	}
	dataSum := sha256.Sum256(obj.Data)
	computedHash := "sha256:" + hex.EncodeToString(dataSum[:])
	if obj.ContentHash == "" {
		obj.ContentHash = computedHash
	} else if obj.ContentHash != computedHash {
		return Object{}, errors.New("ccr: typed object content_hash does not match data")
	}
	if obj.OriginalByteLength == 0 {
		obj.OriginalByteLength = len(obj.Data)
	}
	if obj.StoredByteLength == 0 {
		obj.StoredByteLength = len(obj.Data)

View on GitHub (pinned to 3ee70a1026)