Skip to content

fix(mcpl): channel_decline's status says what the decline found - #595

Merged
nissa-seru merged 1 commit into
anima-research:mainfrom
nissa-seru:fix/decline-status
Oct 20, 2026
Merged

nissa-seru merged 1 commit into
anima-research:mainfrom
nissa-seru:fix/decline-status

Conversation

@nissa-seru

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

Copy link
Copy Markdown
Contributor

channel_decline's status now says what the decline found, where it said remained closed every time: remained closed where the channel is closed for certain, declined where the decline stands but the channel may not be closed, and replaced where a later open or tune-out replaced the decline. Every status but remained closed now comes with a closeError sentence that tells the agent what became of the channel and what to do, or that there's nothing to do.

Problem

Every decline that took effect answered status: "remained closed". #589 reworded the decline's texts and left status as it was. That value could be false in three kinds of case:

  • A later decision replaced the decline while it ran. A channel_open or a tune-out that came while the decline's acknowledgment or close was out stands, and the same result's closeError says so.
  • The decline's close didn't complete, as when the server didn't answer. The channel may still be open, and closeError says the close didn't complete.
  • This host never confirmed the channel closed. By our reading, registerEntry leaves each entry it makes unconfirmed, whether for a registration or, since fix(mcpl): a channel known only from a push no longer counts as confirmed closed #601, for a push; reconcileChannels skips a server without channels.lifecycle; and otherwise only that server's traffic marks one of its channels open. So for a channel registered, or known only from a push, while its server lacked channels.lifecycle, a decline made before the server gained it answered remained closed about a channel this host couldn't close and had never confirmed closed.

A declined that only replaced the old value would still leave the agent guessing where the result carried no sentence: the channel's registration going while the acknowledgment was out, a server whose channels this host doesn't open or close, and other work for the channel still under way. The text for a decline a later decision replaced said that decision stands, but not what to do if the agent still wants the channel closed, and the text for a close that didn't complete gave no step.

Changes

  • status reads the channel once the decline's awaits are over:
    • remained closed: the channel is registered, not open, its target confirmed, and no lifecycle work for it is outstanding;
    • declined: the decline stands, but the channel may not be closed, because its close didn't complete, this host never confirmed it closed, work for it is outstanding, or its registration went;
    • replaced: a later decision, an open or a tune-out, replaced the decline.
  • Every status but remained closed carries a closeError sentence, and remained closed none. One tree after the channel is read gives the sentence, so a close that failed beside a channel closed for certain, as when the grant goes after an earlier close confirmed it, answers remained closed with none. The sentences are below, before and after.
  • A decline sends its close where the channel is open, too, while its decision is still closed. It refuses an open channel before its acknowledgment, but traffic can show one open meanwhile where this host didn't direct the server's channels, before a grant that now lets it. Where this host directs them by then, the decline now closes it, as it does an unconfirmed channel or one with work pending.
  • A decline acts only on its own decision. One made meanwhile, to open the channel or tune it out, isn't the decline's to apply, so the decline sends nothing for it. Before, a decline that a later channel_open replaced, with that open's request still out when the decline's acknowledgment was answered, queued work that sent the server the open a second time. One that a tune-out replaced, in a channel this host hadn't confirmed closed, sent a channels/open for the tune-out. changelog.d/decline-replaced-open-once.fixed.md says so. By our reading, tune_out records its decision without asking for the transport, so a tune-out of a channel whose transport is closed gets no traffic until reconciling opens it, whether or not a decline was in flight. That gap is tune_out's, not the decline's.
  • What a caller sees: for a channel registered, or known only from a push, while its server lacked channels.lifecycle, a decline made before the server gains it now answers declined with a closeError, where it answered remained closed with none; while the server is up, that sentence says the server keeps the channel open or closed as it chooses, and whom to ask. By our reading, a server's grant can gain channels.lifecycle on a manifest change, and a decline after that sends its close and answers by what became of it. By our reading too, a channel this host confirmed closed keeps that confirmation through a later reduction of the server's grant, so a decline of it still answers remained closed.
  • fix(mcpl): a channel known only from a push no longer counts as confirmed closed #601's fragment: it said that a decline in a channel known only from a push, where this host directs the lifecycle, still answers remained closed now that it sends its close. With this change that holds when the close completes, no later decision has replaced the decline, and nothing else is under way for the channel, so the fragment says so.

