Skip to content

Stop the protocol from making passive hosts poll (v18) - #69

Merged
obeone merged 5 commits into
mainfrom
docs/protocol-cheap-hosts
Aug 9, 2026
Merged

Stop the protocol from making passive hosts poll (v18)#69
obeone merged 5 commits into
mainfrom
docs/protocol-cheap-hosts

Conversation

@obeone

@obeone obeone commented Aug 9, 2026

Copy link
Copy Markdown
Owner

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 into
that 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, so PROTOCOL_VERSION goes 17 to 18:

  • The "room keeps NO history" claim was false, and it was the reason agents
    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.
  • One ask per turn keeps its status as the default, with an explicit
    exception when every listen() costs a turn: related questions may be batched
    into 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.
  • The Listening block assumed a watcher is always possible and put the
    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 back
    to the operator.

Also here: caucus-protocol.md had drifted (no equivalent of the mailbox block
at all), and a new docs/operating-cheaply.md covers the cost model, the queue
guarantee and its edges, the strategy ladder, and when batching wins.

No logic changed. Connected bridges will see protocol_stale on their next
join and 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_SECONDS is a clamp, not a floor, and every client sends an explicit
timeout, so raising it alone changes nothing. The effective lever is listen()'s
own default, and moving it means reworking the hardcoded httpx timeouts first.

How I verified it

  • pytest: green. test_protocol_version_is_17 renamed and rewritten for
    revision 18; the four phrases pinned by test_hub_api.py and
    test_protocol_md.py still pass untouched.
  • Added test_messages_sent_while_peer_has_no_poll_in_flight_are_queued: three
    messages land for a peer that has never called /receive, and one later call
    drains them in order. The protocol now promises agents this, and nothing
    asserted it before.
  • ruff check src/ tests/ and mypy src/ clean.
  • python smoke_test.py prints ALL CHECKS PASSED.

Note, unrelated to this branch: tests/test_dependency_bounds.py fails in my
local venv because its installed metadata still says mcp[cli]>=1.2 while
pyproject.toml declares >=1.9,<2. A reinstall fixes it; nothing here touches
dependencies.

Checklist

  • User-visible changes are recorded under ## [Unreleased] in CHANGELOG.md.
  • If PROTOCOL_TEXT changed in hub.py, PROTOCOL_VERSION was bumped too.

obeone added 5 commits August 9, 2026 13:18
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.
@obeone
obeone merged commit 3ffcc7a into main Aug 9, 2026
4 checks passed
@obeone
obeone deleted the docs/protocol-cheap-hosts branch August 9, 2026 11:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant