aad74f26f9
The #102827 corruption is pure zero holes -- frames lost across a WAL generation. SessionDB.close() produces exactly that when it runs against a file another live handle is still writing: PRAGMA wal_checkpoint(PASSIVE), then the connection close that lets SQLite unlink -wal/-shm. The dangerous event is a physical close overlapping any other live physical lifetime for the same path, so both sides of it are closed here. Late write vs. close: a cron watchdog timeout only stops waiting, and ThreadPoolExecutor.shutdown(wait=False) cannot interrupt a worker already inside run_conversation. The agent and its registry reference are now held until that worker's Future completes, so its last frames land before any checkpoint. Close vs. open: the per-path barrier now COUNTS admitted teardowns. A path can own several closes at once -- the current generation's final release and a retired generation's drain are admitted independently under the registry lock, and the per-path mutex only serializes teardowns that already entered it. With one bare event per path, a releasing thread descheduled between generation removal and the mutex let the next teardown to settle remove and signal the shared event: close_all() returned over a pending close and acquire() published a replacement writer on top of a handle still inside checkpoint/unlink. _TeardownBarrier tracks event + pending count, _admit_teardown_locked registers each close in the same lock section that removes the generation, and only the last settled teardown lifts the barrier. Physical I/O stays outside the registry lock and unrelated paths still progress independently. The auto-archive sweep called release_or_close in its finally while the import was local to a different function, so every eligible sweep raised NameError, the outer except Exception swallowed it at debug level, and the borrowed registry reference was never returned -- a holder leak that pins a retired generation open. The helper is now bound in the calling scope. Remaining in-process writable SessionDB() call sites (trace upload, the API-server profile cache, the web-server writable paths, startup schema reconcile) go through the canonical registry acquire/release_or_close, and gateway maintenance borrows pinned handles instead of iterating an unpinned snapshot. Regressions: overlapping final releases of the current and retired generations in both orderings with the first paused before the lifecycle mutex, teardown-error settlement, an unrelated-path control, and refcount assertions for the auto-archive sweep on success, on failure, across repeated sweeps and with auto-archive disabled. Fixes #102827 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BzxCWw6SuHXhXMdkiEwMa2