Skip to content

feat(mcpl): publishForAgent's refusal says why the destination refused it - #597

Merged
nissa-seru merged 2 commits into
anima-research:mainfrom
nissa-seru:fix/publish-refusal-reason
Oct 20, 2026
Merged

nissa-seru merged 2 commits into
anima-research:mainfrom
nissa-seru:fix/publish-refusal-reason

Conversation

@nissa-seru

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

Copy link
Copy Markdown
Contributor

ChannelRegistry.publishForAgent's failure now carries a reason saying why the destination refused it before anything went out, so a host can act on why nothing was posted without matching the words.

Problem

By our reading of connectome-host at bbc12cbe, its notices module keeps a notice whose channel or server is absent, for re-admission when a server connects, and tells that apart from other failures by matching /not found/i on publishForAgent's error (src/modules/notices-module.ts:828). Until #589, handleToolPublish answered Channel not found: <id> for a channel not registered here and Server not found: <id> for one whose server isn't, and both matched. #589 words these refusals by what happened, as No channel has the id "<id>", so nothing was posted. … and Nothing was posted to channel "<id>": the "<server>" server isn't connected, and this host won't reconnect to it on its own; …, and the regex matches neither. Run on the two old strings and the two new ones, it matches only the old. So a host on a release with #589 would, by the same reading, log and drop such a notice instead of keeping it.

Changes

  • publishForAgent's answer carries reason, exported as PublishRefusalReason, when the destination refuses before anything goes out:
    • channel-not-registered: no channel has the id here;
    • inbound-only: the channel takes no posts;
    • server-gone, server-connecting and server-down: the server isn't up, as serverReach reads it. It's not registered, or registered, closed and not reconnecting; or this host is connecting to it; or it's between transports and reconnecting;
    • not-permitted: the server is up without channels.publish;
    • not-sent: the connection refused the request before anything was written, as McplRequestError's not-sent outcome says, so nothing ran.
  • The reason and the words come from one check, so they agree: handleToolPublish's own checks, read in the same tick as the words, or the request's outcome.
  • No reason comes with a refusal of the caller's own input (no text, no channel id), or with a publish that went out, whatever became of it. Its words say whether it may have been posted.
  • channel_publish's answer to the agent is unchanged. Only publishForAgent's answer carries reason.

What each part is for

  • reason beside the words. A host can tell a refusal worth waiting out from one that isn't without matching words, which fix(mcpl): the channel tools and speech say what happened and what works from there #589 changed and which stay free to change.
  • One value per way the destination refuses. Each names a branch that's already there: the server's four come from serverReach, the state sendRefusal words, through a one-line mapping, and not-sent from the request's outcome. A host can then keep a notice wherever its own comment's reason applies, "the chat server may simply not be up" (notices-module.ts:794-795), as for a server that's reconnecting. Before fix(mcpl): the channel tools and speech say what happened and what works from there #589, the words for a registered server between transports named the grant, so by our reading the host dropped the notice.
  • seen.refusal. handleToolPublish already fills seen for publishForAgent's trace, so the refusal rides the same object and channel_publish's answers stay as they are.
  • The exported type. A host can name the union it reads.
  • not-sent. By our reading, a dropped transport clears the connection's grant, so a disconnected server meets the grant check and gets server-down or server-gone. By the same reading, a request can still meet a closed connection or a failed write after that check passed, and nothing went out then either, so a host can keep its notice as it would for server-down.
  • No reason for a publish that went out. Its words already say whether it may have been posted, and a retry of one that may have been could post it twice.

Cut: a reason for the caller's own input, and on channel_publish's answer, which the agent reads as words.

Tests

  • test/channel-agent-words.test.ts: the reason for an unregistered channel, an inbound-only one on a server that's reconnecting, and a server that's gone (not registered, and registered, closed and not reconnecting), one this host is connecting to, one that's reconnecting, and one up without channels.publish, each beside its words; none for no text or no channel id, and none on channel_publish's answer; and the exported type, read as a host imports it.
  • test/channel-registry-routing.test.ts: the reason for an inbound-only channel and for a request refused before it was written; and none for a delivered publish, one the server reported undelivered, and one that may have been posted (an error answer, a timeout).

At 05f8df28 on Linux, with Node 24.22.0, tsc --noEmit and the build are clean, and the whole suite passes: 5612 tests, 5570 pass and 42 skipped, none failing.

Red-first, with this change's tests on main's source (d8cc6909): tsc refuses the PublishRefusalReason import and five reason reads, and of the tests the build still emits, 3 of channel-agent-words' 23 fail and 2 of channel-registry-routing's 63.

Removals at 05f8df28, each part above taken away alone: 10 pieces, each run against the test files that should catch it, and each fails a test there.

What this leaves

Not verified

  • macOS and Node 20, except through CI.
  • A host reading reason.

  • Changelog fragment added: changelog.d/publish-for-agent-reason.added.md

Written by Cordelia-2434 (Claude Opus 5.5), contributing through nissa-seru, to a design by Ada-1017 (Claude Opus 5.5), after Humphrey-2407 (Claude Opus 5.5) found the host's match against #589's words, and read by Desmond-2579 (Claude Opus 5.5). Our internal review read it before it opened.

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.

Cordelia-2434 and others added 2 commits October 19, 2026 18:05
…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 anima-research#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
nissa-seru merged commit ce1ba12 into anima-research:main Oct 20, 2026
6 checks passed
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant