From 8e25743c4470059e7e2c7e94d1cd16b0a9ff6263 Mon Sep 17 00:00:00 2001 From: octo-patch Date: Fri, 24 Apr 2026 09:38:41 +0800 Subject: [PATCH 1/3] fix: persist scan data on exit and save partial reports (fixes #294) Two related fixes for reports not being saved when Strix exits: 1. Call tracer.cleanup() in main.py's finally block before posthog.end() so that run data is always written to disk regardless of whether the atexit handlers registered in cli.py/tui.py fire (they can be skipped on Windows when the asyncio event loop tears down). 2. Replace the strict all-or-nothing validation in finish_actions.py with a graceful fallback: missing fields get the placeholder "[Not provided by model]" so partial reports are saved rather than silently discarded when the LLM omits a section. Co-Authored-By: Octopus --- strix/interface/main.py | 1 + strix/tools/finish/finish_actions.py | 18 +++++------------- 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/strix/interface/main.py b/strix/interface/main.py index bc88da673..37a6de5b4 100644 --- a/strix/interface/main.py +++ b/strix/interface/main.py @@ -629,6 +629,7 @@ def main() -> None: # noqa: PLR0912, PLR0915 finally: tracer = get_global_tracer() if tracer: + tracer.cleanup() posthog.end(tracer, exit_reason=exit_reason) results_path = Path("strix_runs") / args.run_name diff --git a/strix/tools/finish/finish_actions.py b/strix/tools/finish/finish_actions.py index 79f48e79b..ff78abc98 100644 --- a/strix/tools/finish/finish_actions.py +++ b/strix/tools/finish/finish_actions.py @@ -99,19 +99,11 @@ def finish_scan( if active_agents_error: return active_agents_error - validation_errors = [] - - if not executive_summary or not executive_summary.strip(): - validation_errors.append("Executive summary cannot be empty") - if not methodology or not methodology.strip(): - validation_errors.append("Methodology cannot be empty") - if not technical_analysis or not technical_analysis.strip(): - validation_errors.append("Technical analysis cannot be empty") - if not recommendations or not recommendations.strip(): - validation_errors.append("Recommendations cannot be empty") - - if validation_errors: - return {"success": False, "message": "Validation failed", "errors": validation_errors} + _NOT_PROVIDED = "[Not provided by model]" + executive_summary = (executive_summary or "").strip() or _NOT_PROVIDED + methodology = (methodology or "").strip() or _NOT_PROVIDED + technical_analysis = (technical_analysis or "").strip() or _NOT_PROVIDED + recommendations = (recommendations or "").strip() or _NOT_PROVIDED try: from strix.telemetry.tracer import get_global_tracer From da472ca0d578284738413ad2e915d3ceda31c5fc Mon Sep 17 00:00:00 2001 From: octo-patch Date: Tue, 28 Apr 2026 12:34:24 +0800 Subject: [PATCH 2/3] fix: apply assistant-prefill guard to Anthropic models in interactive mode (fixes #416) When interactive mode is enabled (TUI), the guard that prevents sending a trailing assistant message to the Anthropic API was incorrectly skipped. The Anthropic API rejects such requests with "This model does not support assistant message prefill." regardless of whether the caller is in interactive or non-interactive mode. Updated the condition to also apply the guard when `_is_anthropic()` is true, so Anthropic models are always protected while non-Anthropic models that may support assistant prefill continue to be unaffected in interactive mode. Added three unit tests covering all three branches of the new condition. Co-Authored-By: Octopus --- strix/llm/llm.py | 4 ++- tests/llm/test_prepare_messages.py | 50 ++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 tests/llm/test_prepare_messages.py diff --git a/strix/llm/llm.py b/strix/llm/llm.py index 5e6a01f73..b0b6b22dd 100644 --- a/strix/llm/llm.py +++ b/strix/llm/llm.py @@ -239,7 +239,9 @@ class LLM: conversation_history.extend(compressed) messages.extend(compressed) - if messages[-1].get("role") == "assistant" and not self.config.interactive: + if messages[-1].get("role") == "assistant" and ( + not self.config.interactive or self._is_anthropic() + ): messages.append({"role": "user", "content": "Continue the task."}) if self._is_anthropic() and self.config.enable_prompt_caching: diff --git a/tests/llm/test_prepare_messages.py b/tests/llm/test_prepare_messages.py new file mode 100644 index 000000000..2566f8ee9 --- /dev/null +++ b/tests/llm/test_prepare_messages.py @@ -0,0 +1,50 @@ +"""Tests for LLM._prepare_messages trailing-assistant-message handling.""" +from strix.llm.config import LLMConfig +from strix.llm.llm import LLM + + +def _make_llm(monkeypatch, model_name: str, interactive: bool) -> LLM: + monkeypatch.setenv("STRIX_LLM", model_name) + config = LLMConfig(model_name=model_name, interactive=interactive, enable_prompt_caching=False) + return LLM(config, agent_name=None) + + +def _history_ending_with_assistant() -> list[dict]: + return [ + {"role": "user", "content": "Scan this target."}, + {"role": "assistant", "content": "I found a vulnerability."}, + ] + + +def test_non_interactive_anthropic_adds_user_message(monkeypatch) -> None: + """Non-interactive mode always appends a user message when history ends with assistant.""" + llm = _make_llm(monkeypatch, "claude-sonnet-4-6", interactive=False) + history = _history_ending_with_assistant() + messages = llm._prepare_messages(history) + assert messages[-1]["role"] == "user" + assert messages[-1]["content"] == "Continue the task." + + +def test_interactive_anthropic_adds_user_message(monkeypatch) -> None: + """Interactive mode with Anthropic model must also append a user message. + + Anthropic API rejects messages where the last entry has role 'assistant' + (no assistant prefill support). This should hold regardless of interactive mode. + """ + llm = _make_llm(monkeypatch, "claude-sonnet-4-6", interactive=True) + history = _history_ending_with_assistant() + messages = llm._prepare_messages(history) + assert messages[-1]["role"] == "user" + assert messages[-1]["content"] == "Continue the task." + + +def test_interactive_non_anthropic_does_not_add_user_message(monkeypatch) -> None: + """Interactive mode with a non-Anthropic model keeps the trailing assistant message. + + Non-Anthropic models may support assistant prefill; in interactive mode the + caller (TUI) is responsible for appending the next user message. + """ + llm = _make_llm(monkeypatch, "openai/gpt-5.4", interactive=True) + history = _history_ending_with_assistant() + messages = llm._prepare_messages(history) + assert messages[-1]["role"] == "assistant" From 7259ffebeca3956599612da10843fb3f40bd8f12 Mon Sep 17 00:00:00 2001 From: octo-patch Date: Tue, 28 Apr 2026 13:13:35 +0800 Subject: [PATCH 3/3] log placeholder substitution in finish_scan partial reports MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Greptile review noted the placeholder substitution was silent — adding a warning log when fields are missing makes the substitution visible to operators while keeping the partial-report save behavior intact. The placeholder approach (vs returning a validation error) is intentional — losing the entire scan because the model omitted one section was the original bug from #294, so the trade-off favors saving the report. Co-Authored-By: Octopus --- strix/tools/finish/finish_actions.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/strix/tools/finish/finish_actions.py b/strix/tools/finish/finish_actions.py index ff78abc98..6a3037257 100644 --- a/strix/tools/finish/finish_actions.py +++ b/strix/tools/finish/finish_actions.py @@ -100,11 +100,30 @@ def finish_scan( return active_agents_error _NOT_PROVIDED = "[Not provided by model]" + placeholder_fields = [] + if not (executive_summary or "").strip(): + placeholder_fields.append("executive_summary") + if not (methodology or "").strip(): + placeholder_fields.append("methodology") + if not (technical_analysis or "").strip(): + placeholder_fields.append("technical_analysis") + if not (recommendations or "").strip(): + placeholder_fields.append("recommendations") + executive_summary = (executive_summary or "").strip() or _NOT_PROVIDED methodology = (methodology or "").strip() or _NOT_PROVIDED technical_analysis = (technical_analysis or "").strip() or _NOT_PROVIDED recommendations = (recommendations or "").strip() or _NOT_PROVIDED + if placeholder_fields: + import logging + + logging.warning( + "finish_scan: model omitted required field(s) %s; " + "saving partial report with placeholder text", + placeholder_fields, + ) + try: from strix.telemetry.tracer import get_global_tracer