JuliusBrussee/caveman · error

ccr: typed object id collision

Error message

ccr: typed object id collision

What it means

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.

Source

Thrown at engine/ccr/store_wasm.go:124

	return append([]byte(nil), r.metadata...), nil
}

func cloneObject(obj Object) Object {
	obj.Dependencies = append([]string(nil), obj.Dependencies...)
	obj.Data = bytes.Clone(obj.Data)
	return obj
}

func (s *Store) PutObject(input Object) (string, error) {
	obj, err := prepareObject(input)
	if err != nil {
		return "", err
	}
	s.mu.Lock()
	defer s.mu.Unlock()
	if existing, exists := s.objects[obj.ID]; exists {
		if !sameImmutableObject(existing, obj) {
			return "", errors.New("ccr: typed object id collision")
		}
	} else {
		s.objects[obj.ID] = cloneObject(obj)
	}
	return obj.ID, nil
}

func (s *Store) GetObject(id string) (Object, error) {
	s.mu.Lock()
	defer s.mu.Unlock()
	obj, ok := s.objects[id]
	if !ok {
		return Object{}, ErrNotFound
	}
	return cloneObject(obj), nil
}

func (s *Store) SetObjectCurrentness(id string, currentness Currentness) error {

View on GitHub (pinned to 27d5a3981a)

Solutions

  1. Make immutable fields deterministic: derive them from the same inputs every time, never mutate them between puts
  2. Change the Data (and thus ContentHash and ID) when the object's content genuinely changes
  3. Diff existing := store.GetObject(obj.ID) against the incoming object to locate the divergent field

Example fix

// before
objA := ccr.Object{Type: t, SessionID: s, Source: src, Data: d, StoredByteLength: 10}
objB := ccr.Object{Type: t, SessionID: s, Source: src, Data: d, StoredByteLength: 20} // same identity, different length
store.PutObject(objA); store.PutObject(objB) // collision

// after
objB.StoredByteLength = 0 // let the store derive it; identical inputs -> identical immutable object
Defensive patterns

Strategy: validation

Validate before calling

if prev, ok := s.objectsByID[obj.ID]; ok && !sameImmutable(prev, obj) {
    obj.ContentHash = ""
}

Try / catch

if _, err := store.PutObject(obj); err != nil {
    if strings.Contains(err.Error(), "id collision") {
        // reconcile: rebuild object from identical inputs
    }
}

Prevention

When it happens

Trigger: 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.

Common situations: Browser/wasm client re-submitting an object whose metadata was locally augmented between attempts; test code mutating shared object structs between puts.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@27d5a3981a (2026-08-15). Data as JSON: /api/errors/2ca4108d849418ec. Report an issue: GitHub.