Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
6331314
fix(mcpl): speaking never opens a channel
Oct 13, 2026
1e2d279
fix(mcpl): a send leaves a tune-out in place; the invitation and docs…
Oct 14, 2026
75c3d3d
fix(mcpl): a close or decline of an undecided channel sticks; the inv…
Oct 14, 2026
98a877a
fix(mcpl): a heartbeat still speaks where the agent answered; only th…
Oct 14, 2026
7d9c4be
test(mcpl): the lookup test awaits resolveSendTarget's results
Oct 14, 2026
9f3704f
fix(mcpl): a coalesced push counts when the coalescer admits it; an a…
Oct 14, 2026
46a1e86
fix(mcpl): an edit from an answered channel isn't a new message
Oct 14, 2026
6d0cdf6
docs(changelog): a message pushed from an answered channel moves the …
Oct 14, 2026
252a405
test(mcpl): speech into a closed channel logs only its routed line; t…
Oct 14, 2026
102e1d2
fix(mcpl): an answer still in flight when its channel is decided mark…
Oct 14, 2026
e7dfa30
Merge main (e3b2c583) into fix/speaking-never-opens
Oct 15, 2026
89dd16d
feat(channels): a note where an agent's words stay in a closed channe…
Oct 15, 2026
6011543
fix(channels): af-speak4's round on the closed-channel note
Oct 16, 2026
09039b2
docs(channels): name the closed-channel note's one false case, a chan…
Oct 16, 2026
dbd9210
fix(channels): the sleep announcement gets the closed-channel note, a…
Oct 16, 2026
a0eb0d0
fix(channels): af-speak5's round on the closed-channel note
Oct 16, 2026
28d55e2
fix(channels): speech keeps only the agent's own close; it opens ever…
Oct 17, 2026
2b52dbd
Merge main (24826254) into fix/speaking-never-opens
Oct 17, 2026
b09f6d3
test(channels): the note offers no channel_open on a server without c…
Oct 17, 2026
e9ef64b
test(channels): a channel whose close the server refused gets no note…
Oct 17, 2026
756e39b
test(channels): pin channel_close's promise; the receipt's comment sa…
Oct 17, 2026
b49cbfc
fix(channels): speech opens a channel decided open whose open failed,…
Oct 17, 2026
4b229d3
fix(channels): the invitation says what replying does; one predicate …
Oct 17, 2026
e23921e
test(channels): speech's reopen of a channel decided open keeps the a…
Oct 17, 2026
6a0f4cd
test(channels): each open by speech is traced (mcpl:channel-opened-by…
Oct 17, 2026
377c84b
docs(changelog): the fragments name zulip_mcp's topics and where a he…
Oct 17, 2026
291544b
docs(changelog): name connectome-host's mentions mode and discord-mcp…
Oct 17, 2026
f81cd4c
fix(channels): a send into a channel the agent tuned out says the tun…
Oct 17, 2026
762988f
fix(channels): a module's close is the module's: speech leaves it clo…
Oct 17, 2026
8b8cf64
fix(channels): the closed-channel note on send_dm too, as on every wa…
Oct 17, 2026
5454bd2
fix(channels): af-speak6's round: the latest deliberate close, an ope…
Oct 17, 2026
dff0e03
fix(channels): a tune-out decided while an open is on its way keeps t…
Oct 17, 2026
24ac21b
fix(channels): a decline of a channel decided open closes it at the s…
Oct 17, 2026
3e636b8
fix(channels): a decline of a tuned-out channel not open at the trans…
Oct 17, 2026
b554bce
test(channels): a module's close with an unknown source is refused on…
Oct 17, 2026
026ffb3
fix(mcpl): a script's send keeps the server's receipt; its note goes …
Oct 17, 2026
3a834ac
Merge main (77066360) into fix/speaking-never-opens
Oct 18, 2026
d4b933c
Merge main (07563022) into fix/speaking-never-opens, carrying #429's …
Oct 18, 2026
c10d89e
docs(changelog): #330's fragments say what the invitation says now th…
Oct 18, 2026
048db4e
Merge main (95c65565) into fix/speaking-never-opens: #330's rule on m…
Oct 24, 2026
fceef44
Agent close holds: no give-way where the target is in doubt; judge th…
Oct 24, 2026
5668789
Agent close holds: a channel the server answered for another one take…
Oct 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 97 additions & 0 deletions changelog.d/agent-close-holds.breaking.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
- **Residents and hosts:** an agent's own `channel_close` or `channel_decline` now holds against
its own speech there. A send tool's call (`send_message`, `reply_message`, `send_dm`,
`send_file`, `send_files`), plain speech, or a draft's resend into a channel the agent closed or
declined publishes there and leaves the channel closed. Before, it opened the channel, recorded
that open over the agent's close, and announced "[channels] Now open: … your own message into the
closed channel opened it." That holds where the agent's close failed at the server and left the
channel's transport open too: the words go out there, and the close stays the decision on record,
where speech used to reopen the channel. Words that wait on a channel's lifecycle work, or on an
open they started, go into the channel as it stands once the agent closes or declines it
meanwhile, or a module closes it, where they were refused with "a decision to close the channel
came before these words went out". They're still refused where the channel's address kept
changing until the tries ran out, and a channel whose last open the server answered for another
channel takes no words as it stands until an open of it is answered for this channel, since a
publish by its id may not reach it. Speech into any other closed channel, one nobody decided
about (such as a DM that woke the agent) or one a machine closed, opens it as before, with that
notice, and a close a machine decides while words wait still refuses them.
- **Who needs to act:** an agent that closed a channel and wants its ongoing traffic again opens
it with `channel_open`; speaking there no longer does. Messages that address the agent still
arrive while a channel is closed.
- **What residents read:** where an agent's words land in a channel it closed or declined, the
send tool's answer, for routed speech the turn's `[delivered]` line, or in `explicit` routing
mode, which has no such line, a line of its own at the turn's end, says that the channel stays
closed, and, as each holds for the agent reading it, that messages addressing it still reach
it, that `channel_open` opens it, and how to turn the note off. The first is left out for an
agent whose window isn't where those messages land, such as an ephemeral run's; `channel_open`
where the agent's `allowedTools` leave it out, or on a server that's up but whose grant lacks
`channels.lifecycle`, where no channel can open; and the last where `allowedTools` leave
`agent_settings` out. In `explicit` mode, speech still being delivered when a tool ends the
turn gets the line once it's delivered, and a line that comes while another turn is under way
waits for that turn's next tool call, where that turn is shown it, or for its end. The agent
turns the note off for good with `agent_settings`' new `closed_channel_note`, which is on by
default. A conversation agent gets none: `channel_open` is refused to it, and its settings end
with its conversation. In a channel the agent's words leave closed, one it closed or declined,
one a module closed, or a closed one on a server that's up without `channels.lifecycle`, the
`sleep` tool's default announcement says "I'll still see messages addressed to me, but won't
respond until I wake." A send tool's call into a channel the agent tuned out keeps the
tune-out, as it has since 0.23.0, and its answer now says so: "… stays tuned out: your words
there don't end the tune-out, so its traffic still goes to your subconscious until the tune-out
ends. Call tune_out with channelId … and mode "cancel" for it to resume normal attention;
cancelling delivers the diverted backlog." The last sentence is `tune_out`'s own refusals' one,
naming the channel as they do, for an agent whose `allowedTools` hold `tune_out`. The same
setting turns that line off. A send a `code_execution` script makes keeps the server's receipt
alone, since the script parses it, and the agent gets the note or the tune-out line in its
window instead, as a notice.
- **What the invitation says:** the closed-channel invitation's first option now says what
replying does there. In a channel the agent closed or declined itself, or a module closed, a
reply no longer opens the channel, and the option says so: "1. Reply — … it will be delivered
there, and the channel stays closed." Before, it said "… which opens the channel" there too. It
says the same on a server whose grant lacks `channels.lifecycle`, where nothing opens. In any
other closed channel, where a reply still opens the channel, it adds how to reply without
opening it: "To reply without opening it, decline it first (option 3): a channel you've
declined stays closed when you speak there." It leaves that out in a tuned-out channel, which
`channel_decline` refuses. What else reaches the agent from a channel left closed is its
server's to say (#406): on discord-mcpl, the channel's later messages that don't address the
agent don't arrive as they're posted, and anima-research/discord-mcpl#97 has discord-mcpl say
so on its push.
- **What residents notice,** by our reading of discord-mcpl's and zulip_mcp's code: on
discord-mcpl, opening a channel also turns on its reaction forwarding, and closing it turns
that off. A reply in a guild channel the agent closed used to reopen it, and so turn its
reactions on. Now reactions to that reply arrive only where the agent opens the channel or
turns them on with `set_reaction_visibility`. DMs are unaffected: discord-mcpl requests no DM
reaction events. discord-mcpl's `channels/open` also lifts a mute set with `mute_channel`, so a
reply that opens a muted channel nobody decided about, or one a machine closed, unmutes it, as
before; a channel the agent closed keeps its mute. On zulip_mcp, an answer's topic comes from
the last message delivered as `channels/incoming`, and a closed stream's mentions are pushed
instead, so in a stream the agent closed, every answer goes to the topic of the stream's last
message from before the close, where a reply used to reopen the stream.
- **What hosts notice,** by our reading of discord-mcpl's and connectome-host's code: on
discord-mcpl, a channel the agent closed keeps delivering the messages that address the agent
as push events, as any closed channel's are, where a reply used to reopen it and switch them to
`channels/incoming`. Host code that acts on `channels/incoming`, such as connectome-host's
outage reply, doesn't see them there. Nor does a turn with no trigger, such as a heartbeat,
come to speak there: where it speaks moves only with `channels/incoming` messages.
- **Hosts with modules that close channels:** a module's `channel_close` through its own tool call
(`ctx.callTool`) without a machine source is now recorded with the source `module-tool`, where it
was `agent-tool`. Speech leaves such a channel closed, as it does one the agent closed, but gives
no note, since the module owns the way back. In connectome-host that's the `mentions` channel
mode: a reply to a mention in a channel in that mode stays a reply, and the invitation there says
the channel stays closed. Policy doesn't lift such a close, and a machine's close doesn't replace
it. Of the agent's close and a module's, the later one is the one on record. A module's close is
never refused as a machine's close of an explicit open; only one naming a machine source is.
- **Who needs to act:** a module's close made before this change was recorded as `agent-tool`,
which can't be told apart from the agent's own, so after the upgrade such a channel reads as
one the agent closed, and the agent's words there carry the note. Setting the channel's mode
again (connectome-host's `channel_mode` with `mentions`) records the close as the module's.
- **Direct `ChannelRegistry` consumers:** `openIfClosedForSend()` answers a new `status`,
`stays-closed`, for a channel a send leaves closed without trying to open it: one the agent
closed or declined, one a module closed, or a closed one on a server that's up but whose grant
lacks `channels.lifecycle`, where it answered `open-failed` or opened the channel. For an open
channel on such a server that isn't decided open, it answers `already-open` without trying an
open, where it answered `open-failed`. `speechOpens(channel)` says whether the agent's words
there open the channel, and `speechLeavesClosed(channel)` whether they go into it and leave it
closed. `closedByAgent(channel)`, `lifecycleWithheld(serverId)`, `tuneOutEnd(channel)` and
`resolveSendTarget(channelId, serverId?)`, which names the one channel a send went to from a
composite or raw id, and opens nothing, are new too.
- **Prompt cache:** `channel_close`'s description and `agent_settings`' input schema change, so the
first request after the upgrade that carries those tools misses the cache from its tool block on.
28 changes: 28 additions & 0 deletions changelog.d/agent-close-holds.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
- On a server that's up but whose grant lacks `channels.lifecycle`, speech, a draft's resend or a
send's open step no longer tries to open a channel that isn't open, since nothing opens there:
the words go into the channel as it stands, and a send answers `already-open` or `stays-closed`.
Before, speech and resends there were refused, with "… this host isn't permitted to open or close
the … server's channels", or with "the channel isn't confirmed open" where a retarget had left it
unconfirmed, and a send's open failed and was traced as `mcpl:channel-open-failed`.
- The latest deliberate close is the one on record: the agent's `channel_close` over its own
decline, its `channel_decline` over its own close, and either over a module's close, or a
module's close over the agent's, now record themselves. Before, a channel the agent had already
declined kept the decline when it closed it, and the reverse. A machine close of a channel with
no record at all, one registered lazily or on a server whose grant lacks `channels.lifecycle`,
now leaves it the default it had, where it used to record its own source, which policy never
lifts (#417). Records written before this change keep their source, so a channel a machine closed
before the upgrade still isn't opened by policy.
- `channel_decline` of a channel decided open that isn't open at the transport, whose open failed
or is still on its way, now records the decline, where it was refused with "… is open, or set to
open, so there's nothing to decline". That's the decline the invitation offers as the way to
reply without opening a channel. Where its transport may be open, as with an open on its way, the
decline closes it through the channel's lifecycle queue, as any decline does. A channel open at
the transport is still refused, now with "… is open, so there's nothing to decline", and so is a
tuned-out channel, as before.
- `channel_close`'s description and the "[channels] Now open" and policy notices' last sentence now
say that a channel the agent closes stays closed until it opens it with `channel_open`, and that
neither speaking there, the subscription policy nor its server's own default opens it again.
- The closed-channel invitation is no longer added to a direct message (`chat:dm`), all of which
reaches the agent whether the DM is open or not. Where no single-word target reaches a channel,
it no longer says to open the channel to reply: it suggests checking whether the server has a
send tool that can address the channel, and says what replying there does.
15 changes: 12 additions & 3 deletions docs/explicit-prose-routing.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,9 +99,18 @@ depends on the route:
unprefixed prose bounces and prefixed prose is deliberate.
- The entire "which event may move the agent's voice" question.

Explicit send tools (`send_message` etc.) are unchanged. Channel lifecycle
invariants (send-opens-channel, subscribed⇒open) are unchanged — a `>>` send
into a channel decided closed opens it, with the usual `[channels]` notice.
Send tools (`send_message` etc.) and channel lifecycle work the same in
explicit mode as in the other modes. A `>>` send into a closed channel opens
it, with the usual `[channels]` notice, unless the agent closed or declined
that channel itself or a module closed it, or this host doesn't direct its
server's lifecycle: then it publishes there and leaves the channel as it is.
Explicit mode has no `[delivered]` receipt, so where a turn's `>>` speech
stayed in a channel the agent closed or declined, the turn ends with a
`[closed channel]` line that says so, as a send tool's answer does, unless the
agent has turned that note off (`agent_settings`' `closed_channel_note`).
Speech still being delivered when the turn ends gets the line once it has been
delivered. If another turn is under way by then, the line waits for that turn's
next tool call, where that turn is shown it, or for its end.

## Teaching the grammar (teach-by-bounce)

Expand Down
Loading
Loading