From 87078de4f1f92e00ec81cb0d2c3f0a8eff5bb3ad Mon Sep 17 00:00:00 2001 From: root Date: Thu, 19 Mar 2026 10:20:10 +0100 Subject: [PATCH] Fix sub-agents not re-spawning after resume MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root causes: 1. Resume message said "re-spawn sub-agents" but didn't say WHICH ones or that old agent IDs are dead — LLM tried to interact with old IDs and got confused. 2. send_message_to_agent returned unhelpful "not found" error when the LLM used old (dead) agent IDs after resume. Fixes: - _build_resume_context_message / _inject_resume_context_message now accept the full checkpoint_data object and extract tracer_agents to list every non-completed sub-agent by name and task. The LLM now knows exactly which agents to re-spawn. - Message explicitly forbids interacting with any agent ID from history and instructs the LLM to call view_agent_graph first. - send_message_to_agent returns an actionable error when target is not found: explains it may be a dead session ID and tells the LLM to use view_agent_graph then create_agent. Co-Authored-By: Claude Sonnet 4.6 --- strix/interface/cli.py | 50 +++++++++++++++---- strix/interface/tui.py | 46 +++++++++++++---- .../agents_graph/agents_graph_actions.py | 7 ++- 3 files changed, 83 insertions(+), 20 deletions(-) diff --git a/strix/interface/cli.py b/strix/interface/cli.py index edb73b3d..aa249755 100644 --- a/strix/interface/cli.py +++ b/strix/interface/cli.py @@ -97,22 +97,52 @@ def _replay_previous_output( console.print() -def _build_resume_context_message(state: Any, iteration: int) -> None: +def _build_resume_context_message(state: Any, checkpoint_data: Any) -> None: """Inject a user message telling the LLM it was interrupted and must continue. Added for Resume Feature — prevents the model from calling finish_scan or - agent_finish just because the message history ends abruptly (e.g. a dangling - sub-agent tool call that never got a result). + agent_finish just because the history ends abruptly, and explicitly lists + which sub-agents were alive so the LLM knows what to re-spawn. """ + iteration = checkpoint_data.iteration + + # Collect sub-agents that were NOT completed at the time of interruption + # so the LLM knows exactly what to re-create. + dead_sub_agents = [] + for agent_id, node in (checkpoint_data.tracer_agents or {}).items(): + if node.get("parent_id") is None: + continue # skip root agent + status = node.get("status", "unknown") + if status not in ("completed", "finished", "stopped", "error", "failed"): + dead_sub_agents.append({ + "name": node.get("name", "sub-agent"), + "task": (node.get("task") or "")[:300], + "status": status, + }) + + sub_agent_section = "" + if dead_sub_agents: + lines = [ + "\n\nThe following sub-agents were ACTIVE at the time of interruption. " + "They no longer exist — their agent IDs are completely invalid. " + "Re-spawn each one if their work is not yet reflected in the findings above:" + ] + for sa in dead_sub_agents: + lines.append(f" • {sa['name']} (was doing: {sa['task']})") + sub_agent_section = "\n".join(lines) + msg = ( f"[SYSTEM - SCAN RESUMED]\n" f"This penetration test was interrupted at iteration {iteration}. " - f"All sub-agents that were running have been terminated along with their sandbox. " - f"A fresh sandbox will be created automatically. " - f"Review the conversation history above to understand what has already been done, " - f"then CONTINUE the penetration test from where it left off. " - f"Do NOT call finish_scan or agent_finish unless all testing is genuinely complete. " - f"Re-spawn any sub-agents needed to continue uncompleted work." + f"ALL previous sub-agents have been terminated and their agent IDs no longer exist in the graph. " + f"A fresh sandbox will be created automatically.\n\n" + f"CRITICAL: Do NOT attempt to send_message_to_agent or interact with ANY agent ID " + f"that appears in the conversation history above — every one of those IDs is now dead. " + f"Call view_agent_graph to see the current graph (only you, the root agent, exist now)." + f"{sub_agent_section}\n\n" + f"Review the history to understand what was done, then CONTINUE the penetration test. " + f"Re-spawn sub-agents for any incomplete work. " + f"Do NOT call finish_scan unless all testing is genuinely complete." ) state.add_message("user", msg) @@ -156,7 +186,7 @@ async def run_cli(args: Any) -> None: # noqa: PLR0915 # or agent_finish just because the history ended abruptly. # Without this the model sees a dangling tool call (sub-agent that was # killed mid-execution) and may decide the task is complete or broken. - _build_resume_context_message(resumed_state, checkpoint_data.iteration) + _build_resume_context_message(resumed_state, checkpoint_data) start_text = Text() if is_resuming: diff --git a/strix/interface/tui.py b/strix/interface/tui.py index 94a52daa..0981f9dd 100644 --- a/strix/interface/tui.py +++ b/strix/interface/tui.py @@ -41,21 +41,49 @@ from strix.telemetry.tracer import Tracer, set_global_tracer logger = logging.getLogger(__name__) -def _inject_resume_context_message(state: Any, iteration: int) -> None: +def _inject_resume_context_message(state: Any, checkpoint_data: Any) -> None: """Inject a user message telling the LLM it was interrupted and must continue. Added for Resume Feature — prevents the model from calling finish_scan or - agent_finish just because the message history ends abruptly. + agent_finish just because the history ends abruptly, and explicitly lists + which sub-agents were alive so the LLM knows what to re-spawn. """ + iteration = checkpoint_data.iteration + + dead_sub_agents = [] + for agent_id, node in (checkpoint_data.tracer_agents or {}).items(): + if node.get("parent_id") is None: + continue + status = node.get("status", "unknown") + if status not in ("completed", "finished", "stopped", "error", "failed"): + dead_sub_agents.append({ + "name": node.get("name", "sub-agent"), + "task": (node.get("task") or "")[:300], + }) + + sub_agent_section = "" + if dead_sub_agents: + lines = [ + "\n\nThe following sub-agents were ACTIVE at the time of interruption. " + "They no longer exist — their agent IDs are completely invalid. " + "Re-spawn each one if their work is not yet reflected in the findings above:" + ] + for sa in dead_sub_agents: + lines.append(f" • {sa['name']} (was doing: {sa['task']})") + sub_agent_section = "\n".join(lines) + msg = ( f"[SYSTEM - SCAN RESUMED]\n" f"This penetration test was interrupted at iteration {iteration}. " - f"All sub-agents that were running have been terminated along with their sandbox. " - f"A fresh sandbox will be created automatically. " - f"Review the conversation history above to understand what has already been done, " - f"then CONTINUE the penetration test from where it left off. " - f"Do NOT call finish_scan or agent_finish unless all testing is genuinely complete. " - f"Re-spawn any sub-agents needed to continue uncompleted work." + f"ALL previous sub-agents have been terminated and their agent IDs no longer exist in the graph. " + f"A fresh sandbox will be created automatically.\n\n" + f"CRITICAL: Do NOT attempt to send_message_to_agent or interact with ANY agent ID " + f"that appears in the conversation history above — every one of those IDs is now dead. " + f"Call view_agent_graph to see the current graph (only you, the root agent, exist now)." + f"{sub_agent_section}\n\n" + f"Review the history to understand what was done, then CONTINUE the penetration test. " + f"Re-spawn sub-agents for any incomplete work. " + f"Do NOT call finish_scan unless all testing is genuinely complete." ) state.add_message("user", msg) @@ -812,7 +840,7 @@ class StrixTUIApp(App): # type: ignore[misc] resumed_state.llm_failed = False # Inject resume-context message so the LLM does NOT call finish_scan # or agent_finish just because the history ended abruptly. - _inject_resume_context_message(resumed_state, _cp.iteration) + _inject_resume_context_message(resumed_state, _cp) config["state"] = resumed_state _mgr = getattr(args, "_checkpoint_manager", None) diff --git a/strix/tools/agents_graph/agents_graph_actions.py b/strix/tools/agents_graph/agents_graph_actions.py index a351ee32..79c5a163 100644 --- a/strix/tools/agents_graph/agents_graph_actions.py +++ b/strix/tools/agents_graph/agents_graph_actions.py @@ -304,7 +304,12 @@ def send_message_to_agent( if target_agent_id not in _agent_graph["nodes"]: return { "success": False, - "error": f"Target agent '{target_agent_id}' not found in graph", + "error": ( + f"Target agent '{target_agent_id}' not found in the agent graph. " + "This ID may be from a previous session that was interrupted. " + "Call view_agent_graph to see which agents currently exist, " + "then use create_agent to re-spawn any needed sub-agents." + ), "message_id": None, }