From fade37025d095974be61c41f0d2031b119f7e811 Mon Sep 17 00:00:00 2001 From: alex s <46074070+bearsyankees@users.noreply.github.com> Date: Mon, 27 Jul 2026 18:51:02 -0400 Subject: [PATCH] fix viewer tool call collisions across agents (#917) --- strix/interface/tui/live_view.py | 12 +++--- tests/test_viewer.py | 70 ++++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 5 deletions(-) diff --git a/strix/interface/tui/live_view.py b/strix/interface/tui/live_view.py index ec4847dc..4401fb30 100644 --- a/strix/interface/tui/live_view.py +++ b/strix/interface/tui/live_view.py @@ -20,7 +20,7 @@ class TuiLiveView: self.events: list[dict[str, Any]] = [] self._next_event_id = 1 self._open_assistant_event_by_agent: dict[str, dict[str, Any]] = {} - self._tool_event_by_call_id: dict[str, dict[str, Any]] = {} + self._tool_event_by_agent_and_call_id: dict[tuple[str, str], dict[str, Any]] = {} def hydrate_from_run_dir(self, run_dir: Path) -> None: state_dir = runtime_state_dir(run_dir) @@ -223,7 +223,8 @@ class TuiLiveView: timestamp: str | None = None, ) -> None: call_id = call["call_id"] - existing = self._tool_event_by_call_id.get(call_id) + event_key = (agent_id, call_id) + existing = self._tool_event_by_agent_and_call_id.get(event_key) tool_data = { "tool_name": call["tool_name"], "args": call["args"], @@ -233,7 +234,7 @@ class TuiLiveView: } if existing is None: event = self._append_event(agent_id, "tool", tool_data, timestamp=timestamp) - self._tool_event_by_call_id[call_id] = event + self._tool_event_by_agent_and_call_id[event_key] = event else: existing["data"].update(tool_data) self._bump_event(existing, timestamp=timestamp) @@ -249,7 +250,8 @@ class TuiLiveView: timestamp: str | None = None, ) -> None: call_id = output["call_id"] - event = self._tool_event_by_call_id.get(call_id) + event_key = (agent_id, call_id) + event = self._tool_event_by_agent_and_call_id.get(event_key) if event is None: event = self._append_event( agent_id, @@ -263,7 +265,7 @@ class TuiLiveView: }, timestamp=timestamp, ) - self._tool_event_by_call_id[call_id] = event + self._tool_event_by_agent_and_call_id[event_key] = event result = _parse_json_value(output["output"]) event["data"]["result"] = result diff --git a/tests/test_viewer.py b/tests/test_viewer.py index faa306f6..7f0c1886 100644 --- a/tests/test_viewer.py +++ b/tests/test_viewer.py @@ -4,6 +4,7 @@ from __future__ import annotations import json import os +import sqlite3 import urllib.error import urllib.request from typing import TYPE_CHECKING @@ -86,6 +87,75 @@ def test_build_run_state_from_agents_json(tmp_path: Path) -> None: assert state["events"] == [] +def test_build_run_state_keeps_same_call_id_separate_per_agent(tmp_path: Path) -> None: + run_dir = _make_run(tmp_path, "tools", status="completed", end_time=None) + agents_db = run_dir / ".state" / "agents.db" + rows = [ + ( + "root", + { + "type": "function_call", + "call_id": "exec_command_0", + "name": "exec_command", + "arguments": json.dumps({"cmd": "echo root"}), + }, + ), + ( + "root", + { + "type": "function_call_output", + "call_id": "exec_command_0", + "output": json.dumps({"success": True, "output": "root"}), + }, + ), + ( + "child", + { + "type": "function_call", + "call_id": "exec_command_0", + "name": "exec_command", + "arguments": json.dumps({"cmd": "echo child"}), + }, + ), + ( + "child", + { + "type": "function_call_output", + "call_id": "exec_command_0", + "output": json.dumps({"success": True, "output": "child"}), + }, + ), + ] + with sqlite3.connect(agents_db) as conn: + conn.execute( + """ + create table agent_messages ( + id integer primary key, + session_id text not null, + message_data text not null, + created_at text not null + ) + """ + ) + conn.executemany( + """ + insert into agent_messages (session_id, message_data, created_at) + values (?, ?, '2026-01-01T00:00:00+00:00') + """, + [(agent_id, json.dumps(message)) for agent_id, message in rows], + ) + + state = build_run_state(run_dir) + tools = [event for event in state["events"] if event["type"] == "tool"] + + assert len(tools) == 2 + by_agent = {event["agent_id"]: event for event in tools} + assert by_agent["root"]["data"]["args"] == {"cmd": "echo root"} + assert by_agent["root"]["data"]["result"]["output"] == "root" + assert by_agent["child"]["data"]["args"] == {"cmd": "echo child"} + assert by_agent["child"]["data"]["result"]["output"] == "child" + + def _get(url: str, *, cookie: str | None = None) -> tuple[int, str, bytes]: headers = {"Cookie": cookie} if cookie else {} req = urllib.request.Request(url, headers=headers) # noqa: S310 - localhost test server