fix(acp): defer attach notices until options are ready

Finish the awaited service-tier read before scheduling post-attach command
notices, so an editor has its session reply before the command menu. Read
the latest edit policy after that await for both reply representations.

The command-order regression fails on 42500a10d9. A second regression
catches mode changes during discovery. Both pass after the correction.
This commit is contained in:
BearHuddleston committed 2026-11-04 15:43:42 +00:00
1 parent ea9cabf491
commit d761fb9d79
2 files changed
+66 -6

No files matched your search

+10 -6
View File
@@ -300,22 +300,26 @@ class GatewayACPAgent(GatewayReplyMixin, acp.Agent):
"""Main's new/load/resume/fork reply fields (model picker, modes, provenance ``_meta``); the
command list and context indicator follow once the reply is queued, as main scheduled them."""
from acp_adapter.gateway_session_meta import model_state, provenance_meta, session_config_options, session_modes
from acp_adapter.modes import MODE_TO_EDIT_APPROVAL_POLICY
from acp_adapter.modes import EDIT_APPROVAL_POLICY_CONFIG_ID, MODE_TO_EDIT_APPROVAL_POLICY
title = info.get("title")
if isinstance(title, str) and title.strip():
self._titles[session_id] = title
models = await asyncio.to_thread(model_state, info)
meta = await asyncio.to_thread(provenance_meta, self._home / "state.db", editor_id, session_id)
mode = self._modes.setdefault(session_id, session_modes(info).current_mode_id)
options = await session_config_options(self._gateway, session_id, models, info, mode)
# ``_adopt`` seeded the mode from the snapshot: a change the pump projected since is what the reply shows.
policy = MODE_TO_EDIT_APPROVAL_POLICY[self._modes[session_id]]
modes = session_modes({"edit_approval_policy": policy})
for option in options:
if option.id == EDIT_APPROVAL_POLICY_CONFIG_ID:
option.current_value = policy
if self._conn:
task = asyncio.get_running_loop().create_task(self._after_attach(session_id, editor_id))
self._background.add(task)
task.add_done_callback(self._background.discard)
# ``_adopt`` seeded the mode from the snapshot: a change the pump projected since is what the reply shows.
modes = session_modes({"edit_approval_policy": MODE_TO_EDIT_APPROVAL_POLICY[self._modes.setdefault(
session_id, session_modes(info).current_mode_id)]})
return {"models": models, "modes": modes, "field_meta": meta,
"config_options": await session_config_options(
self._gateway, session_id, models, info, modes.current_mode_id)}
"config_options": options}
async def _after_attach(self, session_id, editor_id):
from acp_adapter.gateway_session_meta import available_commands_update
@@ -0,0 +1,56 @@
"""Attach command notices follow the reply even when speed discovery yields."""
import asyncio
from unittest.mock import AsyncMock
import pytest
from acp_adapter.gateway_server import GatewayACPAgent
@pytest.mark.asyncio
async def test_speed_discovery_does_not_publish_commands_before_the_attach_reply(monkeypatch):
monkeypatch.setattr('acp_adapter.gateway_session_meta.model_state', lambda info: None)
monkeypatch.setattr('acp_adapter.gateway_session_meta.provenance_meta', lambda *args: None)
agent = GatewayACPAgent()
reply_returned = False
command_order = []
async def session_update(*, session_id, update):
if update.session_update == 'available_commands_update':
command_order.append(reply_returned)
async def rpc(method, **params):
if method == 'config.get':
await asyncio.gather(*tuple(agent._background))
return {'value': 'normal', 'options': ['normal', 'fast']}
return {}
owner = AsyncMock()
owner.rpc.side_effect = rpc
agent._gateway = owner
monkeypatch.setattr(agent, '_client', AsyncMock(return_value=owner))
agent._conn = AsyncMock()
agent._conn.session_update.side_effect = session_update
fields = await agent._session_fields('session', {'model': 'gpt-6.1-sol'}, 'session')
reply_returned = True
await asyncio.gather(*tuple(agent._background))
assert any(option.id == 'service_tier' for option in fields['config_options'])
assert command_order == [True]
@pytest.mark.asyncio
async def test_mode_changed_during_speed_discovery_is_used_by_the_attach_reply(monkeypatch):
monkeypatch.setattr('acp_adapter.gateway_session_meta.model_state', lambda info: None)
monkeypatch.setattr('acp_adapter.gateway_session_meta.provenance_meta', lambda *args: None)
agent = GatewayACPAgent()
async def rpc(method, **params):
await agent._project_mode('session', 'session', {'edit_approval_policy': 'session'})
return {'value': 'normal', 'options': ['normal', 'fast']}
owner = AsyncMock()
owner.rpc.side_effect = rpc
agent._gateway = owner
fields = await agent._session_fields('session', {'model': 'gpt-6.1-sol'}, 'session')
assert fields['modes'].current_mode_id == 'dont_ask'
assert next(option for option in fields['config_options'] if option.category == 'mode').current_value == 'session'