From 71a00bd1943e10dc8bf1e210e1f7a6b11f6f6caf Mon Sep 17 00:00:00 2001 From: KT Date: Thu, 30 Jul 2026 17:47:18 +0800 Subject: [PATCH] fix(cli): drop the lancedb finalization gate this process cannot trigger `raven.cli.commands.run` hard-exited past interpreter finalization whenever lancedb's `LanceDBBackgroundEventLoop` thread was live. Nothing under `raven/` imports lancedb: it is a transitive dependency only, memory talks to everos over HTTP, and everos itself runs out-of-process. `torch`, named alongside it in the agent-loop comment, is neither declared nor imported either. The probe therefore returned False in every configuration and the branch was unreachable. Removed: the probe in `_exit.py`, the `if` it fed in `commands.py`, and the stale comment in `agent_commands.py` claiming the exit chokepoint covers that hazard. `flush_and_hard_exit` stays -- `tests/conftest.py` calls it on CI, where a fully green suite was observed exiting 139 on Linux. Its module docstring now describes that caller instead of the lancedb story, and says why the gate went. Deleting the whole `except SystemExit` block is behaviour-preserving: with the probe always False it reduced to `except SystemExit: raise`, and the exit-code normalization inside it only ever fed the hard-exit call, never the re-raised exception. Verified through the real console-script entry point, which `CliRunner` never reaches: `raven --version` 0, `skill get` with a missing argument 2, an unknown command 2, `status` 0, and an unparseable config still prints one clean line instead of a traceback. Suite goes 4655 -> 4653: the two tests that exercised the probe are gone, and the four covering `flush_and_hard_exit` remain. Co-authored-by: Claude (claude-opus-5) --- raven/cli/_exit.py | 30 +++++++++------------------- raven/cli/agent_commands.py | 4 ---- raven/cli/commands.py | 18 +---------------- tests/test_cli_exit.py | 40 ++++--------------------------------- 4 files changed, 14 insertions(+), 78 deletions(-) diff --git a/raven/cli/_exit.py b/raven/cli/_exit.py index 0d9787d22..baf322460 100644 --- a/raven/cli/_exit.py +++ b/raven/cli/_exit.py @@ -1,35 +1,23 @@ -"""Hard-exit past CPython interpreter finalization for native-unsafe runtimes. +"""Hard-exit past CPython interpreter finalization. -Building the agent loop opens a lancedb-backed store, and lancedb starts a -process-global ``LanceDBBackgroundEventLoop`` (Rust/tokio) daemon thread with -no public shutdown hook. Finalizing the interpreter while that native runtime -is still live segfaults (``Py_FinalizeEx``; SIGSEGV, exit 139), masking the -command's real exit code. Every raven command that builds the loop starts that -thread, so the guard lives once at the CLI exit chokepoint -(:func:`raven.cli.commands.run`): when the hazard is live, flush and -``os._exit`` past finalization. +Finalizing the interpreter while native state is still live can segfault +(``Py_FinalizeEx``; SIGSEGV, exit 139) and mask the real exit code. The only +caller today is the pytest session hook (``tests/conftest.py``), where a fully +green run was observed exiting 139 on Linux. -CliRunner invokes the Typer ``app`` object directly (in-process), never the -console-script ``run`` wrapper, so test hosts keep normal exit semantics. +This used to also gate ``raven.cli.commands.run`` on a live lancedb background +thread. That gate is gone: nothing under ``raven/`` imports lancedb -- memory +talks to everos over HTTP and everos runs out-of-process -- so the probe could +not fire in any configuration. """ from __future__ import annotations import os import sys -import threading from typing import NoReturn -def lancedb_finalization_hazard() -> bool: - """Whether lancedb's Rust/tokio background thread is live in this process. - - Merely importing lancedb is safe — the thread only exists once a connection - is opened — so key on the live thread, not the imported module. - """ - return any(t.name == "LanceDBBackgroundEventLoop" for t in threading.enumerate()) - - def flush_and_hard_exit(code: int) -> NoReturn: """Flush stdio + loguru sinks, then ``os._exit`` past interpreter finalization.""" try: diff --git a/raven/cli/agent_commands.py b/raven/cli/agent_commands.py index 1d8d1e09b..6f863eeea 100644 --- a/raven/cli/agent_commands.py +++ b/raven/cli/agent_commands.py @@ -462,10 +462,6 @@ async def run_once(): ) asyncio.run(run_once()) - # Native runtimes loaded by the agent loop (lancedb's Rust/tokio - # thread, torch) segfault during interpreter finalization. The exit - # chokepoint in raven.cli.commands.run hard-exits past finalization - # when that hazard is live, so this path just returns normally. else: # Interactive mode — user turns run through spine (submit -> lane -> # hub -> CliOutlet); cron/sentinel nudges go via the spine hub diff --git a/raven/cli/commands.py b/raven/cli/commands.py index 848b39ac6..0c453c290 100644 --- a/raven/cli/commands.py +++ b/raven/cli/commands.py @@ -148,16 +148,7 @@ def main( def run() -> None: - """Console-script entry point. - - Runs the Typer app, then hard-exits past CPython interpreter finalization - when a native runtime that segfaults at finalization is live (lancedb's - Rust/tokio background thread — see :mod:`raven.cli._exit`). Any command that - builds the agent loop starts that thread, so guarding here covers them all - at once. CliRunner invokes ``app`` directly and never reaches this wrapper, - so in-process test hosts keep normal exit semantics. - """ - from raven.cli._exit import flush_and_hard_exit, lancedb_finalization_hazard + """Console-script entry point.""" from raven.config.loader import ConfigReadError try: @@ -170,13 +161,6 @@ def run() -> None: Console(stderr=True).print(f"[red]✗[/red] {exc}") raise SystemExit(1) from exc - except SystemExit as exc: - code = exc.code - if not isinstance(code, int): - code = 0 if code is None else 1 - if lancedb_finalization_hazard(): - flush_and_hard_exit(code) - raise if __name__ == "__main__": diff --git a/tests/test_cli_exit.py b/tests/test_cli_exit.py index e3a879df3..f0ec4042c 100644 --- a/tests/test_cli_exit.py +++ b/tests/test_cli_exit.py @@ -1,15 +1,9 @@ -"""Tests for the hard-exit guard in ``raven.cli._exit``. +"""Tests for the hard-exit helper in ``raven.cli._exit``. -The guard exists because native runtimes loaded by the agent loop segfault -during interpreter finalization; ``raven.cli.commands.run`` and the pytest -session both route their exit through it. These pin the guard's own behaviour +Its only caller is the pytest session hook in ``tests/conftest.py``, which needs +it because a fully green run was observed exiting 139 on Linux when the +interpreter finalized with native state live. These pin the helper's behaviour in-process, so the coverage does not depend on provoking a real native crash. - -Scope note, so these tests are not mistaken for proof the guard is load-bearing: -``flush_and_hard_exit`` is live (the pytest session calls it on CI), but the -``lancedb_finalization_hazard`` gate in ``commands.py`` is currently dormant -- -the memory plugin talks to everos over HTTP and opens no local lancedb -connection, so the probe returns False in every configuration today. """ from __future__ import annotations @@ -17,32 +11,6 @@ import subprocess import sys import textwrap -import threading - -import pytest - -from raven.cli._exit import lancedb_finalization_hazard - -_LANCEDB_THREAD = "LanceDBBackgroundEventLoop" - - -def test_hazard_false_without_the_lancedb_thread() -> None: - if any(t.name == _LANCEDB_THREAD for t in threading.enumerate()): - pytest.skip("a lancedb connection is already open in this process") - assert lancedb_finalization_hazard() is False - - -def test_hazard_true_while_a_thread_of_that_name_is_alive() -> None: - """The probe keys on the thread name, not on lancedb being imported.""" - stop = threading.Event() - worker = threading.Thread(target=stop.wait, name=_LANCEDB_THREAD, daemon=True) - worker.start() - try: - assert lancedb_finalization_hazard() is True - finally: - stop.set() - worker.join(timeout=5) - assert lancedb_finalization_hazard() is False def _hard_exit_child(code: int, *, prints: str = "") -> subprocess.CompletedProcess: