Cut what a connected agent pays in tokens per session - #72
Merged
Conversation
A revived peer broadcast its reconnect notice through route(), so every connected peer received it. A passive, turn-based MCP host burns a whole turn per inbound message, which made one revive cost N turns across the room for an announcement none of the peers has to act on. Emit it via _announce_system instead, like the joined/left/topic notices: the operator console and the log still see it, the peer queues do not. The replayed messages keep going through route().
… join join() shipped the full ~4.4k-token operating protocol on every call, even when the session had just read it and the revision had not moved. The text now goes out on a session's first join and on any revision bump; otherwise join() answers with the revision number and a short note. join(force_protocol=True) forces re-delivery, for an agent recovering from a context compaction that dropped the manual.
…ol surface Tool descriptions are sent to the model on every request. The Returns: blocks in all 16 tools of both connectors restated a result schema the model reads verbatim in the result itself; each tool now names only its behavioural error codes on one line, keeping the Args: sections. ask_operator, floor and watch_command also restated policy the operating protocol already spells out (when to open a form, when to grab the stick, how to run and relaunch the watcher), and watch_command repeated its 630-character usage note in every response body. All of it now has exactly one home: the protocol.
Starting the watcher is the documented next step after join, so every agent paid a second tool round-trip on watch_command() to get a string join could have handed it. On a passive host that round-trip is a whole turn. join() now returns a watch field carrying exactly what watch_command() mints; both go through one _watch_command_for() helper so the token-file handling cannot drift. watch_command() stays, for minting a fresh command mid-session.
…nvelope listen() passed each message through with its full /receive envelope: id, ts, seq, and a kind/origin pair that usually only restated the default. The agent acts on none of it — the connector ACKs the seq on its behalf and nothing ever refers back to an id or a timestamp — so it was context the model paid for once per message, and on a passive host that bill lands once per turn. A message is now sender/recipient/content, plus kind when it is not ordinary chatter (an answer keeps its meta) and origin when it is not 'agent', because that flag is how an agent tells the operator or the hub from a peer claiming to be one. Also drops the dead ReceivedMessage model, unreferenced anywhere.
join() now delivers the protocol once per session (force_protocol to re-request it) and returns the caucus-watch command in a watch field, so the tip telling agents to call watch_command() right after join is a step they no longer need.
Three review findings, all on this branch's own new code. _watch_command_for minted a fresh token file on every call, and _write_token_file unlinks its predecessor. So join() handed back a command naming file A, then watch_command() deleted A and named B: caucus-watch reads --token-file once at startup and exits via parser.error when it cannot, so the command join gave the agent died the moment it called watch_command — the exact sequence the protocol describes. The live file is now reused while the token is unchanged; a fresh one is written only when the token really changed or the file went missing, and _cleanup_token_file clears the ownership tag with the path. The 'protocol unchanged; already delivered this session' note was a dead end for an agent whose context had been compacted, so it now names force_protocol=true. Both connectors. That note is also gated on _protocol_delivered rather than on 'we sent no text this call': an empty cache used to fall through to the reassuring branch and claim a delivery that never happened. A stale join whose body carries no protocol_text no longer parks None in the cache either, so a malformed reply cannot evict a good copy.
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.
A batch of client-token reductions, all measured against a live in-process hub before touching anything.
join()used to re-send the full protocol text (~4.4k tokens) on every call, including routine re-joins. It's now delivered once per session, withjoin(force_protocol=true)to force a re-read (useful after context compaction).join()now hands back the ready-to-run watcher command in awatchfield (stdio bridge), removing the mandatory follow-up round-trip. A shared helper keepsjoin()andwatch_command()minting the same command, and reuses the live token file so calling both can't invalidate one.listen()returns a lean message shape (sender/recipient/content, pluskind/origin/metaonly when they carry signal) instead of the full receive envelope. DeadReceivedMessagemodel deleted.689→694 tests, mypy strict clean. No protocol text change here — the wording that documents the new join surface rides the protocol-diet PR (#71), which merges after this one.