JuliusBrussee/caveman · error

ccr: typed object byte lengths cannot be negative

Error message

ccr: typed object byte lengths cannot be negative

What it means

prepareObject defaults zero byte-length fields to len(obj.Data) but rejects explicitly negative OriginalByteLength or StoredByteLength. Negative lengths would corrupt budget accounting and storage stats, so they fail closed. Only values < 0 are rejected; zero means 'fill in for me'.

Source

Thrown at engine/ccr/store.go:139

		obj.CreatedAt = time.Now().UTC()
	} else {
		obj.CreatedAt = obj.CreatedAt.UTC()
	}
	dataSum := sha256.Sum256(obj.Data)
	computedHash := "sha256:" + hex.EncodeToString(dataSum[:])
	if obj.ContentHash == "" {
		obj.ContentHash = computedHash
	} else if obj.ContentHash != computedHash {
		return Object{}, errors.New("ccr: typed object content_hash does not match data")
	}
	if obj.OriginalByteLength == 0 {
		obj.OriginalByteLength = len(obj.Data)
	}
	if obj.StoredByteLength == 0 {
		obj.StoredByteLength = len(obj.Data)
	}
	if obj.OriginalByteLength < 0 || obj.StoredByteLength < 0 {
		return Object{}, errors.New("ccr: typed object byte lengths cannot be negative")
	}
	if obj.ID == "" {
		identity := strings.Join([]string{string(obj.Type), obj.SessionID, obj.Source, obj.RepositoryState, obj.ContentHash}, "\x00")
		sum := sha256.Sum256([]byte(identity))
		obj.ID = "ccr_obj_" + hex.EncodeToString(sum[:16])
	}
	obj.Dependencies = append([]string(nil), obj.Dependencies...)
	obj.Data = bytes.Clone(obj.Data)
	return obj, nil
}

// sameImmutableObject compares fields PutObject promises never to change.
// CreatedAt is assigned at first storage, while currentness and lifecycle have
// explicit mutation methods, so repeat puts intentionally do not compare them.
func sameImmutableObject(left, right Object) bool {
	return left.ID == right.ID &&
		left.Type == right.Type &&
		left.ContentHash == right.ContentHash &&

View on GitHub (pinned to 27d5a3981a)

Solutions

  1. Leave the length fields at 0 so the store derives them from Data
  2. If you track pre-transform size, ensure OriginalByteLength is a non-negative count of the original bytes
  3. Clamp/validate lengths at your boundary before building the Object

Example fix

// before
obj := ccr.Object{OriginalByteLength: -1, Data: data} // -1 as 'unknown'

// after
obj := ccr.Object{Data: data} // lengths default to len(Data)
// or, when tracking the pre-compression size:
obj := ccr.Object{Data: packed, OriginalByteLength: len(original), StoredByteLength: len(packed)}
Defensive patterns

Strategy: validation

Validate before calling

if obj.OriginalByteLength < 0 || obj.StoredByteLength < 0 {
    return errors.New("byte lengths must be >= 0")
}
store.PutObject(obj)

Type guard

func validByteLengths(o ccr.Object) bool {
    return o.OriginalByteLength >= 0 && o.StoredByteLength >= 0
}

Prevention

When it happens

Trigger: Setting OriginalByteLength or StoredByteLength to -1 or any negative value (often as a sentinel for 'unknown'); arithmetic that underflows (e.g. len(data) - overhead where overhead > len(data)); deserializing a record with corrupt length fields.

Common situations: Using -1 as an 'unset' sentinel in a producer system; integer underflow when computing stored length after subtracting an estimate; hand-edited or externally produced JSON records with negative numbers.

Related errors


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