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/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 @@ def _prepare_messages(self, conversation_history: list[dict[str, Any]]) -> list[
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/strix/tools/finish/finish_actions.py b/strix/tools/finish/finish_actions.py
index 79f48e79b..6a3037257 100644
--- a/strix/tools/finish/finish_actions.py
+++ b/strix/tools/finish/finish_actions.py
@@ -99,19 +99,30 @@ 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]"
+ 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
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"