mirror of
https://github.com/usestrix/strix.git
synced 2026-08-20 02:23:35 +02:00
fix(runtime,interface): mount sources at advertised paths + surface scan failures in TUI
Two fixes that surfaced from a single broken run. (1) Source mounting was double-broken: - ``session_manager.create_or_reuse`` mounted the *parent* of the first local source under a hardcoded ``"sources"`` key, so the host's unrelated content leaked in at ``/workspace/sources/...`` while the agent's task prompt advertised ``/workspace/<workspace_subdir>`` (from ``_build_root_task``). Result: the agent looked at ``/workspace/empty/`` (per the prompt), found nothing, and bailed. - ``backends._docker_backend`` never called ``await session.start()`` after ``client.create()`` — the SDK's manifest application (``LocalDir`` materialization, mount setup) only runs inside ``start()`` (or ``async with session:``). So even with the right ``entries`` the workspace would have been empty anyway. Fix: thread ``args.local_sources`` (already populated by ``collect_local_sources``) all the way through to the session manager, build ``Manifest.entries`` keyed by each source's ``workspace_subdir``, and call ``session.start()`` in the docker backend so the SDK actually materializes the entries. Drop the now-unused ``_resolve_sources_path`` helpers from ``cli.py`` and ``tui.py``. (2) Scan-failure visibility was nonexistent in TUI mode: - The SDK's ``on_agent_end`` hook only fires after the agent reaches its first turn. A failure earlier (model routing, sandbox bring-up, …) left the root agent stuck at ``status=running`` in the bus and tracer, so the TUI animated "Initializing" forever. - ``scan_target`` in ``tui.py`` caught the exception and called ``logging.exception`` but never propagated it. ``run_tui`` returned cleanly when the user finally ctrl-q'd, so ``main.py`` happily printed the success-completion banner over a dead scan. Fix: in ``run_strix_scan``'s ``except BaseException`` block, finalize the root agent as ``"failed"`` in both the bus and the tracer (with the error message attached). Capture the exception on ``StrixTUIApp._scan_error`` from the scan thread; ``run_tui`` re-raises it after ``app.run_async()`` returns so ``main.py``'s existing handler prints the traceback. Add a ``"failed"`` branch to ``_get_status_display_content`` that shows the error message in red, mirroring the existing ``llm_failed`` branch. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
0518599f29
commit
53188a7583
+1
-23
@@ -1,12 +1,10 @@
|
||||
import atexit
|
||||
import contextlib
|
||||
import logging
|
||||
import os
|
||||
import signal
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from rich.console import Console
|
||||
@@ -37,26 +35,6 @@ def _resolve_sandbox_image() -> str:
|
||||
return image
|
||||
|
||||
|
||||
def _resolve_sources_path(args: Any) -> Path:
|
||||
"""Pick the host directory to mount into ``/workspace/sources``.
|
||||
|
||||
- With ``--local-sources``, mount the parent of the first source so
|
||||
the agent can walk down into the actual tree.
|
||||
- Otherwise, a per-run scratch dir under ``$XDG_CACHE_HOME/strix``.
|
||||
"""
|
||||
local_sources: list[dict[str, str]] | None = getattr(args, "local_sources", None)
|
||||
if local_sources:
|
||||
first = local_sources[0]
|
||||
host_path = first.get("host_path") or first.get("source_path") or first.get("path")
|
||||
if host_path:
|
||||
return Path(host_path).expanduser().resolve().parent
|
||||
|
||||
cache_root = os.environ.get("XDG_CACHE_HOME") or str(Path.home() / ".cache")
|
||||
sources = Path(cache_root) / "strix" / "sources" / str(args.run_name)
|
||||
sources.mkdir(parents=True, exist_ok=True)
|
||||
return sources
|
||||
|
||||
|
||||
async def run_cli(args: Any) -> None: # noqa: PLR0915
|
||||
console = Console()
|
||||
|
||||
@@ -200,7 +178,7 @@ async def run_cli(args: Any) -> None: # noqa: PLR0915
|
||||
scan_config=scan_config,
|
||||
scan_id=args.run_name,
|
||||
image=_resolve_sandbox_image(),
|
||||
sources_path=_resolve_sources_path(args),
|
||||
local_sources=getattr(args, "local_sources", None) or [],
|
||||
tracer=tracer,
|
||||
interactive=bool(getattr(args, "interactive", False)),
|
||||
)
|
||||
|
||||
+28
-20
@@ -3,14 +3,12 @@ import asyncio
|
||||
import atexit
|
||||
import contextlib
|
||||
import logging
|
||||
import os
|
||||
import signal
|
||||
import sys
|
||||
import threading
|
||||
from collections.abc import Callable
|
||||
from importlib.metadata import PackageNotFoundError
|
||||
from importlib.metadata import version as pkg_version
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any, ClassVar
|
||||
|
||||
|
||||
@@ -726,6 +724,10 @@ class StrixTUIApp(App): # type: ignore[misc]
|
||||
self._scan_loop: asyncio.AbstractEventLoop | None = None
|
||||
self._scan_stop_event = threading.Event()
|
||||
self._scan_completed = threading.Event()
|
||||
# Captured by ``scan_target`` when the scan thread crashes; read
|
||||
# by ``run_tui`` after ``run_async()`` returns so the user sees
|
||||
# the traceback on stderr instead of just a silent UI hang.
|
||||
self._scan_error: BaseException | None = None
|
||||
|
||||
self._spinner_frame_index: int = 0 # Current animation frame index
|
||||
self._sweep_num_squares: int = 6 # Number of squares in sweep animation
|
||||
@@ -743,18 +745,6 @@ class StrixTUIApp(App): # type: ignore[misc]
|
||||
|
||||
self._setup_cleanup_handlers()
|
||||
|
||||
def _resolve_sources_path(self) -> Path:
|
||||
local_sources = getattr(self.args, "local_sources", None) or []
|
||||
if local_sources:
|
||||
first = local_sources[0]
|
||||
host_path = first.get("host_path") or first.get("source_path") or first.get("path")
|
||||
if host_path:
|
||||
return Path(host_path).expanduser().resolve().parent
|
||||
cache_root = os.environ.get("XDG_CACHE_HOME") or str(Path.home() / ".cache")
|
||||
sources = Path(cache_root) / "strix" / "sources" / str(self.args.run_name)
|
||||
sources.mkdir(parents=True, exist_ok=True)
|
||||
return sources
|
||||
|
||||
def _build_scan_config(self, args: argparse.Namespace) -> dict[str, Any]:
|
||||
return {
|
||||
"scan_id": args.run_name,
|
||||
@@ -1102,7 +1092,7 @@ class StrixTUIApp(App): # type: ignore[misc]
|
||||
|
||||
return self._merge_renderables(renderables)
|
||||
|
||||
def _get_status_display_content(
|
||||
def _get_status_display_content( # noqa: PLR0911
|
||||
self, agent_id: str, agent_data: dict[str, Any]
|
||||
) -> tuple[Text | None, Text, bool]:
|
||||
status = agent_data.get("status", "running")
|
||||
@@ -1141,6 +1131,16 @@ class StrixTUIApp(App): # type: ignore[misc]
|
||||
keymap.append("Send message to retry", style="dim")
|
||||
return (text, keymap, False)
|
||||
|
||||
if status == "failed":
|
||||
error_msg = agent_data.get("error_message", "")
|
||||
text = Text()
|
||||
if error_msg:
|
||||
text.append(error_msg, style="red")
|
||||
else:
|
||||
text.append("Scan failed", style="red")
|
||||
self._stop_dot_animation()
|
||||
return (text, Text(), False)
|
||||
|
||||
if status == "waiting":
|
||||
keymap = Text()
|
||||
keymap.append("Send message to resume", style="dim")
|
||||
@@ -1409,13 +1409,12 @@ class StrixTUIApp(App): # type: ignore[misc]
|
||||
try:
|
||||
if not self._scan_stop_event.is_set():
|
||||
image = load_settings().runtime.image or "strix-sandbox:latest"
|
||||
sources_path = self._resolve_sources_path()
|
||||
loop.run_until_complete(
|
||||
run_strix_scan(
|
||||
scan_config=self.scan_config,
|
||||
scan_id=self.scan_config["run_name"],
|
||||
image=str(image),
|
||||
sources_path=sources_path,
|
||||
local_sources=getattr(self.args, "local_sources", None) or [],
|
||||
tracer=self.tracer,
|
||||
bus=self.bus,
|
||||
interactive=True,
|
||||
@@ -1424,12 +1423,15 @@ class StrixTUIApp(App): # type: ignore[misc]
|
||||
|
||||
except (KeyboardInterrupt, asyncio.CancelledError):
|
||||
logger.info("Scan interrupted by user")
|
||||
except (ConnectionError, TimeoutError):
|
||||
except (ConnectionError, TimeoutError) as e:
|
||||
logging.exception("Network error during scan")
|
||||
except RuntimeError:
|
||||
self._scan_error = e
|
||||
except RuntimeError as e:
|
||||
logging.exception("Runtime error during scan")
|
||||
except Exception:
|
||||
self._scan_error = e
|
||||
except Exception as e:
|
||||
logging.exception("Unexpected error during scan")
|
||||
self._scan_error = e
|
||||
finally:
|
||||
# Best-effort sandbox teardown if early setup failed
|
||||
# before run_strix_scan's own ``finally`` ran.
|
||||
@@ -1995,3 +1997,9 @@ async def run_tui(args: argparse.Namespace) -> None:
|
||||
"""Run strix in interactive TUI mode with textual."""
|
||||
app = StrixTUIApp(args)
|
||||
await app.run_async()
|
||||
# Propagate scan-thread failures: ``app.run_async`` returns normally
|
||||
# when the user quits (ctrl-q) regardless of whether the scan
|
||||
# crashed. Without this re-raise, ``main.py`` would treat a failed
|
||||
# scan as success and print the completion banner.
|
||||
if app._scan_error is not None:
|
||||
raise app._scan_error
|
||||
|
||||
Reference in New Issue
Block a user