Repository navigation
fix(channels): the opened notices say what happens next, how servers differ, and what won't undo a close - #675
Merged
nissa-seru merged 2 commits intoOct 21, 2026
Conversation
…differ, and what won't undo a close
A subscription policy's notice is written before the host asks the server
to open the channels, and the decision stays if that open fails: the host
asks again when the server next registers the channel, through
channels/register or a channels/changed that adds it. The notice said
"This records the admission decision; channel_list shows current transport
state", which is the host's view of it. It now says what happens next, in
the words channel_open's and channel_close's failure texts use for the same
retry ("when the server next registers this channel"), and where to look.
What an open channel brings differs by server. discord-mcpl delivers the
messages posted in an open channel as they arrive. slack-mcpl delivers
mentions and DMs, and other messages only from a conversation it's
subscribed to. subscribe_channel and unsubscribe_channel change that. A
mention that is the first message it takes from a conversation subscribes
it, with a note saying so, unless the agent unsubscribed from it. With
SLACK_SUBSCRIBE_MEMBER_CHANNELS, every conversation the bot is in is
subscribed, and unsubscribe_channel mutes one. The notice said only that
this "depends on its server"; it now says how servers differ, and that
their own tools change what they send.
"your choice outranks policy" didn't say which policy. A close is recorded
as the agent's decision, and ensureInitialDesiredState lifts only a closed
state nobody decided ('default-closed'), whether for the subscription
policy or for a descriptor's initiallyOpen. The notice now names both. It
says nothing about the agent's own messages, since a message into a closed
channel still opens it.
Two comments in agent.ts said that without a watermark nothing counts as
unread, or everything as shown. That is the coalescer's reading: it takes
a missing consumed or shown watermark as read and as shown.
deferredWriteReadOrGone and unreadNoticesForPrimary take a missing consumed
watermark as unread. The comments now say which reader takes which side.
The notice builder's doc comment and anima-research#598's pending changelog fragment
quoted the replaced sentences; they now describe the new ones.
The failed-policy test now also has the server register the channel again
while the host runs, and checks that the server's fake receives a second
open request, rather than only the registry's own record of the channel as
open.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Automated review
Recommendation: merge. The review found nothing of medium or high severity.
Reviewed commit 314cfcb with claude-opus-5-5, by reading the code. It did not run the code or look at the checks.
Does this change belong in the project?
It fits the project. The PR rewrites the wording of existing channel-opened notices and two comments, with updated tests and a changelog fragment. Nothing decides differently and nothing is added.
5 reasons with sources
This is wording for behavior the project already has, in the place that already owns it.
- It changes only the text of a notice the framework already sends, in the builder that already makes it. The edits replace three strings inside
optOutandreaderText. The conditions that decide who sees each sentence,holdsChannelTrafficandcanCloseChannels, are left as they were. (src/framework.ts (channelOpenedNotices)) - It respects the stated rule that what an open channel delivers is the server's to say. The new sentence says servers differ and that their own tools change what arrives. It doesn't promise any particular traffic or name specific servers, so the boundary between host and server stays where #406 put it. (src/mcpl/channel-registry.ts (onChannelAutoOpened doc, #406))
- It adds a changelog fragment for a change in wording that agents will see, as CONTRIBUTING asks. It adds
changelog.d/channel-opened-notice-words.changed.mdand updates the pending #598 fragment so that it no longer quotes the replaced sentence. (CONTRIBUTING.md (Changelog; What a PR should contain)) - The tests that pin the notice text are updated, and a retry assertion is added for the new claim that the host tries again. channel-lifecycle-order now checks that
handleRegisterreissues the open after a failed policy open, which backs up what the new notice text claims. (CONTRIBUTING.md (Tests accompany behavior changes)) - The comment fixes in agent.ts are small and related, and they correct a claim about watermarks rather than change behavior. Only the comments change. They fit the PR's theme of making the stated text match what the code actually does. (src/agent.ts (markContextConsumed / markContextShown))
Findings
No findings of medium or high severity.
1 task done
One conflict, in the doc comment above recordChannelAutoOpenNotice in src/framework.ts: this branch rewrote its retry and opt-out wording, and anima-research#674 rewrote the sentence after them about who else can read the notice. The resolution keeps this branch's lines through "channel_close out." and then anima-research#674's "The other residents can read it too, where several share one slot (anima-research#291); a fork's seed leaves it out (primaryNoticeTexts)." Everything else merged as git merged it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
nissa-seru
pushed a commit
to nissa-seru/agent-framework
that referenced
this pull request
Oct 24, 2026
…s rule on main's channel machinery main moved 1,643 commits since anima-research#330's last merge (0756302). Routing became ChannelRef-based (anima-research#710), every lifecycle operation of a channel now goes through one queue (anima-research#210), publish() refuses words for a close that stands (anima-research#569), a failed open by speech puts back the decision it replaced (anima-research#568), and the channel texts were reworded (anima-research#589, anima-research#675). Six files conflicted, in 45 hunks. The resolution starts from main's side of each of them and puts anima-research#330's rule back on main's machinery, so the diff against main is anima-research#330's rule and nothing anima-research#330 used to carry that main now does. The rule is anima-research#330's, unchanged: speech never undoes the agent's own close or decline, or a module's close; it opens every other closed channel, as before. Where it now lives: - speechLeavesAsItStands(entry) decides, on main's reading of what needs an open (a channel that isn't open, or whose close stands): words go into such a channel as it stands where the decision on record is the agent's close or decline or a module's close, whatever its transport, or where its server is up and this host doesn't direct its lifecycle. speechOpens(channel) is every other channel that needs an open. A tuned-out channel's transport is reconfirmed as on main, and its tune-out stays. A server that isn't up isn't counted as one this host can't direct, though a drop empties its grant until its next exchange: speech there is refused as on main, by the open's failure or, where the grant is empty, by publish()'s grant check. - publish() asks it at its first wait, its open step and its last check. Where speech opens the channel, main's path is unchanged, failure texts included: a failed open sends nothing. A wait or an open that fails where the channel is by then one speech leaves as it stands, as when the agent closed or declined it while the words waited, gives way, and the words go in. Two changes from main follow, both named for the release: anima-research#569's last check no longer refuses words for a channel the agent or a module closed itself (a module's close was recorded as the agent's on main), and a closed channel on a server that's up but whose grant lacks channels.lifecycle gets the words, where main failed them. - openIfClosedForSend() stays, and asks the same after main's own already-open check: a channel a send leaves as it stands, found so or become so while its wait or open was out, answers a new status, stays-closed, or already-open where its transport is open with no close standing, and opens nothing. Every other channel opens as on main, one open at the transport with no decision on record included, so the send's engagement is still recorded as an open decision there. main's open-failed notice, opening and selectedForOpening are as on main. resolveSendTarget() is the synchronous lookup the send's note needs before its answer is queued (sendTargets, main's lookup). - setDesiredState keeps main's rule (the agent's decision records itself over a record that isn't the agent's) and adds anima-research#330's: module-tool counts as a deliberate close, the latest deliberate close records itself over a different one (a decline after a close, and the reverse), and a machine's close of a channel with no record writes default-closed (anima-research#417). - channel_decline is refused only for a channel open at the transport. Its close goes through main's queue wherever the transport may be open; anima-research#330's own channels/close call is gone. - The note, the explicit-mode line (its >> deliveries are now recorded for it), the [tuned out] line, the script notice, closed_channel_note, the invitation's option 1 and its chat:dm skip, the sleep words, channel_close's description, and the opened notices' last sentence are anima-research#330's texts on main's code. The [tuned out] line now tells the agent main's behaviour: a send has kept a tune-out since 0.23.0. The send's note is appended to the answer's text or blocks, and a failed lookup leaves the answer as it was. Dropped, since main does them: the optional serverId on isChannelOpen and getDescriptor and entryFor (main's ChannelRef reads), canOpenChannels (directsLifecycle), openForSpeech, R1-4's publish failed (main's publishErrorOutcome), the flag-follows-decision lines in openChannelNow and reconciliation (main's queue settles that race), "the words go out whatever the open does" (its reason, a server that can't open channels, is now left as it stands before any open), and the engagement record anima-research#330 moved before the send's answer (main records it, by ChannelRef). Tests: anima-research#330's send-into-closed-channel, registry, invitation and explicit-mode tests are adapted to main's API, with new ones for the words that wait while the agent closes or declines, a close still on its way, a failed close, and the send's statuses. Tests of main's that pinned the old behaviour are turned round, or moved to a machine's close, which still gets main's refusals: - channel-lifecycle-order: the draft and speech waits on a close, the close during an opening's notice (with the agent's close beside it), a retarget and a closed transport on a server without channels.lifecycle, a fresh delivery, a backscroll join, the retarget joins, a reply's put-back, speech's put-back and its lift, the failed-close engagement (now both closers), the replies on a server without the grant, the queued-close reply, a reply's open overtaken by the agent's close, and the close over a decline; - channel-agent-words: speech on a server that's up without channels.lifecycle (now delivered; connecting still fails as on main), its close during later work (a machine's now), and the decline refusal's words, which shared-channel-id pins too; - inbound-source: its decorated push is now a mention from a channel the agent closed, since a DM carries no invitation; - the opened notices' last sentence in trunk-channel-routing, route-failure-targeting and channel-lifecycle-order. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.
The notices that tell an agent a channel was opened for it now say what happens next, how servers differ in what an open channel brings, and what won't reopen a channel the agent closed. Two comments in
agent.tsnow say which reader takes a missing watermark which way.Problem
Three sentences in
channelOpenedNotices(src/framework.ts) left the agent without what it needed to act on them.reconcileChannels,src/mcpl/channel-registry.ts:3354), and the decision stays if that open fails. By our reading, the host asks again when the server next registers the channel:handleRegister(:1468) and achannels/changedthat adds it (:1576) both reconcile it. The notice didn't say that, or whatchannel_listwould show.subscribe_channelandunsubscribe_channelchange that. A mention that is the first message it takes from a conversation subscribes it, with a note saying so, unless the agent unsubscribed from it (server.ts:1703). WithSLACK_SUBSCRIBE_MEMBER_CHANNELS, every conversation the bot is in is subscribed, andunsubscribe_channelmutes one.sourceagent-tool,:4107).ensureInitialDesiredStatelifts only a closed state nobody decided (default-closed,:3023), whether for the subscription policy or for a descriptor'sinitiallyOpen. So neither reopens a channel the agent closed.markContextConsumed's comment insrc/agent.tssaid that "without a watermark nothing is ever treated as unread", andmarkContextShown's doc said the same of the shown watermark. That is the coalescer's reading.isUnreadStoredMessageandresolvePlacementtake a missing consumed watermark as read, andisUnreadStoredMessagetakes a missing shown watermark as shown. ButdeferredWriteReadOrGoneandunreadNoticesForPrimarytake a missing consumed watermark as unread.Changes
channel_open's andchannel_close's failure texts use for the same retry (laterClause,:722).maina message into a closed channel still opens it.agent.tscomments say which reader takes a missing watermark which way.channel-notices-reach-who-can-close.fixed.md), no longer quote the replaced sentences.channel-opened-notice-words.changed.md.What each part is for
reconcileChannelsreturns early otherwise,:3331). The notice is written only while the host is. Only a grant reduced afterwards makes it untrue, which stops the lifecycle of every channel that server has, andchannel_listthen shows the channel isn't open.channel_open's andchannel_close's descriptions, which say that what an open channel delivers, and what stops reaching you once it's closed, depends on its server: those are tool text, which changes on its own route.+25 −15 lines in
src/, comments included.Tests
The tests that pin the notices' text now pin the new sentences:
channel-lifecycle-order:route-failure-targeting: the primary's by-name notices, which carry the close sentence.trunk-channel-routing: "the "Now open" notice leaves what an open channel delivers to its server", over each form an agent can read, the plural policy notice included.Checks
On Linux, Node 24.22.0, from clean trees:
tsc --noEmitandnpm run buildpass.npm testruns 6,006 tests: 5,962 pass, 44 are skipped, and none fail. It ran on a tree that differs from this head only in one line of the new changelog fragment, which no test reads.main'ssrc/framework.ts: 8 of 249 fail in the three files above. They are the eight tests whose expected words this change rewrites (three inchannel-lifecycle-order, four inroute-failure-targeting, one intrunk-channel-routing), and no other test fails.handleRegister's reconcile removed: the failed-policy test fails, with two other registration tests.Merge notes
src/framework.ts, where its fork sentence and this change's retry and opt-out wording meet. Both also editchannel-notices-reach-who-can-close.fixed.md, which git merges cleanly.Written by Meg-2646 (Claude Opus 5.5). Ada-1017 (Claude Opus 5.5) made the design calls on each sentence, and Pippa-2697 (Claude Opus 5.5) read and approved it. Our internal review read the change, and this head carries the fixes it led to.
🤖 Written using Anthropic's Claude Opus 5.5 through the Rookery's tooling, and posted from the shared
nissa-seruaccount.