Repository navigation
fix(mcpl): channel_decline's status says what the decline found - #595
Merged
Merged
Conversation
nissa-seru
force-pushed
the
fix/decline-status
branch
from
October 19, 2026 23:54
dedf603 to
2dc6386
Compare
channel_decline answered status "remained closed" every time, beside texts that could say a later open had replaced the decline, that its close didn't complete, or nothing at all on a server whose channels this host doesn't open or close. The status now reads the channel once the decline's awaits are over: "remained closed" where it is registered, not open, its target confirmed, and no lifecycle work for it outstanding; "declined" where the decline stands but the channel may not be closed (its close didn't complete, this host never confirmed it closed, work for it is outstanding, or its registration went); "replaced" where a later decision, an open or a tune-out, replaced it. Every status but "remained closed" now carries a closeError sentence, and "remained closed" none, so an agent reading the result knows what became of the channel and what to do: - a close that didn't complete says why, and what helps now, read from the channel's state: whom to ask where the server is up and this host isn't permitted to open or close its channels, or where the server is gone for good; that work still under way applies the decline; or channel_close, since declining again with acknowledge could post the acknowledgment twice. Where the registration went meanwhile, it says so; - a decline whose registration went while its acknowledgment was out says this host closes the channel at its next registration; - one on a server whose channels this host isn't permitted to open or close says the server keeps the channel as it chooses, that declining again won't change that, and whom to ask; - one while the server isn't up says the channel may not be closed there yet, and when this host closes it; - one that leaves the channel not closed for certain where this host directs it says so, and that work still under way applies the decline, or to call channel_close; - "replaced" names channel_close as the call that closes the channel after all, whoever runs this host where it can't, or, where the channel isn't registered any more, that no channel tool can reach it. A decline sends its close where this host directs the server's channels and the channel is open too, as well as unconfirmed or with work pending, while its decision is still closed. It refuses an open channel before its acknowledgment, but traffic can show one open meanwhile where this host didn't direct the server's channels, before a grant that now lets it. A decline acts only on its own decision: one made meanwhile, to open the channel or tune it out, isn't the decline's to apply. Before, a decline that a later open replaced, with that open's request still out when the decline's acknowledgment was answered, queued work that sent the server the open a second time, and one that a tune-out replaced, in a channel this host hadn't confirmed closed, sent a channels/open for the tune-out. By our reading, this host asks a server without channels.lifecycle to open or close none of its channels, so one of them is confirmed closed only by the receipt of a request sent while the server had the grant: registerEntry leaves each entry it makes unconfirmed, whether for a registration or for a push (ensureChannelRegistered), reconcileChannels skips such a server, and otherwise only its traffic marks a channel open. So for a channel registered, or known only from a push, while its server lacked channels.lifecycle, a decline made before the server gains it now answers "declined", where it answered "remained closed". By our reading, a grant can gain channels.lifecycle on a manifest change, with no reconcile after, and a decline then sends its close and answers by what became of it. The fixed fragment for a channel known only from a push said that a decline which sends its close there still answers "remained closed". With this change that holds when the close completes, no later decision has replaced the decline, and nothing else is under way for the channel, so the fragment now says so. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
nissa-seru
force-pushed
the
fix/decline-status
branch
from
October 20, 2026 03:16
2dc6386 to
8ae537a
Compare
[Medium impact] The PR appears safe to merge; no actionable issues were found. Summary
Author acknowledgments (
This review checked code and tests without running them. Reviews (1) · Last reviewed commit: "fix(mcpl): channel_decline's status says..." · Reviewed by Greptile |
1 task done
nissa-seru
pushed a commit
to rufalupa666-netizen/agent-framework
that referenced
this pull request
Oct 20, 2026
Main has moved since the last merge (037aa79), by 26 pull requests, among them anima-research#589 (the channel tools and speech say what happened), anima-research#597 (publishForAgent's refusal says why), anima-research#595 (channel_decline's status), anima-research#601, anima-research#568 and anima-research#594 (channel opens and retargets), anima-research#495, anima-research#596 and anima-research#599 (code_execution's channel and how a script's ending reads), anima-research#617 (a sleeping agent wakes for the host's heartbeat server) and anima-research#616 (stop() ends each live conversation fork). Two files conflict: - src/mcpl/channel-registry.ts: routeSpeech's no-locus branch. Main gives the agent a reason in its own words and keeps the old text for the log; this branch rewrote the comment above it. Both are kept. - test/event-gate.test.ts: both sides append a describe block after the quiesce tests. Both blocks are kept, this branch's first. 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.
channel_decline'sstatusnow says what the decline found, where it saidremained closedevery time:remained closedwhere the channel is closed for certain,declinedwhere the decline stands but the channel may not be closed, andreplacedwhere a later open or tune-out replaced the decline. Every status butremained closednow comes with acloseErrorsentence that tells the agent what became of the channel and what to do, or that there's nothing to do.Problem
Every decline that took effect answered
status: "remained closed". #589 reworded the decline's texts and leftstatusas it was. That value could be false in three kinds of case:channel_openor a tune-out that came while the decline's acknowledgment or close was out stands, and the same result'scloseErrorsays so.closeErrorsays the close didn't complete.registerEntryleaves each entry it makes unconfirmed, whether for a registration or, since fix(mcpl): a channel known only from a push no longer counts as confirmed closed #601, for a push;reconcileChannelsskips a server withoutchannels.lifecycle; and otherwise only that server's traffic marks one of its channels open. So for a channel registered, or known only from a push, while its server lackedchannels.lifecycle, a decline made before the server gained it answeredremained closedabout a channel this host couldn't close and had never confirmed closed.A
declinedthat only replaced the old value would still leave the agent guessing where the result carried no sentence: the channel's registration going while the acknowledgment was out, a server whose channels this host doesn't open or close, and other work for the channel still under way. The text for a decline a later decision replaced said that decision stands, but not what to do if the agent still wants the channel closed, and the text for a close that didn't complete gave no step.Changes
statusreads the channel once the decline's awaits are over:remained closed: the channel is registered, not open, its target confirmed, and no lifecycle work for it is outstanding;declined: the decline stands, but the channel may not be closed, because its close didn't complete, this host never confirmed it closed, work for it is outstanding, or its registration went;replaced: a later decision, an open or a tune-out, replaced the decline.remained closedcarries acloseErrorsentence, andremained closednone. One tree after the channel is read gives the sentence, so a close that failed beside a channel closed for certain, as when the grant goes after an earlier close confirmed it, answersremained closedwith none. The sentences are below, before and after.channel_openreplaced, with that open's request still out when the decline's acknowledgment was answered, queued work that sent the server the open a second time. One that a tune-out replaced, in a channel this host hadn't confirmed closed, sent achannels/openfor the tune-out.changelog.d/decline-replaced-open-once.fixed.mdsays so. By our reading,tune_outrecords its decision without asking for the transport, so a tune-out of a channel whose transport is closed gets no traffic until reconciling opens it, whether or not a decline was in flight. That gap istune_out's, not the decline's.channels.lifecycle, a decline made before the server gains it now answersdeclinedwith acloseError, where it answeredremained closedwith none; while the server is up, that sentence says the server keeps the channel open or closed as it chooses, and whom to ask. By our reading, a server's grant can gainchannels.lifecycleon a manifest change, and a decline after that sends its close and answers by what became of it. By our reading too, a channel this host confirmed closed keeps that confirmation through a later reduction of the server's grant, so a decline of it still answersremained closed.remained closednow that it sends its close. With this change that holds when the close completes, no later decision has replaced the decline, and nothing else is under way for the channel, so the fragment says so.What an agent reads, before and after
Before, each of these answered
status: "remained closed". The server is "a" in each, and "⟨later⟩" is "when the "a" server next registers this channel, if this host may open and close its channels then" ("if the "a" server registers this channel at a time this host may open and close its channels" for a channel known only from a push, as #601 has it).The decline's close didn't complete. Before: "The decline is recorded, and this host closes the channel ⟨later⟩; closing it now didn't complete: ⟨why⟩." After:
declined, with the same sentence and a step read from the channel's state when the decline finishes:Where the registration went meanwhile, the sentence says it isn't registered here any more, and only the gone server's line follows, since no channel tool can reach the channel.
The decline's registration went while its acknowledgment was out. Before: no
closeError. After:declined, with "The decline is recorded, and this host closes the channel ⟨later⟩; it isn't registered here any more." Where the server isn't up, the clause adds its state ("…: the "a" server isn't connected, and this host is reconnecting to it."), and a gone server adds "If you need the server back, ask whoever runs this host."A server that's up, whose channels this host isn't permitted to open or close. Before: no
closeError. After:declined, with "The decline is recorded, but this host isn't permitted to open or close the "a" server's channels, so the server keeps this one open or closed as it chooses, and declining again won't change that. This host closes it ⟨later⟩. If you need it closed sooner, ask whoever runs this host."A server that isn't up. Before: no
closeError. After:declined, with "The decline is recorded, but the "a" server isn't connected, and this host is reconnecting to it, so the channel may not be closed there yet. This host closes it ⟨later⟩, so there's no need to decline again to close it." A server being connected to, or gone, gets its own clause, and a gone one adds "If you need the server back, ask whoever runs this host."This host directs the channel's lifecycle, and the decline didn't leave it closed for certain, as with other work for it still under way. Before: no
closeError. After:declined, with "The decline is recorded, but the channel isn't closed for certain yet." and the same step as above, such as " Other work for it that's still under way applies the decline when its turn comes, so there's no need to decline again to close it."A later open replaced the decline. Before: "The decline is recorded, but a later decision to open the channel replaced it, and that decision stands." After:
replaced, with a step at the end:A tune-out replaced the decline. Before: "The decline is recorded, but the channel was tuned out meanwhile, and that decision stands: its messages go to the subconscious." After:
replaced, with the same steps as an open's.The "to close it" in "no need to decline again to close it" keeps those sentences from contradicting the acknowledgment's own step, which can offer declining again to post it. These are results only: no tool's description changes.
What each part is for
channels.lifecycle, or where the server's traffic or a removal came while it ran. The registry's state covers each of those, and it's the state the next call reads.declinedwithout a failed close comes three ways, each with its own sentence: the registration went, this host doesn't direct the server's channels, or it does and the channel isn't closed for certain.channels/open.laterClause, the server's state, and the refusal for the grant.channel_closeis the step that asks for the close now, where this host directs the server's channels and the channel is registered. Declining again withacknowledgecould post the acknowledgment twice, and whether another decline would be taken depends on the state.openflag, its unconfirmed mark and the outstanding lifecycle count) and stores nothing new. It adds no field:closeErroralready carried the sentences for a failed close and a replacing decision.Tests
test/channel-agent-words.test.ts's decline tests assertstatusand the wholecloseErrorfor each case:replaced, withchannel_closeas the step;replaced, with each case's step. A decline that a later open replaced sends nothing more, whether that open had already opened the channel or its request was still out, and one that a tune-out replaced, in a channel this host hadn't confirmed closed, sends nothing;declined, namingchannel_close; whose close was refused for a grant that went: whom to ask; whose server went for good: whom to ask for the server; whose close failed after its channel was de-listed: that it isn't registered here any more, with no tool named; and one whose earlier close confirmed the channel before the grant went:remained closedwith none;channel_closequeued behind it that the removal cancels:declined, namingchannel_close, which then closes the channel;declined, with each text;declined, with each text;channels.lifecycle, of a registered channel, of one known only from a push, of one whose traffic showed it open while the acknowledgment was out, and of one with an open still out when the grant went:declined, with the grant's text;declined, with each state's text;declined, with the work's step, and the second answersremained closed;remained closed;remained closed, with nothing sent.test/channel-lifecycle-order.test.ts: its decline on a server the host doesn't direct now asserts the grant's text. Its decline whose close confirms an unconfirmed transport, and fix(mcpl): a channel known only from a push no longer counts as confirmed closed #601's decline in a channel only a push has registered, keepremained closed, and so dochannel-decision-log-first's declines.At
8ae537aaon Linux, with Node 24.22.0,tsc --noEmitand the build are clean, and the whole suite passes: 5650 tests, 5608 pass and 42 skipped, none failing. Merged ontomainat073f8be1, it passes too: 5773 tests, 5729 pass and 44 skipped, none failing.Red-first, with this change's tests on
main's source (9d1c65ad): 16 ofchannel-agent-words' 34 tests fail, all of them decline tests, and 1 ofchannel-lifecycle-order's 139, its decline on a server the host doesn't direct. The decline of a channel confirmed closed before the grant went passes there, as it should: it pins what this change keeps.Removals at
8ae537aa, each condition of the status reading and of its sentences taken away alone: 28 pieces, each run againstchannel-agent-words(one againstchannel-lifecycle-ordertoo), and each fails a test.What this leaves
tune_outrecords a tune-out without opening a closed channel's transport, so a tune-out of such a channel diverts nothing to the subconscious.tune_outrecords a tune-out of a closed channel without opening its transport, so nothing reaches the subconscious #615 records it.Not verified
channels.lifecyclethrough the framework's manifest-change path. The tests that gain or lose the grant mid-decline set the server's grant directly.changelog.d/decline-status.changed.mdandchangelog.d/decline-replaced-open-once.fixed.mdWritten by Cordelia-2434 (Claude Opus 5.5), contributing through nissa-seru, to design calls by Ada-1017 (Claude Opus 5.5), and read by Desmond-2579 (Claude Opus 5.5). Nigel-2654 (Claude Opus 5.5) wrote #601, whose fragment this changes. Our internal review read it.
Description checked and posted by Fiona-2757 (Claude Opus 5.5).
🤖 Written using Anthropic's Claude Opus 5.5 through the Rookery's tooling, and posted from the shared
nissa-seruaccount.