{"record":{"id":"c6dc427203de5b7c","repo":"JuliusBrussee/caveman","slug":"ccr-typed-object-id-collision","errorCode":null,"errorMessage":"ccr: typed object id collision","messagePattern":"ccr: typed object id collision","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"engine/ccr/store_sqlite.go","lineNumber":542,"sourceCode":"\t\tobj.Lifecycle, string(deps), obj.OriginalByteLength, obj.StoredByteLength, obj.Data,\n\t)\n\tif err != nil {\n\t\tif isFull(err) {\n\t\t\treturn \"\", fmt.Errorf(\"ccr typed object put: %w\", ErrBudgetExceeded)\n\t\t}\n\t\treturn \"\", fmt.Errorf(\"ccr typed object put: %w\", err)\n\t}\n\tinserted, err := result.RowsAffected()\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"ccr typed object put rows: %w\", err)\n\t}\n\tif inserted == 0 {\n\t\texisting, err := s.GetObject(obj.ID)\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"ccr typed object collision lookup: %w\", err)\n\t\t}\n\t\tif !sameImmutableObject(existing, obj) {\n\t\t\treturn \"\", errors.New(\"ccr: typed object id collision\")\n\t\t}\n\t}\n\treturn obj.ID, nil\n}\n\nfunc scanObject(scanner interface{ Scan(...any) error }) (Object, error) {\n\tvar obj Object\n\tvar created, deps string\n\tif err := scanner.Scan(\n\t\t&obj.ID, &obj.Type, &obj.ContentHash, &obj.Source, &created, &obj.RepositoryState,\n\t\t&obj.SessionID, &obj.TransformVersion, &obj.Currentness, &obj.Lifecycle, &deps,\n\t\t&obj.OriginalByteLength, &obj.StoredByteLength, &obj.Data,\n\t); err != nil {\n\t\treturn Object{}, err\n\t}\n\tparsed, err := time.Parse(time.RFC3339Nano, created)\n\tif err != nil {\n\t\treturn Object{}, fmt.Errorf(\"ccr typed object created_at: %w\", err)","sourceCodeStart":524,"sourceCodeEnd":560,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/27d5a3981a347890211bb1bf2439e5c821a63bc9/engine/ccr/store_sqlite.go#L524-L560","documentation":"PutObject derives an object's ID from (Type, SessionID, Source, RepositoryState, ContentHash); the SQLite insert is an upsert that is a no-op when the ID already exists. If a row with that ID exists but sameImmutableObject finds the stored row differs from the incoming object in fields PutObject promises never to change, the write is refused as an ID collision — evidence of two different payloads mapping to the same identity.","triggerScenarios":"Putting an object with the same Type/Session/Source/RepositoryState/ContentHash but different Lifecycle, Currentness, Dependencies, or byte lengths than the row already in SQLite; a ContentHash computed over different bytes colliding with an existing row's identity inputs; two writers racing with slightly different metadata for the 'same' object.","commonSituations":"Version skew between components that add or mutate metadata fields between writes; re-putting an object after mutating its Data but forgetting that ContentHash (part of identity) stayed stale; a copied DB where rows were edited.","solutions":["Ensure the identity inputs (Type, SessionID, Source, RepositoryState, ContentHash) and immutable fields are deterministic functions of the same underlying data","If the object genuinely changed, change its ContentHash (i.e. its Data) so it gets a new ID rather than colliding","Inspect the existing row with GetObject(obj.ID) and diff it against your incoming object to find the mismatched field"],"exampleFix":"// before\nobj.Data = append(obj.Data, extra...) // Data mutated but identity reused via stale ContentHash\nstore.PutObject(obj)\n\n// after\nobj.Data = append(obj.Data, extra...)\nobj.ContentHash = \"\" // force re-derivation from the new Data -> new ID\nstore.PutObject(obj)","handlingStrategy":"validation","validationCode":"existing, err := store.GetObject(obj.ID)\nif err == nil && !sameImmutable(existing, obj) {\n    obj.ContentHash = \"\" // derive fresh identity from current Data\n}","typeGuard":null,"tryCatchPattern":"id, err := store.PutObject(obj)\nif err != nil && strings.Contains(err.Error(), \"id collision\") {\n    existing, gerr := store.GetObject(obj.ID)\n    // diff existing vs obj, then fix identity inputs or content\n}","preventionTips":["Derive all identity and immutable fields from one deterministic source","Clear ContentHash whenever Data changes","Never edit CCR rows out-of-band"],"tags":["ccr","go","sqlite","id-collision","content-addressing"],"backgroundTag":null,"analyzedSha":"27d5a3981a347890211bb1bf2439e5c821a63bc9","analyzedAt":"2026-08-15T09:26:11.751Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}