JuliusBrussee/caveman · error
prefix replacement: incomplete entry
Error message
prefix replacement: incomplete entry
What it means
Store.RememberReplacement durably binds original request bytes to their replacement under a SHA-256 key derived from scope+original (first-write-wins). All four arguments are load-bearing: scope separates transform plans, replacement is what must go on the wire, and handle is the CCR recovery reference disclosed to clients. Any empty scope, empty original/replacement bytes, or empty handle is rejected as an incomplete entry rather than written as a useless cache row.
Source
Thrown at proxy/internal/store/prefix_cache.go:45
if !ok {
return nil, "", false
}
// Touch for LRU eviction only. A failed touch changes nothing the caller can
// observe this turn, so it is logged and swallowed rather than turned into a miss.
if _, err := s.db.Exec(`UPDATE prefix_replacements SET last_used_at = ? WHERE original_sha256 = ?`, prefixCacheNow(), key); err != nil && s.logger != nil {
s.logger.Warn("prefix replacement touch failed", "error", err)
}
return replacement, handle, true
}
// RememberReplacement durably records original→replacement and returns the
// authoritative bytes for that original. Storage is first-write-wins: if another
// in-flight request already stored a replacement for the same block, that one is
// returned and the caller forwards it, so two requests can never put two different
// prefixes on the wire for one logical message.
func (s *Store) RememberReplacement(scope string, original, replacement []byte, handle string) ([]byte, error) {
if scope == "" || len(original) == 0 || len(replacement) == 0 || handle == "" {
return nil, errors.New("prefix replacement: incomplete entry")
}
key := prefixCacheKey(scope, original)
now := prefixCacheNow()
if _, err := s.db.Exec(
`INSERT INTO prefix_replacements (original_sha256, handle, replacement, created_at, last_used_at)
VALUES (?,?,?,?,?)
ON CONFLICT(original_sha256) DO UPDATE SET last_used_at=excluded.last_used_at`,
key, handle, replacement, now, now,
); err != nil {
return nil, fmt.Errorf("prefix replacement put: %w", err)
}
stored, _, ok := s.readReplacement(key)
if !ok {
return nil, errors.New("prefix replacement: entry unreadable after write")
}
s.evictPrefixReplacements()
return stored, nil
}View on GitHub (pinned to 766dce6b13)
Solutions
- Guard the call: only invoke RememberReplacement when scope != "" && len(original) > 0 && len(replacement) > 0 && handle != "".
- If the no-replacement path reaches this code, skip the cache write entirely — original bytes forwarded verbatim need no cached replacement.
- Add a unit test for the pass-through/no-compression branch proving the store write is skipped.
Example fix
// before
stored, err := s.store.RememberReplacement(scope, original, replacement, handle) // called on every turn
// after
if scope == "" || len(original) == 0 || len(replacement) == 0 || handle == "" {
return original, nil // nothing durable to record; forward original bytes
}
stored, err := s.store.RememberReplacement(scope, original, replacement, handle) Defensive patterns
Strategy: validation
Validate before calling
// Before RememberReplacement: every argument is load-bearing.
func shouldCacheReplacement(scope string, original, replacement []byte, handle string) bool {
return scope != "" && len(original) > 0 && len(replacement) > 0 && handle != ""
}
if shouldCacheReplacement(scope, original, replacement, handle) {
stored, err = store.RememberReplacement(scope, original, replacement, handle)
} Prevention
- Derive the scope string before entering the transform path so it can never be empty by accident.
- Only record a replacement on turns that actually produced one (non-empty handle from the CCR step).
- Unit-test the pass-through branch to prove no cache write is attempted with empty fields.
When it happens
Trigger: Forwarding code calls RememberReplacement unconditionally: with handle "" on a turn where no CCR handle was produced, with empty replacement bytes when the transform emitted nothing, with empty original (nothing to replace), or with scope "" when the plan/transform scope was never derived before the call.
Common situations: Gateway paths that record 'whatever happened' after a pass-through turn (no compression means no handle and no replacement); refactors that move scope computation after the cache write; tests exercising the no-replacement branch for the first time.
Related errors
- assembly slot {slot_id!r} changed after being declared stabl
- prefix replacement: entry unreadable after write
- ${name} must not contain a newline
AI-assisted analysis of JuliusBrussee/caveman@766dce6b13 (2026-08-18).
Data as JSON: /api/errors/42d4cea418dfe72b.
Report an issue: GitHub.