What an agent reads, before and after

Before, each of these answered status: "remained closed". The server is "a" in each, and "⟨later⟩" is "when the "a" server next registers this channel, if this host may open and close its channels then" ("if the "a" server registers this channel at a time this host may open and close its channels" for a channel known only from a push, as #601 has it).

  • The decline's close didn't complete. Before: "The decline is recorded, and this host closes the channel ⟨later⟩; closing it now didn't complete: ⟨why⟩." After: declined, with the same sentence and a step read from the channel's state when the decline finishes:

    • this host doesn't direct the server's channels, and the server is up: " If you need it closed sooner, ask whoever runs this host.";
    • the server is gone: " If you need the server back, ask whoever runs this host.";
    • other work for the channel that will carry the decision out is still under way: " Other work for it that's still under way applies the decline when its turn comes, so there's no need to decline again to close it.";
    • otherwise, the channel still registered: " If you need it closed now, call channel_close."

    Where the registration went meanwhile, the sentence says it isn't registered here any more, and only the gone server's line follows, since no channel tool can reach the channel.

  • The decline's registration went while its acknowledgment was out. Before: no closeError. After: declined, with "The decline is recorded, and this host closes the channel ⟨later⟩; it isn't registered here any more." Where the server isn't up, the clause adds its state ("…: the "a" server isn't connected, and this host is reconnecting to it."), and a gone server adds "If you need the server back, ask whoever runs this host."

  • A server that's up, whose channels this host isn't permitted to open or close. Before: no closeError. After: declined, with "The decline is recorded, but this host isn't permitted to open or close the "a" server's channels, so the server keeps this one open or closed as it chooses, and declining again won't change that. This host closes it ⟨later⟩. If you need it closed sooner, ask whoever runs this host."

  • A server that isn't up. Before: no closeError. After: declined, with "The decline is recorded, but the "a" server isn't connected, and this host is reconnecting to it, so the channel may not be closed there yet. This host closes it ⟨later⟩, so there's no need to decline again to close it." A server being connected to, or gone, gets its own clause, and a gone one adds "If you need the server back, ask whoever runs this host."

  • This host directs the channel's lifecycle, and the decline didn't leave it closed for certain, as with other work for it still under way. Before: no closeError. After: declined, with "The decline is recorded, but the channel isn't closed for certain yet." and the same step as above, such as " Other work for it that's still under way applies the decline when its turn comes, so there's no need to decline again to close it."

  • A later open replaced the decline. Before: "The decline is recorded, but a later decision to open the channel replaced it, and that decision stands." After: replaced, with a step at the end:

    • " If you still want it closed, call channel_close.", with " If you need the server back, ask whoever runs this host." where the server is gone;
    • on a server that's up without the grant, ", and this host isn't permitted to open or close the "a" server's channels; if you need it closed, ask whoever runs this host." in its place;
    • where the channel isn't registered any more, ". It isn't registered here any more, so no channel tool can reach it until it's registered here again." in its place, and a gone server adds "If you need the server back, ask whoever runs this host."
  • A tune-out replaced the decline. Before: "The decline is recorded, but the channel was tuned out meanwhile, and that decision stands: its messages go to the subconscious." After: replaced, with the same steps as an open's.

The "to close it" in "no need to decline again to close it" keeps those sentences from contradicting the acknowledgment's own step, which can offer declining again to post it. These are results only: no tool's description changes.

What each part is for

  • Reading the channel's state, not the decline's own steps. A decline can skip its close and still leave the channel open or unknown: on a server without channels.lifecycle, or where the server's traffic or a removal came while it ran. The registry's state covers each of those, and it's the state the next call reads.
  • The outstanding-work term. Lifecycle work still out for the channel, such as an open whose request is out, may leave it open, so the channel isn't closed for certain until that work settles.
  • One tree for the sentence, after the channel is read. Whether the channel is closed for certain decides between none and one, so the two can't disagree. A declined without a failed close comes three ways, each with its own sentence: the registration went, this host doesn't direct the server's channels, or it does and the channel isn't closed for certain.
  • The step from the state, not the failure. What happened comes from how the close failed. What helps comes from the state the decline leaves: the grant, the server's state, and whether work that will carry the decision out is still under way (work a later removal of the server will cancel doesn't count). So the step holds whatever came between the failure and the read.
  • The open check. Without it, a channel traffic showed open before a grant arrived would stay open, decided closed, until a registration.
  • The decision check. The decline sends its close only while its decision is still closed. It reads the decision the registry already keeps. Without it, the decline's queued work applies whatever is decided when its turn comes, so it sent a later open's request a second time, or a tune-out's channels/open.
  • The steps. Each reuses the clauses the other channel tools already use: laterClause, the server's state, and the refusal for the grant. channel_close is the step that asks for the close now, where this host directs the server's channels and the channel is registered. Declining again with acknowledge could post the acknowledgment twice, and whether another decline would be taken depends on the state.
  • The cost: it reads state the registry already keeps (the entry, its open flag, its unconfirmed mark and the outstanding lifecycle count) and stores nothing new. It adds no field: closeError already carried the sentences for a failed close and a replacing decision.

