Coverage for src/local_deep_research/exceptions.py: 100%
10 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-22 21:20 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-22 21:20 +0000
1"""Project-wide exception classes."""
4class ResearchTerminatedException(BaseException):
5 """Raised when a user cancels an in-progress research process.
7 Inherits from BaseException (not Exception) so that ``except Exception``
8 blocks throughout the strategy code naturally let it propagate -- the same
9 pattern Python's stdlib uses for asyncio.CancelledError (since 3.9),
10 KeyboardInterrupt, and SystemExit.
11 """
13 pass
16class NoUserDatabaseError(RuntimeError):
17 """Raised when a resolved user has no database to open.
19 Subclasses ``RuntimeError`` deliberately: this condition was signalled by a
20 bare ``RuntimeError`` before, and callers (plus
21 ``tests/utilities/test_db_utils.py``) still catch and match on that. The
22 subclass adds the ability to tell this case apart *without* changing what
23 anything already catches.
25 That distinction is the whole point. ``get_settings_manager`` used to catch
26 every ``RuntimeError`` from ``get_db_session`` and fall back to anonymous
27 defaults, which conflates three unrelated situations:
29 1. This one — a known user with no database. Defaults are the only answer
30 available, so the fallback is correct.
31 2. A true background thread with no request context. That is a programming
32 error the codebase already warns about in several places (#3453), and
33 answering it with defaults means a worker silently reads *someone
34 else's* configuration instead of the user's.
35 3. Anything deeper failing inside ``db_manager.get_session`` — a corrupt or
36 unopenable encrypted database, a keyring failure, or a rekey sidecar
37 or epoch fault on the cached engine (raised as the typed
38 ``RekeyOperationalError``, not answered with ``None``). Serving
39 defaults here is the most dangerous of the three: a setting that is
40 off by default silently reads as off, even though the user turned it
41 on, and a stored ``strict`` egress scope reads as the default.
43 Only case 1 should be swallowed. The other two must reach the caller.
44 """
46 pass
49class SystemAtCapacityError(Exception):
50 """Raised when ``start_research_process`` cannot acquire the global
51 concurrency semaphore.
53 Previously the semaphore was acquired *inside* the worker thread, so a
54 full system would silently park the thread after the HTTP route had
55 already returned 200 — the user saw a thinking spinner that never
56 advanced, and the partial unique in-progress index blocked retries on
57 the same chat session.
59 Acquiring synchronously in the caller and surfacing this exception lets
60 routes return HTTP 429 (or queue/retry, depending on caller) before any
61 ``ResearchHistory`` row is committed.
62 """
64 pass
67class DuplicateResearchError(Exception):
68 """Raised when a research should not be (re-)spawned.
70 Two triggering cases, both handled identically by callers:
72 1. A live thread already exists in the active-research dict for this
73 ``research_id`` — typically a retry after a prior attempt's
74 post-spawn ``UserActiveResearch`` commit failed.
75 2. The ``ResearchHistory.status`` is non-QUEUED (``IN_PROGRESS`` from
76 a prior attempt's pre-spawn commit that succeeded; terminal
77 ``COMPLETED`` / ``FAILED`` / ``SUSPENDED`` from a thread that
78 already finished and cleaned itself out of ``_active_research``).
79 Re-spawning would either contradict the live thread or re-run a
80 finished research.
82 Callers that wrap the spawn in ``except Exception`` to clean up orphan
83 state on spawn failure MUST catch ``DuplicateResearchError`` separately
84 *before* that generic branch and re-raise / return without mutating the
85 research's status or deleting the ``UserActiveResearch`` row — those
86 rows belong to the live thread, and marking them FAILED terminates a
87 running thread from the user's perspective while it keeps executing.
88 """
90 pass
93class InvalidQueuedResearchOverridesError(Exception):
94 """Raised when persisted queued search overrides fail validation."""
96 pass