From f082565a17b60f81e7cb8227aa7e7e6b4d011b66 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sat, 24 Oct 2026 15:50:48 -0400 Subject: [PATCH 1/2] Settle managed turns through TurnState.settles, not the deleted matches Core #2943 deleted TurnState.matches, so receive() at conversation_turn.py:271 raised AttributeError on every idle turn publication, including the owner snapshot Toad publishes when switching threads. Ask Core's TurnState whether the publication settles the held declaration, and delete the three matches_settlement methods nothing calls. The compaction peer now passes typed 128-bit turn ids, which #2943 also required. Requires OpenHCSDev/agent-comms#3076. Co-Authored-By: Claude Opus 5.5 --- src/toad/conversation_turn.py | 11 +---------- tests/support/peers/typed_turn_events.py | 5 +++-- tests/textual/test_compaction_turn_events.py | 4 ++-- 3 files changed, 6 insertions(+), 14 deletions(-) diff --git a/src/toad/conversation_turn.py b/src/toad/conversation_turn.py index 16f646958..81fb28195 100644 --- a/src/toad/conversation_turn.py +++ b/src/toad/conversation_turn.py @@ -60,9 +60,6 @@ def session_state(self) -> str | None: ... def accepts_update(self, update) -> bool: return update.matches_turn(IdleTurn.reported_turn) - def matches_settlement(self, turn_id) -> bool: - return not turn_id - def captured_snapshot(self, widgets: tuple[Widget, ...]): """Each original claim owns capture while this source is active.""" from toad.widgets.committed_presentation import CommitParticipant @@ -112,9 +109,6 @@ class AgentTurn(TurnOwner): activity_state = ActivityState.WORKING session_state = "busy" - def matches_settlement(self, turn_id) -> bool: - return self.managed_id == turn_id - def with_activity(self, activity: str) -> TurnOwner: return replace(self, activity=activity or "Thinking…") @@ -169,9 +163,6 @@ def session_state(self): def accepts_update(self, update) -> bool: return update.matches_turn(self.state.active.reported_turn) - def matches_settlement(self, turn_id): - return self.state.matches(turn_id) - def with_activity(self, activity: str): # Text, tool and heartbeat rendering cannot change backend phase. return self @@ -268,7 +259,7 @@ def accepts(self, message): return message.event.sequence == self.sequence def receive(self, state: TurnState): - if not state.active.phase.busy and not state.matches(self.owner.managed_id): + if not state.settles(self.owner.state.active): return False owner = ManagedTurn(state) if owner == self.owner: diff --git a/tests/support/peers/typed_turn_events.py b/tests/support/peers/typed_turn_events.py index 30314c6e1..4c0a4afc6 100644 --- a/tests/support/peers/typed_turn_events.py +++ b/tests/support/peers/typed_turn_events.py @@ -16,6 +16,7 @@ from agent_comms.agent_event_updates import AcpEventConsumer from agent_comms.controller import SessionUpdateSink from agent_comms.coordinator import Coordination +from agent_comms.field_codec import TurnId from turn_source import turn_source SESSION = "pilot" @@ -35,14 +36,14 @@ async def produce(wire_root: Path, project: Path): name = producer.sessions.require(SESSION) async with AsyncExitStack() as turns: retired_owner = await Coordination.run_worker(partial( - producer.turns.acquire_turn, turns, SESSION, name, "retired-producer", "Authored protocol; no input")) + producer.turns.acquire_turn, turns, SESSION, name, TurnId("5e7a1d0c3b2f4e6a8c9d0b1a2f3e4d5c"), "Authored protocol; no input")) retired_lease = retired_owner.turn_lease retired = await Coordination.run_worker(partial(producer.turns.current_turn_update, SESSION)) producer.on_connect(Pipe()) consumer = AcpEventConsumer(producer, SESSION, producer._runtime) async with AsyncExitStack() as turns: await Coordination.run_worker(partial( - producer.turns.acquire_turn, turns, SESSION, name, "actual-producer", "Authored protocol; no input")) + producer.turns.acquire_turn, turns, SESSION, name, TurnId("a1c2e3f4b5d6e7f8091a2b3c4d5e6f70"), "Authored protocol; no input")) await producer.turns.publish_turn_state(consumer) await consumer.dispatch(agent_events.CompactionStart()) await consumer.dispatch(agent_events.CompactionEnd( diff --git a/tests/textual/test_compaction_turn_events.py b/tests/textual/test_compaction_turn_events.py index 3b376815d..0164a9e83 100644 --- a/tests/textual/test_compaction_turn_events.py +++ b/tests/textual/test_compaction_turn_events.py @@ -51,10 +51,10 @@ async def consume_until(member): await consume_until(CompactionChangedUpdate) assert "Compacting context" not in view.turns.owner.activity stale = await consume_until(TurnChangedUpdate) - assert not stale.state.active.phase.busy and not stale.state.matches(turn_id) + assert not stale.state.settles(active.state.active) assert agent.current_turn.managed_id == view.turns.managed_id == turn_id terminal = await consume_until(TurnChangedUpdate) - assert not terminal.state.active.phase.busy and terminal.state.matches(turn_id) + assert not terminal.state.active.phase.busy and terminal.state.settles(active.state.active) assert agent.current_turn.managed_id is view.turns.managed_id is None assert not agent.current_turn.busy and not view.turns.owner.busy await consume_until(GoalChangedUpdate) From a48d7a944c21d30ec531b975bf67ef04fec6b227 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sat, 24 Oct 2026 15:54:24 -0400 Subject: [PATCH 2/2] Relock agent-comms onto the merged TurnState.settles owner Pins agent-comms to 208a5b2ee, the merge of OpenHCSDev/agent-comms#3076, so this branch never ships calling state.settles against a Core that lacks it. Co-Authored-By: Claude Opus 5.5 --- uv.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/uv.lock b/uv.lock index b19d88990..b7ccab021 100644 --- a/uv.lock +++ b/uv.lock @@ -21,7 +21,7 @@ wheels = [ [[package]] name = "agent-comms" version = "0.1.0" -source = { git = "https://github.com/OpenHCSDev/agent-comms.git?branch=main#ef711ce9b27be28b6c9eda4e6c1e59cfcd2a5c66" } +source = { git = "https://github.com/OpenHCSDev/agent-comms.git?branch=main#208a5b2ee96e0c3eb9308f254adb1ddd4c6e4b8a" } dependencies = [ { name = "agent-client-protocol" }, { name = "aiohttp" },