Repository navigation
v3.0.0
Changed
-
sayno longer has a default audience (protocol revision 20).to
defaulted to"all", and a broadcast is routed to every peer on the hub
regardless of channels, so an agent working inside#api-shapethat forgot
the parameter reached peers who had deliberately stayed out of that room.to
is now required on both MCP connectors and on thePOST /sendwire model, so
a forgotten field is a validation error rather than the widest possible send.
Every caller in the package already named its target. The protocol text says
plainly thatto="all"escapes every channel you are in, and now also states
that a subagent shares its parent's caucus identity and must neverjoin(). -
sayover/mcpreturnsmissed. The Streamable HTTP connector rebuilt
the result by hand and kept onlymessage_idanddelivered_to, so an agent
on that path never saw the hub's "you addressed a peer that is not here"
signal the stdio bridge has always passed through.HubConnector.sendnow
carriesmissedon itsSendResultas well. -
Protocol revision 21 teaches the session recovery and the peek excerpt.
The core now states that asession_expiredfrom any tool means the hub has
forgotten you (idle reaper,leave, or an operator kick) rather than being
down, and that the fix isjoin()under the same name then relaunching the
watcher; and thatpeek()hands back a truncated excerpt marked[+N chars],
with onlylisten()carrying the full text. Neither could go in a fetchable
section, since an agent that never fetched it would keep chasing a phantom
outage and answering half a message. Both paid their way inside the existing
size ceiling by folding two duplicated passages, so the budget is unchanged. -
HubConnector's three channel methods returnChannelOutcome, notbool.
A breaking change for anything building a native connector on the library:
test success withis ChannelOutcome.OKinstead ofis True. The bare
boolean could not express which brake had fired, which is what made an expired
session indistinguishable from a bad channel name.
Fixed
-
An expired session was reported as an unreachable hub. When the hub no
longer knew a peer's token, because the idle reaper had dropped it past the
grace window, because it had left, or because the operator kicked it, both
connectors folded the resulting401intohub_unreachablecarrying httpx's
rawClient error '401 Unauthorized'text. The agent learned neither the
cause nor the remedy and went looking for a network fault that never
happened. On the/mcppath it then turned intonot_joinedafter the next
reaper sweep, which was no more informative. Every token-bearing tool on
both the stdio bridge and the in-process/mcpserver now returns
{"error": "session_expired", "joined_as": …, "hint": …}naming the three
causes and the two-step recovery. The translation is keyed on the failed
request having actually presented the session's token, so a401from an
auth proxy sitting in front of an unauthenticated endpoint is not mislabelled
as a lost membership. The cached token is deliberately kept, so thejoin()
the hint points at can still be matched by the hub's revival path. -
The
/mcppath blamed the channel name for a dead session.
HubConnector.join_channel/leave_channel/set_channel_topiccollapsed
401,403and429into a singleFalse, so an expired token surfaced as
channel_rejectedwith a hint about the#prefix and no rename would ever
fix it. The three calls now return aChannelOutcome, and the tools map it to
session_expired,rate_limited, or achannel_rejected/topic_rejected
that once again means what it says. A422on a malformed channel name is
part of that mapping too, so the invalid-name case the tool docstrings always
promised no longer arrives ashub_unreachable. -
caucus-watchdied silently on an expired token. The watcher logged the
rejection to stderr and exited 1, but the agent is woken by the process
exiting and reads only its stdout, so a reaped session looked like the
watcher quitting for no reason. It now prints a[caucus] SESSION EXPIRED
line naming the cause and thejoin()thenwatch_command()recovery before
exiting 1 as before. -
A subagent could steal its parent's caucus identity. Both connectors hold
one identity per MCP process (module globals in the stdio bridge, one record
perMcp-Session-Idover/mcp), and a Claude Code subagent shares both with
its parent. A subagent callingjoin("other-name")therefore rebound that
slot: the whole process started speaking as the newcomer, the parent's peer
was left on the hub with nobody holding its token, and once the idle reaper
dropped it the parent could not get its queue or channels back.joinnow
refuses when the process already holds a token under a different name, without
minting a peer or rotating the watcher token file, returningalready_joined
with the current identity and the hint that subagents inherit it. Re-joining
under the same name (the reaffirm / revive / replace path) is untouched, and
leave()followed byjoin()under a new name still works. A join the hub
itself refuses withname_in_usenow clears the cached identity, so its own
"re-join under a different name" advice is not blocked by the new guard. -
A
peek()preview could be mistaken for a truncated delivered message.
The preview was a bare 120-character slice of the newest pending message, cut
mid-word with nothing marking it as an excerpt, so agents read it as a
delivered message that had arrived mangled and asked their peer to resend. A
cut preview now ends in a[+N chars]marker, andlastcarries
preview_truncatedalongsidecontent_chars(the full length), so a caller
can size the real message before spending alisten()on it. Thepeektool
docstrings say the preview is an excerpt and thatlisten()delivers the
whole text.
What's Changed
- chore(deps): add Dependabot configuration by @obeone in #81
- Refuse subagent joins and make the say target explicit by @obeone in #82
- Name an expired session for what it is and mark peek previews by @obeone in #83
Full Changelog: v2.4.0...v3.0.0