{"record":{"id":"b9f44c2ae32494c7","repo":"JuliusBrussee/caveman","slug":"session-value-artifact-candidate-pool-mismatch","errorCode":null,"errorMessage":"session-value artifact candidate pool mismatch","messagePattern":"session-value artifact candidate pool mismatch","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"proxy/routing/session_value.go","lineNumber":175,"sourceCode":"\t\treturn SessionValuePolicyArtifact{}, err\n\t}\n\tartifact.ArtifactHash = hash\n\treturn artifact, nil\n}\n\nfunc ValidateSessionValueArtifact(artifact SessionValuePolicyArtifact, organizationID, projectID, candidatePoolHash string, now time.Time) error {\n\tif artifact.Schema != SessionValueArtifactSchema || artifact.StateSchemaVersion != SessionValueStateSchema {\n\t\treturn errors.New(\"session-value artifact schema mismatch\")\n\t}\n\tif strings.TrimSpace(artifact.OrganizationID) == \"\" || artifact.OrganizationID != strings.TrimSpace(organizationID) ||\n\t\tstrings.TrimSpace(artifact.ProjectID) == \"\" || artifact.ProjectID != strings.TrimSpace(projectID) {\n\t\treturn errors.New(\"session-value artifact tenant scope mismatch\")\n\t}\n\tif artifact.PolicyVersion <= 0 || artifact.RouterVersion != SessionValueRouterVersion || strings.TrimSpace(artifact.EstimatorVersion) == \"\" {\n\t\treturn errors.New(\"session-value artifact version identity invalid\")\n\t}\n\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}","sourceCodeStart":157,"sourceCodeEnd":193,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/766dce6b1394ebb56a3090748d5a0240a5aefb36/proxy/routing/session_value.go#L157-L193","documentation":"The artifact embeds CandidatePoolHash, a compact lowercase-hex hash of the exact candidate pool (the provider/model/effort set) it was trained on. Validation requires the hash to be well-formed (validCompactPoolHash: lowercase hex, length-validated) AND identical to the live pool hash passed in by the router — a policy trained on different candidates would score actions that no longer exist or miss new ones.","triggerScenarios":"The candidate roster changed (model added/removed, effort tier changed) after training without regenerating the artifact; the hash recorded in uppercase or with a prefix/suffix that breaks validCompactPoolHash; an artifact built against a different environment's pool being promoted.","commonSituations":"Model roster updates shipped without retraining the session-value policy; promoting an artifact from a staging pool to a production pool; trainer and router computing the pool hash over differently-ordered or differently-formatted candidates.","solutions":["Recompute the live candidate pool hash (over the same CandidateActionID set the router uses) and regenerate the artifact whenever the roster changes.","Gate deployment on pool-hash equality between router config and artifact so drift fails the pipeline, not production.","Check the hash format: compact lowercase hex exactly as validCompactPoolHash expects — no 'sha256:' prefix, no uppercase."],"exampleFix":"// before\n// artifact trained when pool was {claude-x, gpt-y}; gpt-z added to the router later\nartifact.CandidatePoolHash = oldPoolHash\n\n// after\n// after any roster change, retrain and restamp\nartifact.CandidatePoolHash = hashCurrentCandidatePool(routerCandidates())\nartifact = routing.SealSessionValueArtifact(artifact) // re-seal so ArtifactHash covers the new pool","handlingStrategy":"validation","validationCode":"// Before validation: recompute the live pool hash the way the router does and compare.\nlivePoolHash := hashCandidatePool(routerCandidatePool())\nif artifact.CandidatePoolHash != livePoolHash {\n\treturn fmt.Errorf(\"artifact pool hash %s does not match live pool %s; retrain after roster changes\",\n\t\tartifact.CandidatePoolHash, livePoolHash)\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Make candidate-roster changes trigger artifact retraining in the same change or pipeline.","Compute the pool hash over the same canonical CandidateActionID set on both trainer and router.","Use compact lowercase hex for pool hashes — the format check (validCompactPoolHash) is strict."],"tags":["routing","candidate-pool","hash","artifact"],"backgroundTag":"candidate-pool-hash-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"}