JuliusBrussee/caveman · error

native runtime: decode decision: %w

Error message

native runtime: decode decision: %w

What it means

nativeruntime.Explain decodes a stored decision object (object.Data) into a DecisionExplanation via json.Unmarshal. This error means the persisted bytes are not valid JSON — the record was truncated, corrupted, or written by an incompatible older schema. It wraps the encoding/json error so the exact offset/reason is preserved.

Source

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

	object, err := store.FindTaskDecision(decisionID)
	if err != nil {
		return DecisionExplanation{}, err
	}
	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. Retry the explain call for a different, recent decisionID to see if only one record is corrupt
  2. Inspect the stored object bytes for the decisionID to confirm truncation/corruption
  3. If the store is disposable, remove or rotate the native runtime store so fresh records are written in the current schema
  4. Check disk space and file permissions that could have caused the original short write
Defensive patterns

Strategy: try-catch

Validate before calling

func looksLikeJSON(b []byte) bool {
    return len(b) > 0 && b[0] == '{' && json.Valid(b)
}

Try / catch

exp, err := nativeruntime.Explain(ctx, decisionID)
if err != nil {
    if strings.Contains(err.Error(), "decode decision") {
        // one corrupt record: log decisionID, surface 'explanation unavailable',
        // keep serving other decisions — never retry the same corrupt blob
        return explainUnavailable(decisionID)
    }
    return err
}

Prevention

When it happens

Trigger: Calling the explain/why API for a decisionID whose stored object.Data is truncated (crash mid-write), hand-edited, or produced by a newer/older binary with a different serialization; passing a decision ID whose blob collides with non-JSON data.

Common situations: Upgrading the runtime after a schema change without migrating the store; disk-full during an earlier record append; querying a decision whose row was partially written.

Related errors


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