JuliusBrussee/caveman · error

session-value artifact action identity or order invalid

Error message

session-value artifact action identity or order invalid

What it means

Each action's ActionID must equal the deterministic CandidateActionID derived from its Provider/Model/Effort triple, must be non-empty, and the Actions array must be strictly ascending by ActionID. Position-aligned action lookup and duplicate prevention depend on this canonical identity and ordering.

Source

Thrown at proxy/routing/session_value.go:210

		return errors.New("session-value artifact has no features or actions")
	}
	for i, spec := range artifact.FeatureSpecs {
		if spec.Name != featureNames[i] || (i > 0 && artifact.FeatureSpecs[i-1].Name >= spec.Name) {
			return errors.New("session-value artifact feature vocabulary or order invalid")
		}
		if spec.Name == "turn_index" && !spec.Required {
			return errors.New("session-value artifact must require turn_index")
		}
		if !finite(spec.Mean) || !finite(spec.Scale) || spec.Scale <= 0 || !finite(spec.Min) || !finite(spec.Max) || spec.Min < 0 || spec.Max < spec.Min {
			return fmt.Errorf("session-value feature %q bounds invalid", spec.Name)
		}
	}
	seenActions := map[string]struct{}{}
	artifactPool := make([]Candidate, 0, len(artifact.Actions))
	for i, action := range artifact.Actions {
		wantID := CandidateActionID(Candidate{Provider: action.Provider, Model: action.Model, Effort: action.Effort})
		if wantID == "" || action.ActionID != wantID || (i > 0 && artifact.Actions[i-1].ActionID >= action.ActionID) {
			return errors.New("session-value artifact action identity or order invalid")
		}
		if _, duplicate := seenActions[action.ActionID]; duplicate {
			return errors.New("session-value artifact duplicate action")
		}
		seenActions[action.ActionID] = struct{}{}
		artifactPool = append(artifactPool, Candidate{Provider: action.Provider, Model: action.Model, Effort: action.Effort})
		if !finite(action.RewardEffectiveSampleSize) || action.RewardEffectiveSampleSize <= 0 ||
			!finite(action.CostEffectiveSampleSize) || action.CostEffectiveSampleSize <= 0 ||
			!probabilityMetric(action.QualityCalibrationError) || !probabilityMetric(action.RewardBrierScore) ||
			!finite(action.RewardCalibrationIntercept) || !finite(action.RewardCalibrationSlope) || action.RewardCalibrationSlope <= 0 || action.RewardCalibrationSlope > 10 {
			return errors.New("session-value artifact action evidence invalid")
		}
		for _, model := range []SessionValueLinearModel{action.RewardLogit, action.ResidualFutureCostLog1P, action.CorrectiveTurnsLog1P, action.EscalationLogit} {
			if err := validateSessionValueLinearModel(model, len(artifact.FeatureSpecs)); err != nil {
				return err
			}
		}
	}

View on GitHub (pinned to 766dce6b13)

Solutions

  1. Compute IDs during generation exactly as CandidateActionID(Candidate{Provider: a.Provider, Model: a.Model, Effort: a.Effort}) and store that value.
  2. Sort actions by ActionID ascending and de-duplicate the candidate pool before training (this also keeps CandidatePoolHash correct).
  3. Regenerate the artifact whenever the provider/model/effort roster changes.

Example fix

// before
{"action_id": "anthropic/claude-x", "provider": "anthropic", "model": "claude-x", "effort": "high"} // id omits effort

// after
action.ActionID = routing.CandidateActionID(routing.Candidate{Provider: action.Provider, Model: action.Model, Effort: action.Effort})
sort.Slice(actions, func(i, j int) bool { return actions[i].ActionID < actions[j].ActionID })
Defensive patterns

Strategy: validation

Validate before calling

// Before validation: recompute canonical IDs and verify strict ordering.
func actionIDsCanonical(actions []routing.SessionValueActionModel) bool {
	var prev string
	for i, a := range actions {
		want := routing.CandidateActionID(routing.Candidate{Provider: a.Provider, Model: a.Model, Effort: a.Effort})
		if want == "" || a.ActionID != want || (i > 0 && prev >= a.ActionID) {
			return false
		}
		prev = a.ActionID
	}
	return true
}

Prevention

When it happens

Trigger: An ActionID hand-set or computed by a different rule (for example provider:model without the effort tier); duplicate candidates surviving into the pool; actions listed in unsorted order; an empty provider or model string making the derived ID empty.

Common situations: Trainer and router disagreeing on the ID format; new effort tiers added without regenerating; a re-serialization step that reorders the actions array; candidate pools built by concatenating sources without dedup.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@766dce6b13 (2026-08-18). Data as JSON: /api/errors/bb96099e40352b8d. Report an issue: GitHub.