JuliusBrussee/caveman · error · ErrBudgetExceeded
cave_ccr_budget_exceeded
cave_ccr_budget_exceeded
Error message
ccr put: %w
What it means
put writes a recovery record into the CCR sqlite store; if the INSERT fails with a 'database or disk is full' condition (isFull), the error is wrapped as ccr put: %w around ErrBudgetExceeded. The code cave_ccr_budget_exceeded marks this as a budget exhaustion: the PRAGMA max_page_count budget set by configureStorageBudget has been reached and the new record is refused before publishing lossy data.
Solutions
- Handle errors.Is(err, ccr.ErrBudgetExceeded) and evict/GC old records before retrying the put
- Increase the storage budget (WithMaxStorageBytes / config) or delete old recovery records
- Run a cleanup/compaction (VACUUM after deleting) to return pages to the free list
- Check actual disk free space; SQLITE_FULL can also mean the disk itself is full
Example fix
// before
handle, err := store.Put(rec) // fails once budget hit
// after
handle, err := store.Put(rec)
if errors.Is(err, ccr.ErrBudgetExceeded) {
store.PruneOldRecords(cutoff) // free budget
handle, err = store.Put(rec)
} Defensive patterns
Strategy: try-catch
Validate before calling
used, _ := store.StorageUsed() // if exposed
if used > 0.9*budget { store.PruneOldRecords(cutoff) } Try / catch
handle, err := store.Put(rec)
if errors.Is(err, ccr.ErrBudgetExceeded) {
if gcErr := store.PruneOldRecords(time.Now().AddDate(0, 0, -7)); gcErr != nil { return gcErr }
handle, err = store.Put(rec)
} Prevention
- Check errors.Is(err, ccr.ErrBudgetExceeded), not string matching
- Schedule periodic pruning/compaction of old recovery records
- Size the budget above the steady-state working set
- Monitor store usage against maxBytes
When it happens
Trigger: Storing recovery records that grow the database past maxBytes (max_page_count); long-running sessions accumulating many large records; a budget configured smaller than the working set of records being put.
Common situations: Long agent/engine sessions with heavy context accumulation filling the store; after lowering the budget config on an existing populated database; disk-level fullness surfacing as SQLITE_FULL inside the budget cap.
Understand the failure class
Background: "File too large" / "file size exceeds limit" errors: why libraries cap file sizes and how to fix them — this error's family across 46 libraries.
Related errors
- budget is below CCR storage minimum
- ccr storage budget is below minimum
- ccr max storage bytes must be positive
- ccr: unknown lifecycle
- close open arm
AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20).
Data as JSON: /api/errors/49f0897ed3a57961.
Report an issue: GitHub.
Appendix: source
Thrown at engine/ccr/store_sqlite.go:501
`INSERT INTO recoveries (handle, created_at, content_type, compressor, tokens_before, tokens_after, original, metadata)
VALUES (?,?,?,?,?,?,?,?)
ON CONFLICT(handle) DO UPDATE SET
created_at=excluded.created_at,
content_type=CASE WHEN excluded.tokens_before = 0 AND recoveries.tokens_before > 0 THEN recoveries.content_type ELSE excluded.content_type END,
compressor=CASE WHEN excluded.tokens_before = 0 AND recoveries.tokens_before > 0 THEN recoveries.compressor ELSE excluded.compressor END,
tokens_before=CASE WHEN excluded.tokens_before = 0 AND recoveries.tokens_before > 0 THEN recoveries.tokens_before ELSE excluded.tokens_before END,
tokens_after=CASE WHEN excluded.tokens_before = 0 AND recoveries.tokens_before > 0 THEN recoveries.tokens_after ELSE excluded.tokens_after END,
original=excluded.original,
metadata=CASE
WHEN recoveries.metadata IS NULL OR length(recoveries.metadata) = 0 THEN excluded.metadata
ELSE recoveries.metadata
END`,
handle, time.Now().UTC().Format(time.RFC3339Nano),
rec.ContentType, rec.Compressor, rec.TokensBefore, rec.TokensAfter, rec.Original, rec.Metadata,
)
if err != nil {
if isFull(err) {
return "", fmt.Errorf("ccr put: %w", ErrBudgetExceeded)
}
return "", fmt.Errorf("ccr put: %w", err)
}
return handle, nil
}
func (s *Store) recoveryUnchanged(handle string, rec Recovery) (bool, error) {
var contentType, compressor string
var tokensBefore, tokensAfter int
var original, metadata []byte
err := s.db.QueryRow(
`SELECT content_type, compressor, tokens_before, tokens_after, original, metadata
FROM recoveries WHERE handle=?`,
handle,
).Scan(&contentType, &compressor, &tokensBefore, &tokensAfter, &original, &metadata)
if errors.Is(err, sql.ErrNoRows) {
return false, nil
}View on GitHub (pinned to 3ee70a1026)