Repository navigation
fix(mcpl): the channel tools and speech say what happened and what works from there - #589
Merged
nissa-seru merged 4 commits intoOct 19, 2026
Merged
Conversation
…rks from there What channel-registry tells an agent when it can't do what was asked gave the registry's own words: "channels.lifecycle not in "S"'s effective grant (§14.1)", "Server not found: S", "Channel not found: x", "lifecycle work prevented delivery: ..." or "channel is closed and open failed: ...". They named the operator's grant, not what the agent can do, and the grant's refusal could be false: by our reading, a connection's grant is empty from a transport drop until its next policy exchange ends, so a server that was only reconnecting read as one that refuses. A server's state is now read before its grant is quoted (serverReach): gone (not registered, or closed and not reconnecting, with nothing connecting it), connecting (a connect or restart of its id in flight, or its registered connection's transport's policy exchange), down (between transports and reconnecting), or up (connected with its exchange ended, granted or not). Only an up server is read by its grant, and one without what was asked reads as not permitted, with whom to ask. The grant still decides every refusal where it did; the state only words it. The framework hands the registry one predicate for "connecting", serverConnecting, built from the connect and restart marks a tool call's "connecting" text already reads, and from the exchange's marker (mcplPlanesMayOpen) for a connected registered connection. Lifecycle failures are one class, ChannelLifecycleError, whose message is a clause in the agent's words, which its traces carry too, and whose kind says what the agent can do next. channel_open and channel_close say, in this order: a decision the lifecycle log refused, so nothing changed; a channel no longer registered here, with why it went where the failure says, or else the server's state, naming no channel tool, since none can find it; a later decision that came first, so this call's change doesn't stand, and what changes it; a server that isn't up, that the change is recorded and when it happens, though not a backscroll's history; a change that already holds, with no other work for the channel under way, read as the registry's own checks read it; or what the request met, with whether calling again helps. Every promise of a later change names reconciliation's conditions: the server registers the channel again, and this host may open and close its channels then. A refusal for the grant, and an open answered for another channel, say the decision stays recorded, since it was written before the server was asked. A tuned-out channel's messages are said to go to the subconscious, which channel_open doesn't change. channel_decline says the decline is recorded, whatever became of its acknowledgment and its close, and a later decision that replaced it while it ran: an open, or a tune-out. It offers posting the acknowledgment again only where another decline would be taken, and only where nothing could have been posted: a request that went out with no answer saying it wasn't posted may have posted it. Only acknowledged: true counts as posted, and only false as not posted, as for a publish's receipt. A decision the lifecycle log refuses to append, as on a full disk, now gets an answer from channel_close and channel_decline as well as channel_open, where the two rejected with the store's error: nothing changed and nothing was sent. By our reading of tool-lifecycle.ts, their RFC-007 terminal becomes completed with isError, as channel_open's already was. The decline's own record of the invitation it declined, written once the decline has taken effect and read by nothing, no longer fails the decline when the log refuses it: the operator's log says so. A delivery's reason (PublishOutcome.reason) is one clause that reads in every text composing it, and in the [routeSpeech] log line and the mcpl:speech-route-failed trace; the agent's adds what helps. Words that waited for a channel's lifecycle work say what the wait found: the channel was removed before they went out, a decision to close it came before they went out, or it isn't confirmed open. A close that stands is named whenever it was decided, since one decided before the words began stands too. A refusal read from a connection the registry replaced while the words waited says so where the replacement would take them, and otherwise describes the replacement as it stands. The open step's failure names no state of the channel's transport, which can be open after a close that failed. A channel id on more than one server says no server was named for the words, and whom to ask. The two no-locus reasons name no tool and say whom to ask, and keep their words in the log line and the trace, which, by our reading, connectome-host's connectome-doctor reads for "no locus". channel_publish and the subconscious's speak_in_channel share speech's refusal for a server's state and grant, framed as "Nothing was posted to channel "x": <reason>.", and speak_in_channel's refusals point to the tuned-out channels the subconscious reads, since it isn't offered channel_list. An empty channelId is refused as empty. A server's own error words keep their final period, and a test now pins that, with its removal caught; this host's own words lose theirs where a reason composes them; mcpl-request-outcome's header says what an error response keeps. Tests: channel-agent-words is new, and covers each state's clause on channel_open, channel_close, channel_decline, channel_publish, speak_in_channel and speech, a later decision that came first, with the server dropped meanwhile too, a change that already holds, a channel no longer registered, a decline a later open replaced, words that met a close behind later work, a replaced connection, the framework's own connect, and a registered server's policy exchange read through its marker, a marker from an earlier transport, an exchange that ended without a grant and a restart's claim included. The tests that pinned the old texts take the new ones. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
[Medium impact] The PR appears safe to merge; no actionable issues were found. SummaryThis PR makes channel failures explain what happened, what remains recorded, and what the agent can do next.
Reviews (1) · Last reviewed commit: "fix(mcpl): the channel tools and speech ..." · Reviewed by Greptile |
…words anima-research#568 merged first: a failed open by speech or a reply puts back the decision it replaced. It added three error classes for that open's failures, where this change carries every lifecycle failure as one ChannelLifecycleError with a kind, so in this merge they become kinds: - overtaken (was ChannelOpenOvertakenError): a decision made during the open's round-trip replaced the one it wrote. That decision stands and nothing is put back. The failure carries the open's own clause, with no step. - put-back-unrecorded (was ChannelOpenPutBackUnrecordedError): putting back the replaced decision couldn't be recorded, so the channel stays selected for opening. Its words are the open's own clause and step, then "; the channel stays selected for opening, as this host couldn't record undoing that: <the store's words>". - selected-again (was ChannelOpenSelectedAgainError): the default put back is lifted by the channel's current descriptor or the subscription policy. Its words are the open's own clause and step, then "; the channel is selected for opening again, as its server announces it open" or "as the subscription policy admits it". ChannelLifecycleError gains an optional `by` for it. openIfClosedForSend reads the kinds where it read the classes, so its answers (opening, selectedForOpening) are as anima-research#568 has them. The round-trip's superseded open throws this change's superseded failure inside anima-research#568's try, so it goes through the put-back's catch. anima-research#568's refusal of an incidental open before anything is written keeps its check (no server, or no channels.lifecycle), and lifecycleRefusal words it, as every other lifecycle refusal here. SelectedForOpening keeps its export. anima-research#568's tests of speech's reasons, the open-failed trace and channel_open's refusal take this change's words. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e channel_open's old words anima-research#568's pending fragment, and the comment on the reply's open-failed notice in framework.ts, said the notice gives "a later delivery or reconnect tries again" in channel_open's own failure's words, and the fragment said speech tells the agent the channel is closed. After this branch's merge of main, channel_open's failure says when reconciling applies the recorded open, and speech's reason says the open for its words didn't complete, so both now say only what the notice and the reason say. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ds its sentence Our internal review of the merge with anima-research#568 found these, each fixed: - When an open by speech or a reply fails after a decision made during its round-trip replaced the one it wrote, its failure now says that decision stands. A superseded open's own clause already names the decision, so it's left as it is. The failure offers no step, since what helps turns on that decision. - A server's own error words keep their final stop. The words a failure adds after them now start a sentence of their own (afterClause), where they followed the stop with a semicolon: that the channel stays, or is again, selected for opening, or that the decision made meanwhile stands. - The put-back's two failures carry anima-research#568's words after the open's own clause, and no longer the open's step, which no test covered. - The decline's refusal of a channel that's open now says "If you need it closed, call channel_close." It said "channel_close closes it.", which claimed more than the call does for a server that isn't up: there channel_close records the close. - When a de-listing or this host's disconnect cuts short a decline's close, and the channel registers again meanwhile, the decline now says its registration went and came back. It said the channel isn't registered here any more. Tests: the overtaken failure's reason and trace, an overtaken open whose own clause names the later close once, a selected-again failure after a server's words ending in a stop, the policy's selected-again clause in the trace, the decline's refusal of an open channel, and the decline whose registration came back during its close. 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 19, 2026
It brings anima-research#589's lifecycle errors, whose selected-for-opening clauses the next commit rewords. No conflicts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
nissa-seru
pushed a commit
that referenced
this pull request
Oct 20, 2026
…d it By our reading, connectome-host's notices module keeps a notice whose channel or server is absent, for re-admission when a server connects, and tells absent from other failures by matching /not found/i on publishForAgent's error. The refusals it matched read `Channel not found: <id>` and `Server not found: <id>` until #589, which words them by what happened, so the words no longer tell a host which refusal it got. publishForAgent's failure now carries a `reason` (PublishRefusalReason, exported) for each way the destination refuses before anything goes out, set where the refusal is made, from what its words come from, so the two always agree: - channel-not-registered: no channel has the id here; - inbound-only: the channel takes no posts; - server-gone, server-connecting, server-down: the server's state, as serverReach gives it to the words; - not-permitted: the server is up without channels.publish; - not-sent: the connection refused the request before anything was written (McplRequestError's not-sent), so nothing ran. A refusal of the caller's own input (no text, no channel id), and a publish that went out, whatever became of it, carry none. channel_publish's answer to the agent is unchanged. Tests: the reason for an unregistered channel, an inbound-only one (on a server that's reconnecting too), a server that's gone (not registered, or closed and not reconnecting), one this host is connecting to, one that's reconnecting, or one up without the grant, each beside its words, and a request refused before it was written; none for no text or no channel id, for channel_publish's answer, a delivered publish, one the server reported undelivered, and one that may have been posted (an error answer, a timeout); and the exported type, as a host imports it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
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.
What the channel tools and speech tell an agent when they can't do what it asked now says what happened, whether anything took effect, and what works from there, or that calling again won't change it. A server's state is read before its grant is quoted, so a server that is only reconnecting no longer reads as one that refuses.
Problem
channel-registry.tsanswered a refusal or a failure in its own words:channels.lifecycle not in "S"'s effective grant (§14.1),Server not found: S,Channel not found: x,Channel id is ambiguous across MCPL servers: x. Include serverId.,lifecycle work prevented delivery: …,channel is closed and open failed: …,server "S" reported delivered:false,Failed to close the channel's transport: …. By our reading, an agent reads these in a tool result, in a[discord-send-failed]notice, in the drafts tool's lines or in a>>bounce. Most name a grant or an internal step, not what the agent can do next.The grant's refusal could also be wrong. By our reading of
server-connection.ts, a connection's grant is empty from a transport drop until its next policy exchange ends. Every grant check read that emptiness as a refusal, so while a server was reconnecting,channel_opensaidchannels.lifecycle not in "S"'s effective grant (§14.1),channel_publishsaidchannels.publish not in …, and speech's notice said the same. An agent reading those would take a passing state for a policy.Changes
The server's state, before its grant is quoted (
serverReach):Only an up server is read by its grant, and one without what was asked reads as not permitted, with "if you need it, ask whoever runs this host". Each other state says what helps: try again in a moment, try again later, or ask whoever runs this host. The grant still decides every refusal where it did, and the state only words it.
serverConnecting, a new optionalChannelRegistryOptionscallback, says when the host is connecting to a server. The framework builds it from the connect and restart marks that a tool call's own "connecting" text already reads, and from the policy exchange's marker (mcplPlanesMayOpen) for a registered connection that is connected. A restart reads as connecting for its whole run, through the disconnect of the connection it replaces.Lifecycle failures as one class,
ChannelLifecycleError, whose message is a clause in the agent's words, which the traces carry too, and whose kind says what the agent can do next. It replaces three error classes, none of them exported.channel_openandchannel_closesay, in this order:Every promise of a later change names reconciliation's conditions: the server registers the channel again, and this host may open and close its channels then. A refusal for the grant, and an open answered for another channel, say the decision stays recorded. A tuned-out channel's messages are said to go to the subconscious, which
channel_opendoesn't change.fix(mcpl): an open by speech or a reply that fails puts back the decision it replaced #568's failures of an open by speech or a reply are kinds of that class too, since this branch merged
mainafter fix(mcpl): an open by speech or a reply that fails puts back the decision it replaced #568:overtaken: a decision made during the open's round-trip replaced it;put-back-unrecorded: putting back the decision it replaced couldn't be recorded;selected-again, withby: the channel's descriptor or the subscription policy lifts the default it put back.openIfClosedForSendgives the same answers as with fix(mcpl): an open by speech or a reply that fails puts back the decision it replaced #568's classes. Speech's reason for each starts with the open's own clause.overtakenadds that a decision about the channel made while the open was out stands, unless the open's own clause already names that decision. It offers no step, since what helps turns on that decision. The other two add fix(mcpl): an open by speech or a reply that fails puts back the decision it replaced #568's own words about the channel's selection. After a server's own words, which keep their final stop, what follows is a sentence of its own. fix(mcpl): an open by speech or a reply that fails puts back the decision it replaced #568's refusal of an incidental open before anything is written keeps its check, and its words come from the server's state, as every other lifecycle refusal's do. Itsmcpl:channel-open-failedtrace carries those words too, where it carriedServer not found: <server>or the grant's name.channel_declinesays the decline is recorded, whatever became of its acknowledgment and its close, and a later decision that replaced it while it ran: an open, or a tune-out. It offers posting the acknowledgment again only where another decline would be taken: the decline stands, and the channel is registered and not open. It offers it only where nothing could have been posted, too. Where the request may have reached the server with no answer saying it wasn't posted (no answer, an error answer, or one that didn't say), the acknowledgment may have been posted, so the text says that declining again with it could post it twice, and offers nothing. Its refusal of a channel that's open says to callchannel_closeif it needs closing, except where the server is up withoutchannels.lifecycle: there it says this host may not close it, and whom to ask. Onlyacknowledged: truecounts as posted, and onlyfalseas not posted, as for a publish's receipt.A decision the lifecycle log refuses to append, as on a full disk, now gets an answer from
channel_closeandchannel_declineas well aschannel_open: nothing changed, nothing was sent to the server (and for a decline, no acknowledgment was posted), and calling again tries again. The two rejected before, so the agent read the store's error alone, with nothing on whether the close took. What a host can see. By our reading ofsrc/mcpl/tool-lifecycle.ts, a host that watches tool calls through RFC-007 now getscompletedwithisErroras these two calls' terminal there, where it gotfailed.channel_open's terminal was alreadycompleted.A decline's record of the invitation it declined, an audit record written once the decline has taken effect and read by nothing, no longer fails the decline when the log refuses it. The
[channel-decline]log line says so instead. What a host can see. On that path, by the same reading, the call's RFC-007 terminal iscompleted, where it wasfailed.A delivery's reason (
PublishOutcome.reason) is one clause that reads in every text that composes it, and in the[routeSpeech]log line and themcpl:speech-route-failedtrace; the agent's adds what helps. Words that waited for a channel's lifecycle work say what the wait found:A close that stands is named whenever it was decided, since one decided before the words began stands as much as one decided while they waited. Words checked against a connection the registry replaced while they waited say so where the replacement would take them, and otherwise describe the replacement as it stands. Speech whose channel id is on more than one server says no server was named for the words, and whom to ask. The open step's own failure names no state of the channel's transport, which can be open after a close that failed.
The two reasons for speech with no channel to go to keep their words in the log line and the trace, which, by our reading, connectome-host's
connectome-doctorreads for "no locus". The agent reads its own words, which name no tool, since whether one it may call could post them isn't known there, and say whom to ask. The code says why the two differ.channel_publishand the subconscious'sspeak_in_channelshare speech's refusal for a server's state and grant, framed asNothing was posted to channel "x": <reason>., and an unconfirmed post asPosting to channel "x" wasn't confirmed: ….speak_in_channel's refusals for a missing or unknown channel point to the tuned-out channels the subconscious reads, since it isn't offeredchannel_list.channel_publishrefuses an emptychannelIdas empty.A channel id on another server, or on several, names them in
channel_open's,channel_close's andchannel_decline's refusal.A server's own error words keep their final period, which a test now pins, and this host's own words, a request's or its store's, lose theirs where a reason composes them.
mcpl-request-outcome.test.ts's header says what an error response keeps.Pending changelog:
publish-waits-for-new-registration.fixed.mdno longer quotes the oldlifecycle work prevented delivery: …prefix. fix(mcpl): an open by speech or a reply that fails puts back the decision it replaced #568'sfailed-speech-open-puts-back.fixed.mdno longer says speech tells the agent the channel is closed, nor that the reply's notice useschannel_open's own words.Left as they are: the inbound-only refusals, which already say what to do, and the
>>resolver's texts.What each part is for
serverReach. It is the fix for the wrong refusal: without it, a reconnecting server's empty grant reads as a policy. It reads the registry's connection and one callback, and changes no decision.serverConnectingand its framework wiring. Without it, a server mid-connect or mid-restart would read as gone, with "ask whoever runs this host", while it was being connected. It adds one optional callback and its few lines in the framework, plus the helpermcplConnectInFlight, whichmcplServerAbsentTextnow shares.ChannelLifecycleErrorand its kinds. Each kind has its own next step: try again, wait for the server, ask whoever runs this host, or nothing. Traces carry the same clause, so the agent's words and the operator's can't drift apart.openIfClosedForSend's answers stay as fix(mcpl): an open by speech or a reply that fails puts back the decision it replaced #568 has them.byis the one field a kind adds, for the reply's notice, andcausekeeps the open's own error, as fix(mcpl): an open by speech or a reply that fails puts back the decision it replaced #568's classes did, for whoever inspects or logs the failure.afterClause. A server's own words keep their final stop, so a failure's further words after them start a sentence of their own, rather than following a stop with a semicolon.lifecycleFailureText's order. A decision the log refused changed nothing, so that comes first. A channel no longer registered comes next, since every other text would name a tool that can't find it. A later decision is what stands, so it comes before the server's state.channel_open's already-open answer, the decline's refusal, delivery): a request that failed leaves the open flag as it was, and the unconfirmed mark counts only while the channel isn't open. A text that read the mark there would tell the agent one thing while the next call did another.channel-decision-log-first's failing store takes the words it throws, so a test pins that a store's own sentence loses its final period where a reason composes it; with one fixed message, no test could tell.channel_closeas the call that closes the channel, and lets that call's own answer say when, since for a server that isn't up the close waits for the server. Where the server is up withoutchannels.lifecycle,channel_closewould only record a close, so the text says this host may not close it and whom to ask.laterClause. One phrase for when reconciling applies a recorded decision, with both of its conditions, so no text promises a change that reconciling would skip.channel_openrefuses one and points totune_out's cancel.settledcheck. A decision is said to be in effect only when no other lifecycle work for the channel is under way, since that work may have changed it already, as an open out at the server may have.channel_openalready answered.refusalRemedy. It adds what helps after a refused or unsettled operation: the server state's remedy, whom to ask for a grant, or another try where the channel's target kept changing or the decision couldn't be recorded, with whom to ask if that keeps failing. It goes on the open step's reason, on "the channel isn't confirmed open", and on a joined delivery's failure.openFailedReason. The open step's failure is said as an open that didn't complete, with what helps. A refusal from the wait after a successful open passes through whole, since "opening it didn't complete" would be false there.waitForOpenChannel's three reasons, and the failure it remembers. "The channel isn't confirmed open" alone can't say why. The failure of the work it waited for says that, when there was one.sendRefusal. One refusal for a send, worded from the server's state, so speech,channel_publishandspeak_in_channelsay the same thing in the same words. Itscheckedconnection keeps the last check from describing a connection that replaced the one whose grant refused: where the replacement would take the words, another try is worth it, and otherwise the refusal is worded as the replacement stands.channelIdcheck.??takes an emptychannelIdas given, so it read as having no channel to default to, when leaving it out would have used the default.requestErrorText. One rule for a failed request's words in a composed reason: this host's sentences lose their final period, since every composition adds its own ending, and a server's error answer keeps its words as they came, whichpublish-outcome's tests pin.resolveToolChannelEntry. AserverIdthat doesn't have the channel would otherwise read as no such channel, when the channel is on another server, which the clause names.loggedargument of deliverSpeech'sfail. It keeps the two no-locus reasons' operator words whereconnectome-doctorreads them, and only there.channel_publish's new frame. The reason now ends with its remedy, and fix(mcpl): require explicit final speech delivery receipts #163's trailing "Nothing was posted." came after it. The new frame says what happened first and leaves the remedy last.speak_in_channel's pointer. The subconscious isn't offeredchannel_list, so the pointer names where it finds a channel id.Cut: removing a gone server's lingering channel entries (behaviour, not words), the
>>resolver's texts, module-origin close texts that no agent reads, and the tools' descriptions.Tests
test/channel-agent-words.test.tsis new. It covers:channel_open,channel_close,channel_decline,channel_publish,speak_in_channeland speech, a server this host is restarting included, reconnecting or not;channel-lifecycle-order,channel-registry-routing,publish-outcome,channel-open-substitution,refusals-are-error-results,route-failure-targeting,channel-decision-log-firstandmcpl-gate-roundtrip. Inchannel-decision-log-first, the close and the decline whose decision fails to append now get an answer, where they asserted a rejection, a decline whose declined invitation can't be recorded still declines, and speech whose open can't record its decision says what helps. Inchannel-registry-routing, the no-locus test also reads the[routeSpeech]lineconnectome-doctorreads. Inchannel-lifecycle-order, fix(mcpl): an open by speech or a reply that fails puts back the decision it replaced #568's tests of speech's reasons, of the open-failed trace for a gone server and ofchannel_open's refusal without the grant take the new words, and its tests ofopenIfClosedForSend's answers pass unchanged. New cases there cover an overtaken open whose own clause already names the later close, a selected-again failure after a server's words that end in a stop, and the subscription policy's clause in the open-failed trace.At
ef4ea120, which holds this branch's merge ofmain63835621, on Linux, with Node 24.22.0,tsc --noEmitand the build are clean, and the whole suite passes: 5428 tests, 5401 pass and 27 skipped, none failing. Merged with today'smain(f63bbfa8), it typechecks and the whole suite passes too: 5507 tests, none failing.Red-first, with this branch's tests on
main's source (63835621): 88 of the 10 touched files' 347 tests fail, among them 21 ofchannel-agent-words' 22 and 28 ofchannel-lifecycle-order's 129. The twenty-second, that a granted server is refused nothing while the host is connecting to its id, passes there too, since it pins what the change keeps.mcpl-request-outcome's change is its header comment alone.Removals at
ef4ea120, each part above taken away alone: 74 pieces, each run against the test files that should catch it, and each fails a test there. #568's own 50 removals, with the anchors that named its classes or its words moved to the kinds, each fail a test there too.Not verified
changelog.d/channel-agent-words.changed.md, and fix(mcpl): publish() publishes only into a registration confirmed open, under the grant as it is then #569'schangelog.d/publish-waits-for-new-registration.fixed.mdand fix(mcpl): an open by speech or a reply that fails puts back the decision it replaced #568'schangelog.d/failed-speech-open-puts-back.fixed.mdupdatedWritten 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). Petra-1012 (Claude Opus 5.5), who wrote #568, read #568's side of this branch's merge of
main. Our internal review read the change before it opened, and read the merge too.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.