{"record":{"id":"5a914f998d3278e1","repo":"JuliusBrussee/caveman","slug":"session-value-artifact-has-no-features-or-actions","errorCode":null,"errorMessage":"session-value artifact has no features or actions","messagePattern":"session-value artifact has no features or actions","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"proxy/routing/session_value.go","lineNumber":192,"sourceCode":"\tif !validCompactPoolHash(artifact.CandidatePoolHash) || artifact.CandidatePoolHash != candidatePoolHash {\n\t\treturn errors.New(\"session-value artifact candidate pool mismatch\")\n\t}\n\tif !validSHA256Ref(artifact.TrainingManifestHash) || strings.TrimSpace(artifact.TrainingExtractor) == \"\" || strings.TrimSpace(artifact.OutcomeContractVersion) == \"\" {\n\t\treturn errors.New(\"session-value artifact training lineage invalid\")\n\t}\n\tif artifact.ValidFrom.IsZero() || artifact.ValidUntil.IsZero() || !artifact.ValidUntil.After(artifact.ValidFrom) || now.Before(artifact.ValidFrom) || !now.Before(artifact.ValidUntil) {\n\t\treturn errors.New(\"session-value artifact outside validity window\")\n\t}\n\tif artifact.RollbackParentHash != \"\" && (!validSHA256Ref(artifact.RollbackParentHash) || artifact.RollbackParentHash == artifact.ArtifactHash) {\n\t\treturn errors.New(\"session-value artifact rollback lineage invalid\")\n\t}\n\tif !finite(artifact.QualityUncertaintyZ) || artifact.QualityUncertaintyZ <= 0 || artifact.QualityUncertaintyZ > 5 ||\n\t\t!finite(artifact.MaxInversePropensity) || artifact.MaxInversePropensity < 1 || artifact.MaxInversePropensity > 100 {\n\t\treturn errors.New(\"session-value artifact confidence policy invalid\")\n\t}\n\tfeatureNames := SessionValueFeatureNames()\n\tif len(artifact.FeatureSpecs) != len(featureNames) || len(artifact.Actions) == 0 {\n\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\")","sourceCodeStart":174,"sourceCodeEnd":210,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/766dce6b1394ebb56a3090748d5a0240a5aefb36/proxy/routing/session_value.go#L174-L210","documentation":"The artifact must carry exactly one FeatureSpec per name in the stable metadata vocabulary returned by SessionValueFeatureNames() (21 sorted names today, from audio to user_turns) plus at least one action. A FeatureSpecs array of the wrong length (features added or removed since training) or an empty Actions list fails here, because the evaluator aligns models by position.","triggerScenarios":"The router's feature vocabulary changed (feature added/removed) while the artifact was trained on the old set; an artifact with specs but zero Actions; JSON arrays emptied or truncated by generation tooling.","commonSituations":"Router upgrade introducing a new metadata feature; a trainer that filters out features it considers unused; an artifact template whose arrays default to empty.","solutions":["Regenerate the artifact against the current router's SessionValueFeatureNames() so lengths match.","Lock trainer and router to the same vocabulary version and retrain whenever the vocabulary changes.","Ensure at least one candidate action (provider/model/effort) is included when the artifact is produced."],"exampleFix":"// before\nartifact.FeatureSpecs = trainOnLegacyVocabulary() // 20 specs, router expects 21\nartifact.Actions = nil\n\n// after\nartifact.FeatureSpecs = specsFor(routing.SessionValueFeatureNames()) // exactly 21, current names\nartifact.Actions = trainActions(currentCandidatePool())             // >= 1 action","handlingStrategy":"validation","validationCode":"// Before validation: compare collection sizes against the live vocabulary.\nwanted := routing.SessionValueFeatureNames() // authoritative length and names\nif len(artifact.FeatureSpecs) != len(wanted) {\n\treturn fmt.Errorf(\"artifact has %d feature specs, router vocabulary has %d; regenerate after vocabulary changes\",\n\t\tlen(artifact.FeatureSpecs), len(wanted))\n}\nif len(artifact.Actions) == 0 {\n\treturn errors.New(\"artifact has no actions; not loadable\")\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Regenerate artifacts whenever the router's feature vocabulary changes; treat vocabulary changes as breaking.","Pin trainer and router versions together in one release unit.","Refuse to train against an empty candidate pool so Actions is never empty."],"tags":["routing","features","vocabulary","artifact"],"backgroundTag":"missing-required-collection","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"}