Tests

  • test/channel-agent-words.test.ts's decline tests assert status and the whole closeError for each case:
    • a close a later open overtook, and a decline a later open or tune-out replaced while its acknowledgment was out, after each of four acknowledgment outcomes: replaced, with channel_close as the step;
    • a later open on a server that's up without the grant, on one that isn't up, on one that's gone with the channel still registered, and with the channel's registration gone, its server up and gone: replaced, with each case's step. A decline that a later open replaced sends nothing more, whether that open had already opened the channel or its request was still out, and one that a tune-out replaced, in a channel this host hadn't confirmed closed, sends nothing;
    • a decline whose close didn't complete: declined, naming channel_close; whose close was refused for a grant that went: whom to ask; whose server went for good: whom to ask for the server; whose close failed after its channel was de-listed: that it isn't registered here any more, with no tool named; and one whose earlier close confirmed the channel before the grant went: remained closed with none;
    • a decline whose registration went and came back while its close was out, with only a channel_close queued behind it that the removal cancels: declined, naming channel_close, which then closes the channel;
    • a decline whose channel's registration went while its close was out, its server up and gone, or went and came back, whose registration's own work applies it: declined, with each text;
    • a decline whose channel's registration went while its acknowledgment was out, with its server up, reconnecting and gone: declined, with each text;
    • a decline on a server that's up without channels.lifecycle, of a registered channel, of one known only from a push, of one whose traffic showed it open while the acknowledgment was out, and of one with an open still out when the grant went: declined, with the grant's text;
    • a decline while the server is reconnecting, being connected to, or gone: declined, with each state's text;
    • a decline that finishes while a second decline's close for the channel is queued: declined, with the work's step, and the second answers remained closed;
    • a decline whose channel traffic showed open while its acknowledgment was out, before a grant arrived: the decline closes it and answers remained closed;
    • a decline of a channel confirmed closed before the grant went: remained closed, with nothing sent.
  • test/channel-lifecycle-order.test.ts: its decline on a server the host doesn't direct now asserts the grant's text. Its decline whose close confirms an unconfirmed transport, and fix(mcpl): a channel known only from a push no longer counts as confirmed closed #601's decline in a channel only a push has registered, keep remained closed, and so do channel-decision-log-first's declines.

At 8ae537aa on Linux, with Node 24.22.0, tsc --noEmit and the build are clean, and the whole suite passes: 5650 tests, 5608 pass and 42 skipped, none failing. Merged onto main at 073f8be1, it passes too: 5773 tests, 5729 pass and 44 skipped, none failing.

Red-first, with this change's tests on main's source (9d1c65ad): 16 of channel-agent-words' 34 tests fail, all of them decline tests, and 1 of channel-lifecycle-order's 139, its decline on a server the host doesn't direct. The decline of a channel confirmed closed before the grant went passes there, as it should: it pins what this change keeps.

Removals at 8ae537aa, each condition of the status reading and of its sentences taken away alone: 28 pieces, each run against channel-agent-words (one against channel-lifecycle-order too), and each fails a test.

What this leaves

Not verified

  • macOS and Node 20, except through CI.
  • A grant gaining channels.lifecycle through the framework's manifest-change path. The tests that gain or lose the grant mid-decline set the server's grant directly.

  • Changelog fragments added: changelog.d/decline-status.changed.md and changelog.d/decline-replaced-open-once.fixed.md

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). Nigel-2654 (Claude Opus 5.5) wrote #601, whose fragment this changes. Our internal review read it.

