Skip to content

fix(mcpl): the channel tools and speech say what happened and what works from there - #589

Merged
nissa-seru merged 4 commits into
anima-research:mainfrom
nissa-seru:fix/channel-agent-words
Oct 19, 2026
Merged

nissa-seru merged 4 commits into
anima-research:mainfrom
nissa-seru:fix/channel-agent-words

Conversation

@nissa-seru

@nissa-seru nissa-seru commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

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.ts answered 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_open said channels.lifecycle not in "S"'s effective grant (§14.1), channel_publish said channels.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):

    • gone: not registered, or registered, closed and not reconnecting, with nothing connecting it;
    • connecting: a connect or restart of its id in flight, or a registered connection whose transport's policy exchange is still in flight;
    • down: registered, between transports, and reconnecting;
    • 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 "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 optional ChannelRegistryOptions callback, 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_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 (this host's disconnect or the server's de-listing), or else the server's state where it isn't up, and a request's own words where it failed with them, naming no channel tool, since none can find it;
    • a later decision that came first, 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. A tuned-out channel's messages are said to go to the subconscious, which channel_open doesn'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 main after 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, with by: the channel's descriptor or the subscription policy lifts the default it put back.

    openIfClosedForSend gives 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. overtaken adds 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. Its mcpl:channel-open-failed trace carries those words too, where it carried Server not found: <server> or the grant's name.

  • 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: 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 call channel_close if it needs closing, except where the server is up without channels.lifecycle: there it says this host may not close it, and whom to ask. 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: 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 of src/mcpl/tool-lifecycle.ts, a host that watches tool calls through RFC-007 now gets completed with isError as these two calls' terminal there, where it got failed. channel_open's terminal was already completed.

  • 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 is completed, where it was failed.

  • A delivery's reason (PublishOutcome.reason) is one clause that reads in every text that composes 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, true whether or not the close itself took;
    • or it isn't confirmed open, with the failure of the work they waited for, if there was one.

    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-doctor reads 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_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 an unconfirmed post as Posting 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 offered channel_list. channel_publish refuses an empty channelId as empty.

  • A channel id on another server, or on several, names them in channel_open's, channel_close's and channel_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.md no longer quotes the old lifecycle work prevented delivery: … prefix. fix(mcpl): an open by speech or a reply that fails puts back the decision it replaced #568's failed-speech-open-puts-back.fixed.md no longer says speech tells the agent the channel is closed, nor that the reply's notice uses channel_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.
  • serverConnecting and 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 helper mcplConnectInFlight, which mcplServerAbsentText now shares.
  • Reading the exchange's marker, not the grant, for "connecting". An exchange that was refused or went unanswered leaves the grant empty for that whole transport. A grant-based reading would say "still connecting" until the next reconnect, and the marker says "not permitted" once the exchange has ended, unless a connect or restart of the id is still under way. A closed connection's marker isn't read: it has no exchange to wait for.
  • ChannelLifecycleError and 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.
  • fix(mcpl): an open by speech or a reply that fails puts back the decision it replaced #568's three kinds. One class carries every lifecycle failure, so speech words fix(mcpl): an open by speech or a reply that fails puts back the decision it replaced #568's failures as it words the rest, and 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. by is the one field a kind adds, for the reply's notice, and cause keeps 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.
  • The in-effect reading. A change that already holds is read from the registry as its own checks read it (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.
  • The store fake's words. 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.
  • The decline's refusal of an open channel. It names channel_close as 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 without channels.lifecycle, channel_close would only record a close, so the text says this host may not close it and whom to ask.
  • The decline's close and a registration that went. The server may have closed the channel before its registration went, so a close cut short by a de-listing or by this host's disconnect isn't said to have failed. The text says the channel isn't registered here any more, or that its registration went and came back.
  • 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.
  • The registration-gone branch. A registration can go at the server's word or at this host's, so the text names which only where the failure says (a cancellation by this host's disconnect, or the server's de-listing), and otherwise the server's state where it isn't up, a request's own words, and the decision that stands; every other branch would name a tool that can't find the channel. A tune-out is in effect already, so only an open or a close is said to happen later.
  • "Doesn't stand" and "didn't go through". A change a later decision replaced, or one refused for the grant, can leave the transport either way, since other work for the channel, or the later decision's own, may hold it; the text says only what became of this call's change, and the later decision's own call says what became of the transport.
  • The tuned-out texts. A tune-out sends the channel's messages to the subconscious and keeps its epoch, which a plain open would discard, so channel_open refuses one and points to tune_out's cancel.
  • The backscroll clause. Reconciling opens a channel but asks for no history, so a recorded open would otherwise read as bringing the history too.
  • The settled check. 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.
  • Answering a refused decision write in all three tools. The store's error alone doesn't say whether the close or the decline took, and the agent needs that. channel_open already answered.
  • "stays recorded" on a refusal for the grant and on a substituted open. The decision is written before the server is asked, and reconciling applies it later, so "wasn't opened" alone would read as void.
  • 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_publish and speak_in_channel say the same thing in the same words. Its checked connection 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.
  • The shared-id reason. Speech can't name a server, so whoever runs this host is the one who can help, and a caller that can name one reads that none was.
  • The empty-channelId check. ?? takes an empty channelId as 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, which publish-outcome's tests pin.
  • The other-server clause in resolveToolChannelEntry. A serverId that doesn't have the channel would otherwise read as no such channel, when the channel is on another server, which the clause names.
  • The decline's texts. The decline takes effect whatever becomes of its acknowledgment, so each acknowledgment text starts there. Reading the decision once the awaits are done is what tells a decline that stands from one a later open replaced, and only the first can be repeated. The not-sent split, and reading the receipt by fix(mcpl): require explicit final speech delivery receipts #163's rule, keep a retry from posting the acknowledgment twice. The catch around the declined invitation's record keeps a decline that took effect from reading as failed. A representation that isn't a string is none, since the declined invitation's record keeps it as one.
  • The logged argument of deliverSpeech's fail. It keeps the two no-locus reasons' operator words where connectome-doctor reads 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 offered channel_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.ts is new. It covers:
    • each state's clause on channel_open, channel_close, channel_decline, channel_publish, speak_in_channel and speech, a server this host is restarting included, reconnecting or not;
    • a backscroll whose server isn't up, and a refusal for the grant;
    • a channel no longer registered while its open or close was out, at the server's word or at this host's, with the request's own words and the server's state;
    • a later decision that came first, for an open, a close and a tune-out, and with the server dropped meanwhile;
    • a change that already holds, with and without the grant, a close that isn't in effect while an open is out, and an open after a close that failed, said as channel_open's already-open answer reads it;
    • speech that has to open its channel, refused for the grant or the state, words that waited for lifecycle work the server dropped during, words that met a close behind later work after their own open completed, and words checked against a connection the registry replaced, by one that would take them, one without the grant and one still connecting;
    • each acknowledgment outcome and answer of a decline, including an error answer and answers that don't say whether it was posted;
    • a decline whose close a later open overtook, and one a later open or tune-out replaced while its acknowledgment was out, after each of four acknowledgment outcomes;
    • a decline whose channel the server stopped listing while its acknowledgment was out, and one whose channel stopped being registered while its close was out, at the server's word and at this host's, and one whose registration came back;
    • a decline's refusal of a channel that's open, where the server is up with the grant, up without it, and down;
    • an acknowledgment refused for the grant, whose text still says so when the server drops during the decline's close;
    • the framework's own connect of an unregistered id, a restart's claim on a registered id and an unregistered one, and a closed connection left with a marker;
    • a registered server's policy exchange read through its marker, including a marker an earlier transport left and an exchange that ended without a grant;
    • that a granted server is refused nothing while the host is connecting to its id.
  • The tests that pinned the old texts take the new ones. That's in channel-lifecycle-order, channel-registry-routing, publish-outcome, channel-open-substitution, refusals-are-error-results, route-failure-targeting, channel-decision-log-first and mcpl-gate-roundtrip. In channel-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. In channel-registry-routing, the no-locus test also reads the [routeSpeech] line connectome-doctor reads. In channel-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 of channel_open's refusal without the grant take the new words, and its tests of openIfClosedForSend'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 of main 63835621, on Linux, with Node 24.22.0, tsc --noEmit and the build are clean, and the whole suite passes: 5428 tests, 5401 pass and 27 skipped, none failing. Merged with today's main (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 of channel-agent-words' 22 and 28 of channel-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

  • macOS and Node 20, except through CI.
  • A live server's reconnect or restart, through the framework end to end. The marker's reading and the restart's claim are tested with a stub connection on the framework's own registry.

Written 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-seru account.

…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>
@greptile-apps

greptile-apps Bot commented Oct 19, 2026 •

Copy link
Copy Markdown

RetriggerView in GreptileConfidence Score: 5/5 Tier: apex

[Medium impact] The PR appears safe to merge; no actionable issues were found.

Summary

This PR makes channel failures explain what happened, what remains recorded, and what the agent can do next.

  • Channel refusals tell agents whether a server is ready.
  • Channel changes explain what took effect and what can happen next.
  • Speech and channel sends explain why delivery failed or remains uncertain.

Reviews (1) · Last reviewed commit: "fix(mcpl): the channel tools and speech ..." · Reviewed by Greptile

Cordelia-2434 and others added 3 commits October 19, 2026 16:10
…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
nissa-seru merged commit d8cc690 into anima-research:main Oct 19, 2026
6 checks passed
@nissa-seru
nissa-seru deleted the fix/channel-agent-words branch October 19, 2026 21:30
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant