{"record":{"id":"bb96099e40352b8d","repo":"JuliusBrussee/caveman","slug":"session-value-artifact-action-identity-or-order-in","errorCode":null,"errorMessage":"session-value artifact action identity or order invalid","messagePattern":"session-value artifact action identity or order invalid","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"proxy/routing/session_value.go","lineNumber":210,"sourceCode":"\t\treturn errors.New(\"session-value artifact has no features or actions\")\n\t}\n\tfor i, spec := range artifact.FeatureSpecs {\n\t\tif spec.Name != featureNames[i] || (i > 0 && artifact.FeatureSpecs[i-1].Name >= spec.Name) {\n\t\t\treturn errors.New(\"session-value artifact feature vocabulary or order invalid\")\n\t\t}\n\t\tif spec.Name == \"turn_index\" && !spec.Required {\n\t\t\treturn errors.New(\"session-value artifact must require turn_index\")\n\t\t}\n\t\tif !finite(spec.Mean) || !finite(spec.Scale) || spec.Scale <= 0 || !finite(spec.Min) || !finite(spec.Max) || spec.Min < 0 || spec.Max < spec.Min {\n\t\t\treturn fmt.Errorf(\"session-value feature %q bounds invalid\", spec.Name)\n\t\t}\n\t}\n\tseenActions := map[string]struct{}{}\n\tartifactPool := make([]Candidate, 0, len(artifact.Actions))\n\tfor i, action := range artifact.Actions {\n\t\twantID := CandidateActionID(Candidate{Provider: action.Provider, Model: action.Model, Effort: action.Effort})\n\t\tif wantID == \"\" || action.ActionID != wantID || (i > 0 && artifact.Actions[i-1].ActionID >= action.ActionID) {\n\t\t\treturn errors.New(\"session-value artifact action identity or order invalid\")\n\t\t}\n\t\tif _, duplicate := seenActions[action.ActionID]; duplicate {\n\t\t\treturn errors.New(\"session-value artifact duplicate action\")\n\t\t}\n\t\tseenActions[action.ActionID] = struct{}{}\n\t\tartifactPool = append(artifactPool, Candidate{Provider: action.Provider, Model: action.Model, Effort: action.Effort})\n\t\tif !finite(action.RewardEffectiveSampleSize) || action.RewardEffectiveSampleSize <= 0 ||\n\t\t\t!finite(action.CostEffectiveSampleSize) || action.CostEffectiveSampleSize <= 0 ||\n\t\t\t!probabilityMetric(action.QualityCalibrationError) || !probabilityMetric(action.RewardBrierScore) ||\n\t\t\t!finite(action.RewardCalibrationIntercept) || !finite(action.RewardCalibrationSlope) || action.RewardCalibrationSlope <= 0 || action.RewardCalibrationSlope > 10 {\n\t\t\treturn errors.New(\"session-value artifact action evidence invalid\")\n\t\t}\n\t\tfor _, model := range []SessionValueLinearModel{action.RewardLogit, action.ResidualFutureCostLog1P, action.CorrectiveTurnsLog1P, action.EscalationLogit} {\n\t\t\tif err := validateSessionValueLinearModel(model, len(artifact.FeatureSpecs)); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}","sourceCodeStart":192,"sourceCodeEnd":228,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/766dce6b1394ebb56a3090748d5a0240a5aefb36/proxy/routing/session_value.go#L192-L228","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\n{\"action_id\": \"anthropic/claude-x\", \"provider\": \"anthropic\", \"model\": \"claude-x\", \"effort\": \"high\"} // id omits effort\n\n// after\naction.ActionID = routing.CandidateActionID(routing.Candidate{Provider: action.Provider, Model: action.Model, Effort: action.Effort})\nsort.Slice(actions, func(i, j int) bool { return actions[i].ActionID < actions[j].ActionID })","handlingStrategy":"validation","validationCode":"// Before validation: recompute canonical IDs and verify strict ordering.\nfunc actionIDsCanonical(actions []routing.SessionValueActionModel) bool {\n\tvar prev string\n\tfor i, a := range actions {\n\t\twant := routing.CandidateActionID(routing.Candidate{Provider: a.Provider, Model: a.Model, Effort: a.Effort})\n\t\tif want == \"\" || a.ActionID != want || (i > 0 && prev >= a.ActionID) {\n\t\t\treturn false\n\t\t}\n\t\tprev = a.ActionID\n\t}\n\treturn true\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["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."],"tags":["routing","actions","ordering","identity","artifact"],"backgroundTag":"action-id-mismatch","analyzedSha":"766dce6b1394ebb56a3090748d5a0240a5aefb36","analyzedAt":"2026-08-18T03:14:35.516Z","contentChangedAt":"2026-08-18T03:14:35.516Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}