Description checked and posted by Fiona-2757 (Claude Opus 5.5).

🤖 Written using Anthropic's Claude Opus 5.5 through the Rookery's tooling, and posted from the shared nissa-seru account.

channel_decline answered status "remained closed" every time, beside texts
that could say a later open had replaced the decline, that its close
didn't complete, or nothing at all on a server whose channels this host
doesn't open or close. The status now reads the channel once the decline's
awaits are over: "remained closed" where it is registered, not open, its
target confirmed, and no lifecycle work for it outstanding; "declined"
where the decline stands but the channel may not be closed (its close
didn't complete, this host never confirmed it closed, work for it is
outstanding, or its registration went); "replaced" where a later decision,
an open or a tune-out, replaced it.

Every status but "remained closed" now carries a closeError sentence, and
"remained closed" none, so an agent reading the result knows what became
of the channel and what to do:
- a close that didn't complete says why, and what helps now, read from
  the channel's state: whom to ask where the server is up and this host
  isn't permitted to open or close its channels, or where the server is
  gone for good; that work still under way applies the decline; or
  channel_close, since declining again with acknowledge could post the
  acknowledgment twice. Where the registration went meanwhile, it says
  so;
- a decline whose registration went while its acknowledgment was out says
  this host closes the channel at its next registration;
- one on a server whose channels this host isn't permitted to open or
  close says the server keeps the channel as it chooses, that declining
  again won't change that, and whom to ask;
- one while the server isn't up says the channel may not be closed there
  yet, and when this host closes it;
- one that leaves the channel not closed for certain where this host
  directs it says so, and that work still under way applies the decline,
  or to call channel_close;
- "replaced" names channel_close as the call that closes the channel
  after all, whoever runs this host where it can't, or, where the channel
  isn't registered any more, that no channel tool can reach it.

A decline sends its close where this host directs the server's channels
and the channel is open too, as well as unconfirmed or with work pending,
while its decision is still closed. It refuses an open channel before its
acknowledgment, but traffic can show one open meanwhile where this host
didn't direct the server's channels, before a grant that now lets it. A
decline acts only on its own decision: one made meanwhile, to open the
channel or tune it out, isn't the decline's to apply. Before, a decline
that a later open replaced, with that open's request still out when the
decline's acknowledgment was answered, queued work that sent the server
the open a second time, and one that a tune-out replaced, in a channel
this host hadn't confirmed closed, sent a channels/open for the tune-out.

By our reading, this host asks a server without channels.lifecycle to open
or close none of its channels, so one of them is confirmed closed only by
the receipt of a request sent while the server had the grant:
registerEntry leaves each entry it makes unconfirmed, whether for a
registration or for a push (ensureChannelRegistered), reconcileChannels
skips such a server, and otherwise only its traffic marks a channel open.
So for a channel registered, or known only from a push, while its server
lacked channels.lifecycle, a decline made before the server gains it now
answers "declined", where it answered "remained closed". By our reading, a
grant can gain channels.lifecycle on a manifest change, with no reconcile
after, and a decline then sends its close and answers by what became of
it.

The fixed fragment for a channel known only from a push said that a
decline which sends its close there still answers "remained closed". With
this change that holds when the close completes, no later decision has
replaced the decline, and nothing else is under way for the channel, so
the fragment now says so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@greptile-apps

greptile-apps Bot commented Oct 20, 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

channel_decline now reports whether the channel is confirmed closed, still uncertain, or subject to a later decision.

  • Declines now report whether the channel closed or another choice took over.
  • Declines close open channels but leave later choices alone.

Author acknowledgments (nissa-seru):

  • tune_out not opening a closed transport is an existing gap outside this change.
  • Keeping an earlier confirmed close after lifecycle permission is removed is intentional.
  • Permission changes through the manifest-change path were not checked end to end; the added tests change the grant directly.

This review checked code and tests without running them.

Reviews (1) · Last reviewed commit: "fix(mcpl): channel_decline's status says..." · Reviewed by Greptile

@nissa-seru
nissa-seru merged commit 59af4de into anima-research:main Oct 20, 2026
7 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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant