JuliusBrussee/caveman · error

native runtime: invalid decision record

Error message

native runtime: invalid decision record

What it means

After fetching the TaskDecision object from CCR, ExplainDecision decodes its Data as JSON and requires schema == "caveman.native.decision.v1", decision_id equal to the requested id, and non-empty action and reason. A record failing any of these is corrupt or was written by an incompatible writer, so it refuses to explain it.

Source

Thrown at proxy/internal/nativeruntime/explain.go:58

	}
	var record struct {
		Schema               string         `json:"schema"`
		DecisionID           string         `json:"decision_id"`
		TimestampMS          int64          `json:"timestamp_ms"`
		Action               string         `json:"action"`
		Reason               string         `json:"reason"`
		InputBasis           map[string]any `json:"input_basis"`
		AlternativesRejected []string       `json:"alternatives_rejected"`
		TaskStateBefore      string         `json:"task_state_before"`
		TaskStateAfter       string         `json:"task_state_after"`
		Currentness          string         `json:"currentness"`
		RecoveryRef          string         `json:"recovery_ref"`
	}
	if err := json.Unmarshal(object.Data, &record); err != nil {
		return DecisionExplanation{}, fmt.Errorf("native runtime: decode decision: %w", err)
	}
	if record.Schema != "caveman.native.decision.v1" || record.DecisionID != decisionID || record.Action == "" || record.Reason == "" {
		return DecisionExplanation{}, errors.New("native runtime: invalid decision record")
	}
	return DecisionExplanation{
		Schema:               WhySchema,
		DecisionID:           record.DecisionID,
		SessionID:            object.SessionID,
		TimestampMS:          record.TimestampMS,
		Action:               record.Action,
		Reason:               record.Reason,
		InputBasis:           record.InputBasis,
		AlternativesRejected: record.AlternativesRejected,
		TaskStateBefore:      record.TaskStateBefore,
		TaskStateAfter:       record.TaskStateAfter,
		Currentness:          record.Currentness,
		RecoveryRef:          record.RecoveryRef,
	}, nil
}

View on GitHub (pinned to 27d5a3981a)

Solutions

  1. Re-record the decision with the current runtime so the payload carries schema "caveman.native.decision.v1" and the matching decision_id
  2. Verify the payload: unescape/pretty-print object.Data and check schema, decision_id, action, reason fields
  3. If reading legacy records, migrate or skip them instead of explaining

Example fix

// before
exp, err := nativeruntime.ExplainDecision(store, id) // legacy record with schema "caveman.native.decision.v0"

// after
// detect and skip legacy records:
obj, _ := store.FindTaskDecision(id)
if !bytes.Contains(obj.Data, []byte("caveman.native.decision.v1")) {
    return ErrLegacyDecision // migrate or skip
}
exp, err := nativeruntime.ExplainDecision(store, id)
Defensive patterns

Strategy: try-catch

Validate before calling

obj, err := store.FindTaskDecision(decisionID)
if err == nil && !bytes.Contains(obj.Data, []byte("caveman.native.decision.v1")) {
    // legacy record: migrate or skip instead of explaining
}

Try / catch

exp, err := nativeruntime.ExplainDecision(store, decisionID)
if err != nil {
    if strings.Contains(err.Error(), "invalid decision record") {
        // corrupt/legacy record: skip, migrate, or re-record
    }
    return err
}

Prevention

When it happens

Trigger: A decision record whose payload schema string differs (older/newer writer), whose embedded decision_id doesn't match the object it was found under, or with empty action/reason fields; truncated or hand-edited payload bytes that still decode as JSON.

Common situations: Upgrading the runtime across a schema change while reading old stored decisions; two writers recording decisions with different serializers; storage corruption or partial writes.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@27d5a3981a (2026-08-15). Data as JSON: /api/errors/0456fe785f76093d. Report an issue: GitHub.