08e9dcf182
The second lock block in get_or_create_session held self._lock during six blocking operations on every inbound message: _is_session_ended_in_db (SQLite SELECT), _should_reset (callback), _save (SQLite write + JSON write + os.fsync), and _recover_session_from_db (SQLite SELECT + UPDATE). A code comment at line 1607 claimed 'SQLite calls are made outside the lock' -- true only for _compression_tip_for_session_id, which was moved out in a prior fix. The remaining I/O was never addressed. Restructure into a four-phase lock/no-lock split that mirrors the pattern already established at the bottom of the function: Phase 1 (lock) -- read entry + session_id Phase 1b (no lock) -- stale check + reset policy Phase 2 (lock) -- apply decisions to _entries, capture snapshot + flags Phase 3 (no lock) -- recovery DB query, _save from snapshot, end/create _save_entries(snapshot) replaces _save() to avoid dict-mutation races when called outside the lock. _query_recoverable_session splits the DB I/O out of _recover_session_from_db so only the _entries assignment needs the lock. Three early returns inside the lock block are eliminated in favour of a unified save + return path.