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
- Compute IDs during generation exactly as CandidateActionID(Candidate{Provider: a.Provider, Model: a.Model, Effort: a.Effort}) and store that value.
- Sort actions by ActionID ascending and de-duplicate the candidate pool before training (this also keeps CandidatePoolHash correct).
- 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
- Generate ActionID only via CandidateActionID; never hand-write ID strings.
- Sort and dedupe the candidate pool before training so ordering and uniqueness are guaranteed.
- Regenerate artifacts on every roster change so IDs and CandidatePoolHash stay in sync.
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
- session-value artifact feature vocabulary or order invalid
- session-value artifact schema mismatch
- session-value artifact tenant scope mismatch
- session-value artifact version identity invalid
- session-value artifact candidate pool mismatch
AI-assisted analysis of JuliusBrussee/caveman@766dce6b13 (2026-08-18).
Data as JSON: /api/errors/bb96099e40352b8d.
Report an issue: GitHub.