JuliusBrussee/caveman · error
ccr: recovery store is closed
Error message
ccr: recovery store is closed
What it means
The CCR recovery store refuses all operations after Close() has been called. checkGeneration runs on every withStore/Close path and short-circuits with this sentinel when s.closed is set, preventing use of a store whose recovery files may already have been released.
Solutions
- Ensure Close is the last operation: reorder code so no withStore call happens after Close.
- Audit goroutines holding a reference to the store and shut them down before Close (waitgroup/context cancellation).
- If the store must be reopened, construct a new Store instead of reusing the closed instance.
- Guard call sites with a closed check or run operations through a single owner that serializes Close.
Example fix
// before
store.Close()
record, err := store.WithStore(func(...) {...}) // panics-free but errors
// after
record, err := store.WithStore(func(...) {...})
if err != nil { ... }
store.Close() Defensive patterns
Strategy: try-catch
Validate before calling
// track liveness yourself
if store.IsClosed() { return errors.New("store already closed") } Try / catch
rec, err := store.WithStore(fn)
if err != nil {
if strings.Contains(err.Error(), "store is closed") {
// reopen or abort shutdown path
}
return err
} Prevention
- Make Close the terminal operation in a defer placed last
- Cancel worker goroutines before Close (context + WaitGroup)
- Never reuse a Store after Close; construct a new one
When it happens
Trigger: Any operation routed through withStore (reads/writes to the recovery store) or Close itself, executed after a prior call to Store.Close on the same *ccr.Store instance.
Common situations: Calling Close in a defer and then still using the store later in the function; a long-lived process that closed the store during shutdown but a lingering goroutine still writes decisions; double-Close; tests reusing a closed fixture store.
Understand the failure class
Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.
Related errors
- closed
- A dynamic Strands scope requires the native agent…
- broken supersession history at
- budget is below CCR storage minimum
- cave_budget_cap_breached
AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20).
Data as JSON: /api/errors/e6d2c2e4e3e428e9.
Report an issue: GitHub.
Appendix: source
Thrown at engine/ccr/store_generation.go:141
// before the first complete generation snapshot. Never close an opened disk
// connection on that error path: SQLite close can checkpoint its stale WAL into
// the current main file. The process must exit to release those descriptors.
func closeSQLiteAfterOpenFailure(db *sql.DB, path string) {
if path == ":memory:" || db.Stats().OpenConnections == 0 {
_ = db.Close()
}
}
// checkGeneration runs with mu held. A detected generation change is terminal
// for this Store. A fresh process must open the restored/current database;
// pathname snapshots cannot establish which files a replacement connection
// actually opened, so automatically adopting a replacement is unsafe.
func (s *Store) checkGeneration() error {
if s.quarantined != nil {
return s.quarantined
}
if s.closed {
return errors.New("ccr: recovery store is closed")
}
files, err := inspectSQLiteGeneration(s.path)
if errors.Is(err, errStorageUnverifiable) {
// "Could not look" is not "was replaced". A momentary stat/permission
// failure — an antivirus lock on Windows, EIO on a network home, a
// parent directory whose mode was loose for one instant — fails THIS
// operation closed, but must not latch the terminal state below and
// make already-stored recoveries unreadable for the rest of the process.
return err
}
if err != nil {
s.quarantined = err
return s.quarantined
}
if s.db != nil && s.files.same(files) {
return nil
}
// No SQLite calls, including Close, follow invalidation. The public driverView on GitHub (pinned to 3ee70a1026)