Stop the protocol from making passive hosts poll (v18) - #69
Merged
Conversation
A host that cannot be woken by an inbound message pays a full turn for every listen(), and the protocol pushed it straight into that cost. Three corrections: - The room does keep messages for a peer that has joined. Its queue holds what arrives between polls and delivers the backlog whole on the next listen(), so an agent need not poll to stay reachable. State the limits too: nothing is kept for a peer that never joined or has left, and the queue is bounded. - One ask per turn stays the default, with an explicit batching exception when every listen() costs a turn. Related questions only. - Rank the three listening strategies instead of assuming a watcher: wake on process exit, one long blocking read, or a single listen() and hand the turn back. Also fixes the blocking figure, which said ~35s where the hub clamps at 25.
PROTOCOL_TEXT now tells agents a joined peer stays reachable without polling. Nothing asserted it. Three messages land for a peer that has never called /receive; one later call drains them all, in order.
The deployed mirror still assumed a watcher is always available, still stated one ask per turn as absolute, and had no equivalent of the room-is-live block at all. It now carries the three-strategy ladder, the batching exception, and the queue guarantee.
Covers the cost model (a turn buys 25s of waiting), what the peer queue does and does not guarantee, the three listening strategies ranked, and when batching questions beats one ask per turn. No existing doc owned this: ARCHITECTURE is structural, the runbook is for the human operator.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What changed
An agent on a passive MCP host cannot be woken by an inbound message, so every
listen()costs it a full turn. The protocol was steering it straight intothat cost. A Claude Desktop session recently burned about twenty empty
listen()calls in one exchange, which is what prompted this.Three fixes to
PROTOCOL_TEXT, soPROTOCOL_VERSIONgoes 17 to 18:polled speculatively. A peer that has joined owns a queue: it holds whatever
arrives between polls and delivers the backlog whole on the next
listen().Idling loses nothing. The real limits are stated too, since they do exist:
nothing is kept for a peer that never joined or has left, and the queue is a
bounded ring buffer that drops oldest under flood.
exception when every
listen()costs a turn: related questions may be batchedinto one numbered message asking for a numbered reply. The rule was protecting
a human's ability to follow the room, and a structured batch still does that.
blocking figure at ~35s, where the hub clamps at 25. It now gives the right
number and ranks three strategies: wake on watcher exit, one long blocking
read of the watcher's output, or a single
listen()then hand the turn backto the operator.
Also here:
caucus-protocol.mdhad drifted (no equivalent of the mailbox blockat all), and a new
docs/operating-cheaply.mdcovers the cost model, the queueguarantee and its edges, the strategy ladder, and when batching wins.
No logic changed. Connected bridges will see
protocol_staleon their nextjoinand re-read the text once, which is the designed behaviour.A follow-up PR raises the poll ceiling itself. Worth knowing why it is separate:
LONG_POLL_SECONDSis a clamp, not a floor, and every client sends an explicittimeout, so raising it alone changes nothing. The effective lever is
listen()'sown default, and moving it means reworking the hardcoded httpx timeouts first.
How I verified it
pytest: green.test_protocol_version_is_17renamed and rewritten forrevision 18; the four phrases pinned by
test_hub_api.pyandtest_protocol_md.pystill pass untouched.test_messages_sent_while_peer_has_no_poll_in_flight_are_queued: threemessages land for a peer that has never called
/receive, and one later calldrains them in order. The protocol now promises agents this, and nothing
asserted it before.
ruff check src/ tests/andmypy src/clean.python smoke_test.pyprintsALL CHECKS PASSED.Note, unrelated to this branch:
tests/test_dependency_bounds.pyfails in mylocal venv because its installed metadata still says
mcp[cli]>=1.2whilepyproject.tomldeclares>=1.9,<2. A reinstall fixes it; nothing here touchesdependencies.
Checklist
## [Unreleased]inCHANGELOG.md.PROTOCOL_TEXTchanged inhub.py,PROTOCOL_VERSIONwas bumped too.