{"record":{"id":"2ca4108d849418ec","repo":"JuliusBrussee/caveman","slug":"ccr-typed-object-id-collision-2ca410","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_wasm.go","lineNumber":124,"sourceCode":"\treturn append([]byte(nil), r.metadata...), nil\n}\n\nfunc cloneObject(obj Object) Object {\n\tobj.Dependencies = append([]string(nil), obj.Dependencies...)\n\tobj.Data = bytes.Clone(obj.Data)\n\treturn obj\n}\n\nfunc (s *Store) PutObject(input Object) (string, error) {\n\tobj, err := prepareObject(input)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\ts.mu.Lock()\n\tdefer s.mu.Unlock()\n\tif existing, exists := s.objects[obj.ID]; exists {\n\t\tif !sameImmutableObject(existing, obj) {\n\t\t\treturn \"\", errors.New(\"ccr: typed object id collision\")\n\t\t}\n\t} else {\n\t\ts.objects[obj.ID] = cloneObject(obj)\n\t}\n\treturn obj.ID, nil\n}\n\nfunc (s *Store) GetObject(id string) (Object, error) {\n\ts.mu.Lock()\n\tdefer s.mu.Unlock()\n\tobj, ok := s.objects[id]\n\tif !ok {\n\t\treturn Object{}, ErrNotFound\n\t}\n\treturn cloneObject(obj), nil\n}\n\nfunc (s *Store) SetObjectCurrentness(id string, currentness Currentness) error {","sourceCodeStart":106,"sourceCodeEnd":142,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/27d5a3981a347890211bb1bf2439e5c821a63bc9/engine/ccr/store_wasm.go#L106-L142","documentation":"In-memory/wasm CCR store: PutObject checks the in-memory map by derived ID; if the ID already exists and sameImmutableObject reports the stored clone differs from the incoming object, the put is refused as a typed-object ID collision. Same semantics as the SQLite store, just against the map. It indicates two different objects claiming one content-addressed identity.","triggerScenarios":"Two PutObject calls with identical (Type, SessionID, Source, RepositoryState, ContentHash) but differing immutable metadata (e.g. Dependencies or byte lengths); re-putting after changing fields the store treats as immutable without changing the content hash.","commonSituations":"Browser/wasm client re-submitting an object whose metadata was locally augmented between attempts; test code mutating shared object structs between puts.","solutions":["Make immutable fields deterministic: derive them from the same inputs every time, never mutate them between puts","Change the Data (and thus ContentHash and ID) when the object's content genuinely changes","Diff existing := store.GetObject(obj.ID) against the incoming object to locate the divergent field"],"exampleFix":"// before\nobjA := ccr.Object{Type: t, SessionID: s, Source: src, Data: d, StoredByteLength: 10}\nobjB := ccr.Object{Type: t, SessionID: s, Source: src, Data: d, StoredByteLength: 20} // same identity, different length\nstore.PutObject(objA); store.PutObject(objB) // collision\n\n// after\nobjB.StoredByteLength = 0 // let the store derive it; identical inputs -> identical immutable object","handlingStrategy":"validation","validationCode":"if prev, ok := s.objectsByID[obj.ID]; ok && !sameImmutable(prev, obj) {\n    obj.ContentHash = \"\"\n}","typeGuard":null,"tryCatchPattern":"if _, err := store.PutObject(obj); err != nil {\n    if strings.Contains(err.Error(), \"id collision\") {\n        // reconcile: rebuild object from identical inputs\n    }\n}","preventionTips":["Never mutate shared Object structs between puts (clone first)","Let the store derive lengths and hash where possible","Keep object construction in one code path per type"],"tags":["ccr","go","wasm","id-collision","in-memory"],"backgroundTag":null,"analyzedSha":"27d5a3981a347890211bb1bf2439e5c821a63bc9","analyzedAt":"2026-08-15T09:26:11.751Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}