fix(phone): accept is_reconnect in PhoneAdapter.connect — unbreak two-way reply
The gateway's platform supervisor calls adapter.connect(is_reconnect=...) (BasePlatformAdapter.connect contract, gateway/run.py:3195). PhoneAdapter.connect was declared connect(self), so every gateway connect raised TypeError, the adapter never came up, and the Phase 2c inbound reply long-poll (_run_reply_loop, spawned only inside connect()) never started. Net: the phone's replies reached the relay and sat buffered forever (0 GET /phone/replies polls); the agent never saw them and never answered back. The 2026-06-29 device round-trip surfaced it: agent->phone push worked (relay->phone), the reply buffered in the relay (proactive <<< ... buffered=1), but the gateway never drained it. - Add `*, is_reconnect: bool = False` to PhoneAdapter.connect (matches the base contract + the ntfy template it was modeled on). Behavior otherwise unchanged. - Add ConnectContractTests regression guard (inspect-based — the live adapter binds to the gateway base class that's absent in CI, which is the blind spot that let this ship). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
64c0c25c25
commit
3018a186c9
@@ -289,12 +289,22 @@ class PhoneAdapter(BasePlatformAdapter): # type: ignore[misc,valid-type]
|
||||
|
||||
# -- Connection lifecycle ----------------------------------------------
|
||||
|
||||
async def connect(self) -> bool:
|
||||
async def connect(self, *, is_reconnect: bool = False) -> bool:
|
||||
"""Open the outbound HTTP client and start the inbound reply loop.
|
||||
|
||||
Outbound is loopback POSTs (no socket held). Inbound is a background
|
||||
long-poll against the relay's ``/phone/replies`` — the phone's replies
|
||||
are buffered by the relay (different process) and drained here.
|
||||
|
||||
The gateway's platform supervisor invokes ``connect(is_reconnect=...)``
|
||||
(the ``BasePlatformAdapter.connect`` contract, forwarded from
|
||||
``gateway/run.py``), so the keyword MUST be accepted. A bare
|
||||
``connect(self)`` raises ``TypeError`` at connect time — the adapter
|
||||
never comes up and the inbound reply loop never starts, so the relay
|
||||
buffers replies that nothing drains (the Phase 2c round-trip silently
|
||||
dies here). ``is_reconnect`` does not change behavior today: we always
|
||||
drain the relay's small bounded reply buffer. Using it to drop stale
|
||||
replies on a cold boot is a possible future refinement.
|
||||
"""
|
||||
if not HTTPX_AVAILABLE:
|
||||
logger.warning("[%s] httpx not installed — cannot push to phone", self.name)
|
||||
|
||||
@@ -351,5 +351,37 @@ class NormalizeReplyTests(_EnvIsolated):
|
||||
self.assertIsNone(pp._normalize_reply(["x"], "home"))
|
||||
|
||||
|
||||
# ── Adapter ↔ gateway connect contract (Phase 2c regression) ───────────────
|
||||
|
||||
|
||||
class ConnectContractTests(unittest.TestCase):
|
||||
"""Guard the gateway's ``connect(is_reconnect=...)`` call contract.
|
||||
|
||||
The gateway supervisor calls ``adapter.connect(is_reconnect=is_reconnect)``
|
||||
(``BasePlatformAdapter.connect`` / ``gateway/run.py``). When
|
||||
``PhoneAdapter.connect`` was declared ``connect(self)``, that call raised
|
||||
``TypeError`` at connect time — the adapter never came up and the inbound
|
||||
reply loop never started, so the two-way reply round-trip silently died
|
||||
with the user's reply stuck buffered in the relay. The live ``PhoneAdapter``
|
||||
binds to the gateway base class (absent here), so we assert via ``inspect``
|
||||
rather than instantiating it.
|
||||
"""
|
||||
|
||||
def test_connect_accepts_is_reconnect_keyword(self) -> None:
|
||||
import inspect
|
||||
|
||||
sig = inspect.signature(pp.PhoneAdapter.connect)
|
||||
self.assertIn(
|
||||
"is_reconnect",
|
||||
sig.parameters,
|
||||
"PhoneAdapter.connect must accept is_reconnect — the gateway always "
|
||||
"passes it; a bare connect(self) raises TypeError and the inbound "
|
||||
"reply loop never starts.",
|
||||
)
|
||||
param = sig.parameters["is_reconnect"]
|
||||
self.assertEqual(param.kind, inspect.Parameter.KEYWORD_ONLY)
|
||||
self.assertEqual(param.default, False)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user