Skip to content

Cut what a connected agent pays in tokens per session - #72

Merged
obeone merged 7 commits into
mainfrom
feat/token-diet
Aug 19, 2026
Merged

Cut what a connected agent pays in tokens per session#72
obeone merged 7 commits into
mainfrom
feat/token-diet

Conversation

@obeone

@obeone obeone commented Aug 19, 2026

Copy link
Copy Markdown
Owner

A batch of client-token reductions, all measured against a live in-process hub before touching anything.

  • A peer revival used to broadcast its "X reconnected" notice into every peer's queue, waking each passive host for a full turn. The notice now goes to the operator console and the log only; the replayed messages themselves still route normally.
  • 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, with join(force_protocol=true) to force a re-read (useful after context compaction).
  • The 16 tool docstrings carried "Returns:" blocks describing JSON the model sees verbatim anyway, and three of them (ask_operator, floor, watch_command) restated policy the protocol already owns. Both gone; behavioural error codes stay. ~575 tokens off every session, on each connector.
  • join() now hands back the ready-to-run watcher command in a watch field (stdio bridge), removing the mandatory follow-up round-trip. A shared helper keeps join() and watch_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, plus kind/origin/meta only when they carry signal) instead of the full receive envelope. Dead ReceivedMessage model 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.

obeone added 7 commits August 20, 2026 00:08
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.
@obeone
obeone merged commit d2e076a into main Aug 19, 2026
7 checks passed
@obeone
obeone deleted the feat/token-diet branch August 20, 2026 00:11
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