From c3479b5fd3ef1be11a25b5fffee7bd91961159b4 Mon Sep 17 00:00:00 2001 From: glitch-ux Date: Thu, 23 Jul 2026 23:31:46 +0100 Subject: [PATCH] fix(swarm): deliver leader messages to in-process teammates InProcessBackend.send_message writes to the teammate's inbox keyed by the bare agent name (agents//inbox), but start_in_process_teammate polled the inbox keyed by the fully-qualified 'name@team' id. The two paths never matched, so every leader -> teammate message (user_message and mailbox-based shutdown) was silently dropped for in-process teammates. Poll the inbox keyed by config.name so it matches where send_message and the rest of the swarm (write_to_mailbox, the 'leader' idle-notification target) deliver. Add a regression test that drives a real teammate query turn and asserts the leader's message is consumed from the inbox. --- src/openharness/swarm/in_process.py | 4 ++- tests/test_swarm/test_in_process.py | 42 +++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/src/openharness/swarm/in_process.py b/src/openharness/swarm/in_process.py index 30f2c04df..c142c621d 100644 --- a/src/openharness/swarm/in_process.py +++ b/src/openharness/swarm/in_process.py @@ -242,7 +242,9 @@ async def start_in_process_teammate( ) set_teammate_context(ctx) - mailbox = TeammateMailbox(team_name=config.team, agent_id=agent_id) + # Poll the inbox keyed by the bare agent name — that is where the leader's + # send_message() delivers, not the fully-qualified ``name@team`` id. + mailbox = TeammateMailbox(team_name=config.team, agent_id=config.name) logger.debug("[in_process] %s: starting", agent_id) diff --git a/tests/test_swarm/test_in_process.py b/tests/test_swarm/test_in_process.py index ce7e4c165..098dd3179 100644 --- a/tests/test_swarm/test_in_process.py +++ b/tests/test_swarm/test_in_process.py @@ -3,14 +3,17 @@ from __future__ import annotations from pathlib import Path +from types import SimpleNamespace import pytest from openharness.swarm.in_process import ( InProcessBackend, + TeammateAbortController, TeammateContext, get_teammate_context, set_teammate_context, + start_in_process_teammate, ) from openharness.swarm.types import TeammateMessage, TeammateSpawnConfig @@ -155,6 +158,45 @@ async def test_send_message_invalid_agent_id_raises(backend): await backend.send_message("no-at-sign", TeammateMessage(text="hi", from_agent="l")) +async def test_teammate_receives_message_sent_by_leader(backend, tmp_path, monkeypatch): + # send_message writes to the inbox keyed by the bare agent name, so the + # running teammate must poll that same inbox. Regression guard for the key + # mismatch where start_in_process_teammate polled "name@team" instead and + # every leader -> teammate message was silently dropped. + monkeypatch.setattr(Path, "home", lambda: tmp_path) + config = TeammateSpawnConfig( + name="rcvr", + team="myteam", + prompt="wait", + cwd="/tmp", + parent_session_id="s", + ) + + await backend.send_message( + "rcvr@myteam", TeammateMessage(text="work on it", from_agent="leader") + ) + + # Drive a single query turn so the teammate drains its mailbox exactly once. + async def fake_run_query(query_context, messages): + yield SimpleNamespace(type="text"), None + + monkeypatch.setattr("openharness.engine.query.run_query", fake_run_query) + + await start_in_process_teammate( + config=config, + agent_id="rcvr@myteam", + abort_controller=TeammateAbortController(), + query_context=object(), + ) + + from openharness.swarm.mailbox import TeammateMailbox + + inbox = TeammateMailbox(team_name="myteam", agent_id="rcvr") + assert await inbox.read_all(unread_only=True) == [] + delivered = await inbox.read_all(unread_only=False) + assert any(m.payload.get("content") == "work on it" for m in delivered) + + # --------------------------------------------------------------------------- # active_agents / shutdown_all # ---------------------------------------------------------------------------