fix: save and restore sub-agent state in checkpoints

Previously checkpoints only saved tracer.chat_messages and
tracer.vulnerability_reports, leaving tracer.agents and
tracer.tool_executions empty on resume — so all sub-agents
(both in-progress and completed) were invisible after resuming.

Changes:
- checkpoint.py: add tracer_agents, tracer_tool_executions,
  tracer_next_execution_id fields to CheckpointModel; populate
  them in CheckpointManager.save() from the live tracer
- cli.py: on resume, restore agents dict, tool_executions dict,
  and advance _next_execution_id to avoid ID collisions
- tui.py: same restore logic so TUI sidebar shows all previous
  agents and their tool results immediately on resume

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
root 2026-03-19 08:26:17 +01:00
parent 8a86a92e48
commit fd5366f612
3 changed files with 39 additions and 0 deletions

View file

@ -204,9 +204,18 @@ async def run_cli(args: Any) -> None: # noqa: PLR0915
tracer.set_scan_config(scan_config)
# Added for Resume Feature — pre-populate tracer so stats/vulns are correct
# Also restores sub-agent registry and tool executions so the full run is visible
if is_resuming and checkpoint_data:
tracer.chat_messages.extend(checkpoint_data.tracer_chat_messages)
tracer.vulnerability_reports.extend(checkpoint_data.tracer_vulnerability_reports)
# Restore every agent (root + sub-agents) with their last-known status
tracer.agents.update(checkpoint_data.tracer_agents)
# Restore tool execution records; keys were serialised as str, restore as int
for k, v in checkpoint_data.tracer_tool_executions.items():
tracer.tool_executions[int(k)] = v
# Advance the ID counter so new executions don't overwrite saved ones
if checkpoint_data.tracer_next_execution_id > tracer._next_execution_id:
tracer._next_execution_id = checkpoint_data.tracer_next_execution_id
# Added for Resume Feature — show resume banner + replay previous output
if is_resuming and checkpoint_data:

View file

@ -711,10 +711,20 @@ class StrixTUIApp(App): # type: ignore[misc]
# Added for Resume Feature — pre-populate tracer with checkpoint data so
# stats and findings reflect the full scan history including past sessions.
# Also restores sub-agents and their tool executions so the TUI sidebar
# shows every agent (completed or in-progress) from the previous session.
_cp = getattr(args, "_checkpoint_data", None)
if _cp and getattr(args, "resume_from_checkpoint", False):
self.tracer.chat_messages.extend(_cp.tracer_chat_messages)
self.tracer.vulnerability_reports.extend(_cp.tracer_vulnerability_reports)
# Restore full agent registry (root + all sub-agents)
self.tracer.agents.update(_cp.tracer_agents)
# Restore tool execution records (keys were serialised as str)
for k, v in _cp.tracer_tool_executions.items():
self.tracer.tool_executions[int(k)] = v
# Advance execution ID counter to avoid collisions
if _cp.tracer_next_execution_id > self.tracer._next_execution_id:
self.tracer._next_execution_id = _cp.tracer_next_execution_id
self.agent_nodes: dict[str, TreeNode] = {}

View file

@ -41,6 +41,13 @@ class CheckpointModel(BaseModel):
tracer_chat_messages: list[dict[str, Any]] = Field(default_factory=list)
tracer_vulnerability_reports: list[dict[str, Any]] = Field(default_factory=list)
# Added for sub-agent persistence: full agent registry and tool execution log
# tracer.agents → every agent (root + sub) with status, name, task, parent_id
# tracer.tool_executions → every tool call result across all agents
tracer_agents: dict[str, Any] = Field(default_factory=dict)
tracer_tool_executions: dict[str, Any] = Field(default_factory=dict) # key is str(int)
tracer_next_execution_id: int = 1 # restore the ID counter so new IDs don't collide
# Original scan configuration (passed to execute_scan)
scan_config: dict[str, Any] = Field(default_factory=dict)
@ -100,11 +107,21 @@ class CheckpointManager:
tracer_chat_messages: list[dict[str, Any]] = []
tracer_vulnerability_reports: list[dict[str, Any]] = []
tracer_agents: dict[str, Any] = {}
tracer_tool_executions: dict[str, Any] = {}
tracer_next_execution_id: int = 1
if tracer:
tracer_chat_messages = list(getattr(tracer, "chat_messages", []))
tracer_vulnerability_reports = list(
getattr(tracer, "vulnerability_reports", [])
)
# Added for sub-agent persistence — capture full agent registry
# and all tool execution records so sub-agents are fully restored
tracer_agents = dict(getattr(tracer, "agents", {}))
# tool_executions keys are ints; serialise as strings for JSON
raw_execs = getattr(tracer, "tool_executions", {})
tracer_tool_executions = {str(k): v for k, v in raw_execs.items()}
tracer_next_execution_id = getattr(tracer, "_next_execution_id", 1)
checkpoint = CheckpointModel(
run_name=self.run_name,
@ -114,6 +131,9 @@ class CheckpointManager:
agent_state=state_dict,
tracer_chat_messages=tracer_chat_messages,
tracer_vulnerability_reports=tracer_vulnerability_reports,
tracer_agents=tracer_agents,
tracer_tool_executions=tracer_tool_executions,
tracer_next_execution_id=tracer_next_execution_id,
scan_config=scan_config,
)