{"record":{"id":"49f0897ed3a57961","repo":"JuliusBrussee/caveman","slug":"cave-ccr-budget-exceeded","errorCode":"cave_ccr_budget_exceeded","errorMessage":"ccr put: %w","messagePattern":"ccr put: %w","errorType":"error_code","errorClass":"ErrBudgetExceeded","httpStatus":null,"severity":"error","filePath":"engine/ccr/store_sqlite.go","lineNumber":501,"sourceCode":"\t\t`INSERT INTO recoveries (handle, created_at, content_type, compressor, tokens_before, tokens_after, original, metadata)\n\t\t VALUES (?,?,?,?,?,?,?,?)\n\t\t ON CONFLICT(handle) DO UPDATE SET\n\t\t   created_at=excluded.created_at,\n\t\t   content_type=CASE WHEN excluded.tokens_before = 0 AND recoveries.tokens_before > 0 THEN recoveries.content_type ELSE excluded.content_type END,\n\t\t   compressor=CASE WHEN excluded.tokens_before = 0 AND recoveries.tokens_before > 0 THEN recoveries.compressor ELSE excluded.compressor END,\n\t\t   tokens_before=CASE WHEN excluded.tokens_before = 0 AND recoveries.tokens_before > 0 THEN recoveries.tokens_before ELSE excluded.tokens_before END,\n\t\t   tokens_after=CASE WHEN excluded.tokens_before = 0 AND recoveries.tokens_before > 0 THEN recoveries.tokens_after ELSE excluded.tokens_after END,\n\t\t   original=excluded.original,\n\t\t   metadata=CASE\n\t\t     WHEN recoveries.metadata IS NULL OR length(recoveries.metadata) = 0 THEN excluded.metadata\n\t\t     ELSE recoveries.metadata\n\t\t   END`,\n\t\thandle, time.Now().UTC().Format(time.RFC3339Nano),\n\t\trec.ContentType, rec.Compressor, rec.TokensBefore, rec.TokensAfter, rec.Original, rec.Metadata,\n\t)\n\tif err != nil {\n\t\tif isFull(err) {\n\t\t\treturn \"\", fmt.Errorf(\"ccr put: %w\", ErrBudgetExceeded)\n\t\t}\n\t\treturn \"\", fmt.Errorf(\"ccr put: %w\", err)\n\t}\n\treturn handle, nil\n}\n\nfunc (s *Store) recoveryUnchanged(handle string, rec Recovery) (bool, error) {\n\tvar contentType, compressor string\n\tvar tokensBefore, tokensAfter int\n\tvar original, metadata []byte\n\terr := s.db.QueryRow(\n\t\t`SELECT content_type, compressor, tokens_before, tokens_after, original, metadata\n\t\t FROM recoveries WHERE handle=?`,\n\t\thandle,\n\t).Scan(&contentType, &compressor, &tokensBefore, &tokensAfter, &original, &metadata)\n\tif errors.Is(err, sql.ErrNoRows) {\n\t\treturn false, nil\n\t}","sourceCodeStart":483,"sourceCodeEnd":519,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/3ee70a102609e550bd2e68004bf5990a9341c851/engine/ccr/store_sqlite.go#L483-L519","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\nhandle, err := store.Put(rec) // fails once budget hit\n// after\nhandle, err := store.Put(rec)\nif errors.Is(err, ccr.ErrBudgetExceeded) {\n    store.PruneOldRecords(cutoff) // free budget\n    handle, err = store.Put(rec)\n}","handlingStrategy":"try-catch","validationCode":"used, _ := store.StorageUsed() // if exposed\nif used > 0.9*budget { store.PruneOldRecords(cutoff) }","typeGuard":null,"tryCatchPattern":"handle, err := store.Put(rec)\nif errors.Is(err, ccr.ErrBudgetExceeded) {\n    if gcErr := store.PruneOldRecords(time.Now().AddDate(0, 0, -7)); gcErr != nil { return gcErr }\n    handle, err = store.Put(rec)\n}","preventionTips":["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"],"tags":["go","sqlite","budget","storage"],"backgroundTag":"file-size-limit-exceeded","analyzedSha":"3ee70a102609e550bd2e68004bf5990a9341c851","analyzedAt":"2026-09-20T15:53:39.229Z","contentChangedAt":"2026-09-20T15:53:39.229Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}