* fix(system): never close another thread's library connection during a switch
Move library closed every thread's old connection from the mover thread,
including one whose owner was inside a statement waiting for the old
file's write lock. Closing a sqlite3 connection under a running
sqlite3_step is a use-after-free: Windows CI died with "access violation"
in record_prompt_use, and on Linux the writer got a MemoryError once its
busy timeout ran out.
Managed connections now count the calls their owner is inside.
Retiring one from another thread closes it at once only when idle;
otherwise it is interrupted (sqlite3_interrupt is thread-safe) and its
owner closes it as the call ends. A retired connection refuses new work
and never commits, so a write that waited for the lock is rolled back
and survive_library_switch runs it again on the new library. The mover
retires connections while it holds the locks and waits for the owners
to close them only after releasing them, before the old files are
renamed or a failed copy is removed. Cursors are closed before the
connection so Python 3.10/3.11 release the file handle too.
* test(tagging): wait for in-flight pages before reading the loaded range
The scroll-position test read the loaded count right after scrolling the
continuation out of view, while a listing page the continuation had
already requested could still be in flight. When it landed the count
grew (80 -> 120 in CI) and the stability poll failed. The test now waits
a frame for any scheduled load to start and then for every listing
request to finish before it reads the count.
* fix(system): make retiring a library connection race-free and cheap
Review round for the cross-thread close fix:
- cursor() now records its cursor under the lock _close_now copies the
cursor set under, and refuses once the connection is retired; a cursor
opened while the connection closed raised "Set changed size during
iteration" out of retire() on Python 3.10-3.12. retire() never raises,
retire_connections keeps going when one connection fails, and the mover
never reports a completed switch as failed because retiring or awaiting
old connections did.
- The restore's backup target is marked in use (ManagedConnection.using),
and in_transaction and create_function are guarded.
- Iterating a cursor marks the connection in use once per fetchmany(128)
chunk instead of once per row: a 1000-row scan costs 1.10x a plain
cursor instead of 2.7x (3.14) / 3.8x (3.10).
- Document what an interrupt does not end, the statements a retired
connection cannot hold back, close() from another thread returning
before the file is closed, and the await timeout ending as old-in-use.