POST /prompt_manager/system/library/move starts "Move library", an exclusive, non-cancellable job (database/library_move.py). It checks the target volume's free space, copies the database with SQLite's backup API from one read snapshot, verifies the copy (integrity check, every workflow document, the same rows in every table), copies the safety and pre-migration backups and config.json (owner-only, checked by size and SHA-256), then publishes under the old file's write lock and switches the live process (every PromptModel, the review and history observers, the API's plans and health report). The old prompts.db and config.json are renamed *.moved-to-<folder> only after the switch. A failure removes everything it wrote and leaves the old library in use; a marker in the new folder makes a restart after any crash pick the right library. GET /prompt_manager/system/library and the Overview's "location" say where the library lives and why; POST .../keep persists "Keep it here".
76 lines
2.5 KiB
Python
76 lines
2.5 KiB
Python
"""Needs review queue change notices (#249 PR F2).
|
|
|
|
Every committed change to the pending queue (an item recorded by the scan or
|
|
the monitor, a spelling refresh, a decision) calls ``notify_review_change``
|
|
after its transaction, from whichever thread wrote it. The review event
|
|
stream (``py/api/review_events.py``) subscribes, so an open Needs review page
|
|
re-reads its queue when it changes instead of polling. Observers are keyed by
|
|
the database file, like ``subscribe_history_restore``; an observer that fails
|
|
is logged and never fails the write.
|
|
"""
|
|
|
|
import os
|
|
import threading
|
|
from typing import Callable
|
|
|
|
try:
|
|
from ..utils.logging_config import get_logger
|
|
except ImportError:
|
|
from utils.logging_config import get_logger
|
|
|
|
_logger = get_logger("prompt_manager.database.review_changes")
|
|
_lock = threading.Lock()
|
|
_observers: dict[str, dict[object, Callable[[], None]]] = {}
|
|
|
|
|
|
def _identity(db_path: str) -> str:
|
|
return os.path.normcase(os.path.realpath(db_path))
|
|
|
|
|
|
def subscribe_review_changes(
|
|
db_path: str, callback: Callable[[], None]
|
|
) -> Callable[[], None]:
|
|
"""Call ``callback()`` after each committed queue change; return the unsubscribe."""
|
|
identity = _identity(db_path)
|
|
token = object()
|
|
with _lock:
|
|
_observers.setdefault(identity, {})[token] = callback
|
|
|
|
def unsubscribe() -> None:
|
|
with _lock:
|
|
observers = _observers.get(identity)
|
|
if observers is not None:
|
|
observers.pop(token, None)
|
|
if not observers:
|
|
_observers.pop(identity, None)
|
|
|
|
return unsubscribe
|
|
|
|
|
|
def observer_count(db_path: str) -> int:
|
|
"""How many observers watch ``db_path`` (tests and diagnostics)."""
|
|
with _lock:
|
|
return len(_observers.get(_identity(db_path), {}))
|
|
|
|
|
|
def notify_review_change(db_path: str) -> None:
|
|
"""Tell every observer of ``db_path``; call it after the commit."""
|
|
with _lock:
|
|
callbacks = tuple(_observers.get(_identity(db_path), {}).values())
|
|
for callback in callbacks:
|
|
try:
|
|
callback()
|
|
except Exception:
|
|
_logger.warning("Needs review change observer failed", exc_info=True)
|
|
|
|
|
|
def rekey_review_changes(old_path: str, new_path: str) -> None:
|
|
"""Move the observers of ``old_path`` to ``new_path`` (Move library)."""
|
|
old, new = _identity(old_path), _identity(new_path)
|
|
if old == new:
|
|
return
|
|
with _lock:
|
|
observers = _observers.pop(old, None)
|
|
if observers:
|
|
_observers.setdefault(new, {}).update(observers)
|