Skip to content

fix(refusal): mark the message a refused turn answered; give rewind retries their channel; count pushed messages as delivered - #947

Open
nissa-seru wants to merge 38 commits into
anima-research:mainfrom
nissa-seru:fix/refusal-mark-trigger
Open

nissa-seru wants to merge 38 commits into
anima-research:mainfrom
nissa-seru:fix/refusal-mark-trigger

Conversation

@nissa-seru

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

Copy link
Copy Markdown
Contributor

Why. When the model refuses a turn, the person whose message it was answering should see a reaction on that message, and should get the reply when a retry succeeds. On main, a refused turn woken by a pushed mention marks no message, or an earlier message in another channel, and a rewind's retry carries no channel, so under the default routing its reply isn't published.

This makes a refused turn's reaction go on the message the turn answered, in that message's channel, makes the retry a rewind queues take that channel as the channel it was woken from, and counts a push that names a new message, such as a mention in a channel the agent hasn't opened or a DM, as the latest delivered inbound message unless its server calls it ambient. On main, the reaction and the failure log's locus both take the latest message delivered from any channel, no push counts, and a rewind's retry carries no channel. So a refused turn woken by a pushed mention marks no message, or an earlier one in another channel, and, run through a whole host on agent-framework 0.32.0 with autoRewind on and no home channel, a turn retried after a refusal's rewind published no reply.

A refused turn woken by a mention pushed from a channel the agent hasn't opened

  • base (main): no reaction, and its failure record's locus is null. After a message answered in an open channel, the reaction goes on that earlier message instead, with locus its channel.
  • head: the reaction goes on the mention, and locus is its channel.

A refused turn started from wakes for different messages, or woken by a reaction

  • base (main): the reaction goes on the latest delivered message.
  • head: no reaction.

A rewind's retry, from the refusal auto-rewind, /unstick or the poison-history breaker

  • base (main): it's queued with no channel. After a turn that answered a message in an open channel, no answer is published in that channel, and, with speech.locus and prose routing at their defaults, its request carries "[routing] Your plain speech currently has no channel". Later requests say the reply had no channel to go to.
  • head: it takes the shed turn's channel as the channel it was woken from. A refusal of it that no further rewind takes, the one a rewind gives up on with its budget spent included, is marked on the shed message. Under the default prose routing its answer is published there, and later requests carry a [delivered] receipt. A conversation fork's retry speaks in its home channel, and locked, explicit and disabled prose routing route a retry's speech as they route any turn's. A later turn with no channel of its own is told its plain speech has no channel, since the retry's channel was the last one it was told of.

An operator's /unstick after a refused turn

  • base (main): its run, and the retry its rewind queues, have no channel, so with prose routing at its defaults neither's answer is published.
  • head: both answer the message of the agent's newest turn, in that message's channel. After a restart since the refusal, or when the newest turn was a timer's that failed before it stored anything, the run answers no message and is told its plain speech has no channel.

A rewind's retry merged into one start with other wakes

  • base (main): the retry has no channel, so the start speaks in the channel of its newest addressed wake, or else of its newest wake.
  • head: while the message the retry answers is still in what the agent is sent, the retry ranks with addressed wakes by when it was queued. So when a message from another channel addressed the agent while the turn ran and stays in what it's sent, as on a stream that takes no injected messages, the retry, queued later at the rewind, speaks in its own channel, where main speaks in the other's. Once the rewind has set the retry's own message aside, or when the retried turn was woken for a channel but answered no single message, any addressed wake outranks the retry, and the retry outranks ambient wakes. A retry of a turn woken for no channel, such as a timer's, carries no channel, as before. Either way the retry isn't marked addressed, and a start it wins is marked addressed only when an addressed wake is in it. A start whose channel the retry decides credits no author in its telemetry.

A push that names a new message

  • base (main): never the latest delivered inbound message.
  • head: the latest inbound, for incoming and for the 'latest-inbound' fallback, unless its server tagged it chat:ambient and not chat:addressed, or its server was removed before the push was processed. A channel-scoped coalesced push becomes it once its acceptance commits, and a deferred create still doesn't. An ambient message from an open channel, which comes as a channel message, still counts, as on main.

A coalesced acceptance after a newer message became the latest delivered inbound

  • base (main): the older message becomes the latest.
  • head: the newer one stays the latest.

InferenceRequest

  • base (main): no answersMessageId.
  • head: an optional answersMessageId, the message a wake is for.

No notice's text changes.

This changes what an agent reads. A rewind's retry now takes the channel the shed turn was woken from, and so does an operator's /unstick run. In a probe of three cases, with speech.locus and prose routing at their defaults, the retry's request at 71193a9fef carried the "[routing] Your plain speech currently has no channel" notice, and on this branch it doesn't. "What an agent reads", below, gives the cases and a table of what changes. No notice's text changes.

It hasn't run against discord-mcpl or Discord: the tests use a loopback MCPL server that sends the push shapes discord-mcpl 0.6.0's source sends. Not verified names the cases no test covers.

  • Changelog fragments added: changelog.d/fix-refusal-mark-trigger.fixed.md and changelog.d/fix-refusal-mark-trigger.added.md.
Credits

Ada-1017 decided its shape, Gladys-2592 built it, and Cedric-2714 read it and ran its late-commit cases and mutants. Cedric-2714 also read the round that answers the automatic review, and the merge of main. Agnes-1009 reported the refusal whose locus was null, and Gladys-2413 found where it came from. Gladys-2592 merged main at 0.33.0 into it and re-premised its tests on what that brought, and Hilda-2681 read the merge and those tests. William-2616 merged main at 36b6dfe9de into it and built the rounds after, which Byron-2209 read, with Oscar-1808 ruling where /unstick answers and which pushes count, Octavia-2412 proposing that a retry rank by an internal mark, and Amos-2081 finding that the framework had stopped calling noteDelivered. Fiona-2757 wrote this description, Gladys-2592 updated it for the third commit, both merges and the context view, Daniel-2612 put it in this shape, and William-2616 updated it for the rounds after 8b6038ec70.

Details for agents

Head 1192bca9c8: 8b6038ec70 merged with main at 36b6dfe9de (c1c06e97b7), then 28 commits (git log c1c06e97b7..1192bca9c8), whose test commits and runs Tests gives by range. 8b6038ec70 is af9d4b6c66 merged with main at fe4631718f, the 0.33.0 release (73769b4cf8), then two commits that change only test/refused-turn-mark.test.ts (78ec541617, cb45b124b5), and one that changes only the fixed changelog fragment. On main at 36b6dfe9de, reactToRefusal and noteRefusal still read incomingFor (src/framework.ts:14982, :35838), only the channel lane calls noteDeliveredOrPark (:20538, :20622, :20749), and the refusal auto-rewind's, /unstick's and the breaker's retries are queued with no channel (:30235, :15540, :36138).

A refused turn's reaction now goes on the message the turn answered, in that message's channel, and the retry a rewind queues now takes that channel as the channel it was woken from. A push that names a new message, such as a mention in a channel the agent hasn't opened or a DM, now counts as the latest delivered inbound message. At 71193a9fef the reaction and the failure log's locus both take the latest message delivered from any channel, and no push counts. So a refused turn woken by a pushed mention marks no message, or an earlier message in another channel, and its locus is null or names that other channel. A rewind's retry carries no channel. In two tests whose shed turn answered a message in an open channel, one a refusal auto-rewind and one the poison-history breaker, no answer is published in that channel within 60 seconds. This branch adds InferenceRequest.answersMessageId, the message a wake is for. Its two new test files fail 19 of their 26 tests over 71193a9fef's src, and at 82122c4c9e, this branch's first commit, they pass, with the change's other test files and three more that call noteDelivered, 506 of 506. Its second commit, 00aeb8e911, adds a test that a subagent's refused inference still marks nothing and logs no locus. In its third, 1f72f72798, wakes merged while the agent is quiesced or waiting out its provider now answer a message only when every merged wake answers the same one, as a merged start's already did, and a turn of an ephemeral run answers no message even when a channel message woke it. The branch then merges main at 4429afe2b6. Then af9d4b6c66 turns off wake onboarding in the gate configuration that one test file uses for its tests that expect no turns, since main's onboarding, added after this branch's base, woke the first message of one of those tests.

Problem

At 71193a9fef:

  • The refusal mark and the failure locus read the registry's latest delivered inbound message. reactToRefusal (src/framework.ts:13388) and noteRefusal (:31696) both read channelRegistry.incomingFor(agentName) (src/mcpl/channel-registry.ts:2522), one message per agent across every channel.
  • No push sets it. Only the channels/incoming lane notes a delivery (noteDeliveredOrPark, src/framework.ts:17461, :17525, :17648). The push lane (handleMcplPushEvent, :19161) computes admission's locus (:19263) and doesn't apply it. discord-mcpl 0.6.0 sends a message from a channel the agent hasn't opened, or a DM, as a push (src/server.ts:4528 at its v0.6.0), and as a channel-scoped coalesced create when the host offers channel-scoped coalescing and the channel is registered (:3825, :4620). This framework always offers it (PUSH_COALESCING_SUPPORT, src/mcpl/push-coalescer.ts:25). Only the channel lane's acceptance commits a parked anchor (src/framework.ts:18711), so a coalesced push's stays parked.
  • A rewind's retry is queued with no channel: the refusal auto-rewind and /unstick (doRewind), and the poison-history breaker (quarantinePoisonedHistory, :32040), which is on by default.
  • The channel lane commits a parked anchor whatever was delivered since. A resend answered from its receipt after a failed acknowledgement, or a message delivered while the acceptance waits for its commit, leaves the older message as the latest.

Run over 71193a9fef's src, this branch's tests show each of these:

  • a mention pushed from a closed channel after a message answered in an open one: the reaction goes on that earlier message, in the other channel;
  • a pushed mention alone, and the same mention as a channel-scoped coalesced create: the refusal is logged, and no reaction comes within 60 seconds;
  • a channel-scoped push that names no message, a deferred push's rendered batch, and a debounced push: locus is null where the push's channel is expected;
  • a refusal rewind's retry, and the poison-history breaker's: no answer published in the shed message's channel within 60 seconds;
  • a turn started from a merge of two wakes for messages that share an id in two channels: a reaction goes on that id in one of the channels;
  • under 'latest-inbound', a mention pushed from a closed channel: the fallback locus stays on the earlier message's channel;
  • on the channel lane, a coalesced create resent after a failed acknowledgement, or committed after a newer message arrived: the older message is the latest.

Changes

  • The reaction goes on the message the refused turn answered, in that message's channel, or on none. A turn re-armed after its retries ran out that speaks in another channel than its message's answers no message, so its refusal marks none and its record names the channel it speaks in: a mark in the message's channel would sit where the turn didn't speak. The failure record's locus names that channel, or, for a turn that answers no single message, the channel it was woken for, unless the wake gave only a channel id that no single registered channel answers to. One pair, TurnRouting.answers, is set when the turn opens, from its first start's wake. It serves the reaction, the record and a rewind's retry through every later start of the turn, and a re-pin or channel_open doesn't move it.
  • reactToRefusal's doc. agent-framework 0.31.0's dist/src/framework.d.ts (:2857–2864) documents the private method as reacting "on the message that holds the refused agent's conversational locus (the most recent incoming channel message that isn't a reaction, reaction removal, edit or deletion marker or a chat:system notice, apart from an edit the coalescer delivers on acceptance as the agent's first view; an ephemeral run has none, so a subagent's refusal marks no message)". On this branch it reads "react on the message the refused turn answers". 0.23.0's changelog says of an ephemeral run that "a refusal of its inference adds no reaction anywhere, and its failures.log record names no locus" (CHANGELOG.md:5491–5492). The test the second commit adds pins that (Tests). A turn of an ephemeral run gets no answered message even when a channel message woke it, as when one names the run in its targetAgents, so the rule holds there too, for the reaction, the locus and a rewind's retry (third commit).
  • InferenceRequest.answersMessageId. Each wake for one message carries the message's platform id: a channels/incoming resident's, a conversation fork's and a correction's take the event's messageId, a push's its origin.messageId when the push's channel is known, and the wake that wake onboarding starts its message's, so a refusal of that turn is marked on it. A marker carries none (a reaction, a deletion, a chat:system notice, or an edit not delivered as the agent's first view of its message), and neither does a batch, a render, a timer, a notice, or a channel-scoped push that names no message. Wherever wakes merge, in a merged start, a quiesce or a provider hold, the wake that's kept answers a message only when every merged wake has the same one in the same channel, and otherwise answers none. A quiesce coalesces wakes by the same key as at 71193a9fef.
  • A rewind's retry carries the shed turn's channel, message and wake channels, for the refusal auto-rewind, /unstick and the poison-history breaker: its refusal is marked on that message, and its turn takes that channel as the channel it was woken from. The wake channels are those main gives a turn's retries, so the retry is told of the channels the shed turn's batch named as well as answering its message, and a test pins the breaker's retry keeping both. The rewind sheds the turn's newest exchange, which is the shed message when the refused round was the turn's first, and the retry sees the rewind's marker in its place. A refusal a further rewind takes isn't marked, and the one a rewind gives up on is.
  • The retry ranks by whether its message is still in what the agent is sent. A merged start speaks in the channel of its newest addressed request, taken to be the message the agent answers. The rewind can set aside the retry's message, when the refused round was the turn's first, or, when it wasn't, a newer message injected into the refused round. So the retry ranks with addressed requests only while a row with its message's id is still in the agent's presented window, or its record, and otherwise below every addressed request and above ambient ones. The rank is an internal mark, so InferenceRequest.addressed keeps meaning that the triggering message addressed the agent, on the retry and on a start it wins. A ranked retry that wins is its own provenance, as an addressed winner is.
  • /unstick's request carries the routing of the agent's newest turn, which a turn keeps until the next one opens, and which /unstick reads only while the agent is idle. So its run answers that turn's message in its channel, usually the refused message an operator acts on. A restart drops the routing, which isn't persisted.
  • A push that names a new message counts as the latest delivered inbound, through noteDeliveredOrPark on both lanes, with origin.threadId. A channel-scoped coalesced push counts once its acceptance commits its parked anchor. A delivery at assembly, such as a deferred push's render, neither notes nor parks, so a deferred create's anchor stays unset, as at 71193a9fef. An ambient push, one its server tagged chat:ambient and not chat:addressed, neither sets nor parks the anchor, on either lane: discord-mcpl sends a closed channel's chatter as a push, often channel-scoped coalesced, and under 'latest-inbound' it would otherwise become where a turn with no channel of its own speaks. A push whose server was removed before the push was processed sets nothing, since its channel is then a placeholder. A push wake carries its message only with its channel.
  • A parked anchor is committed only when no newer delivery has set the anchor since, on both lanes, by a delivery sequence. The parked key is JSON.stringify([serverId, eventId]), so a colon in either can't make two pairs collide now that the map holds pushes too. The framework records deliveries through ChannelRegistry.noteDelivered(...), which still returns void, so a host or module that wraps it on framework.channels sees them, as on main, except a coalesced message accepted after a newer message became the latest delivered inbound, whose note main made and this change skips. To learn whether a call set the anchor, the framework reads a count noteDelivered bumps when it does, keyed by a symbol the package doesn't export, before and after the call. A wrap that never calls through moves nothing, so nothing counts as set.

What an agent reads

A probe outside this branch printed the bracket-tagged notices in each request, with this branch's tree and with 71193a9fef's, in three cases with speech.locus and prose routing at their defaults, each of which kept the turn's channel: a refusal auto-rewind in an open channel, the same for a mention pushed from a closed channel, and the poison-history breaker. In each case, at 71193a9fef the retry's request added "[routing] Your plain speech currently has no channel — it stays in your archive. Use an explicit send tool to reach a channel." after the rewind's marker, and on this branch it added the marker alone. No notice's text changes.

The context view of cb45b124b5 merged onto main at aa134e75fc19 (#947 (comment)) shows the same in its refusal-recovery run, and two things that follow from the retry's answer being published:

  • the requests after it carry a [delivered] receipt (appendProseDeliveryReceipt, src/framework.ts:23669), where main's carry "[send-undeliverable] Your previous reply (… chars) had no channel to go to" (:35620);
  • a later turn with no channel of its own, such as a timer's, is told "[routing] Your plain speech currently has no channel", since the retry's channel was the last one announced (announceLocusIfChanged, :24196, which speaks only when the channel changes). On main that turn isn't told, since the channel was already none.
71193a9fef this branch
Refused turn woken by a mention pushed from a closed channel no reaction, locus: null reaction on that mention, locus its channel
The same, after a message answered in an open channel reaction on that earlier message, locus its channel reaction on the mention, locus its channel
Refused turn woken by a debounced push, with nothing delivered before it no reaction, locus: null no reaction, locus the push's channel
Refused turn started from a merge of wakes for different messages reaction on the latest delivered message none
Refused turn woken by a reaction reaction on the latest delivered message none
A rewind's retry, the breaker's included, after a turn that answered a message in an open channel no answer published in that channel answer published in that channel
A pushed message (closed channel, DM) its server doesn't call ambient never the latest inbound the latest inbound: incoming, and the 'latest-inbound' fallback
/unstick's run after a refused turn that answered a message in an open channel no channel that message's channel
A rewind's retry merged with a newer ambient wake from another channel the ambient wake's channel the retry's channel
A coalesced acceptance after a newer message became the latest the older message becomes the latest the newer stays the latest
A deferred create anchor unset anchor unset

Tests

On Linux with Node 24.22.2 and npm 11.19.1, with membrane 0.8.0, chronicle 0.5.1 and context-manager 0.24.2, as npm ls printed them at 82122c4c9e:

  • At 82122c4c9e, in a clean checkout, the 11 test files this change adds or edits and the 3 others that call noteDelivered directly give 506 of 506. The whole suite runs in CI on this pull request.

  • Red first: test/refused-turn-mark.test.ts and test/push-delivery-anchor.test.ts from 82122c4c9e, run over 71193a9fef's src, fail 19 of their 26 tests. Problem names what twelve of them show. The 7 that pass are:

    • three controls: an addressed message in an open channel, refused, is marked on itself; every resident a message wakes marks it; a turn refused in its retry after a provider's stated wait marks the message it opened for;
    • the push lane's two late-commit tests, since 71193a9fef never commits a push's anchor;
    • the two tests that a push naming no message, and a deferred create, leave the latest inbound where it was.
  • Mutants, at 82122c4c9e: 15 mutants, each taking one piece of the change out of the built dist/src/framework.js, each run against both new test files whole. 14 fail at least one test. The one that doesn't is under Not verified.

  • At 00aeb8e911, in a clean checkout with the same versions, test/ephemeral-no-global-fallback.test.ts passes 12 of 12. Its new test refuses a subagent's inference after a resident's turn has answered the latest inbound message, and checks that the refusal record names no locus. It then marks a refusal of the resident's as a control, and checks that the only reaction sent is that one, on the message the resident answered. Two mutants each take the ephemeral guard away from one half. With the mark falling back to the latest delivered inbound message, 2 of 12 fail: the new test, at its check that only the resident's refusal is marked, and the older direct test of a subagent's mark. With the record's locus falling back the same way, 2 of 12 fail: the new test, at its check of the locus, and the older direct test of a subagent's record. Over 71193a9fef's src the file passes 12 of 12 as well, so the rule held there, and the new test is red against the mutants, not the base.

  • At 1f72f72798, the third commit, in a clean checkout with the same versions, the 14 test files pass 509 of 509.

    • Red first: its three changed test files, run over 00aeb8e911's src, fail 3 of their 40 tests: a conversation fork's two messages that wait out a quiesce wake one turn, whose refusal marks nothing; a provider hold that takes two wakes for different messages as one keeps neither message; and an ephemeral run's turn woken by a channel message, whose refusal and whose rewind's retry mark nothing and log no locus. That last test marks the agent's name as a run's, as createEphemeralAgent does, and wakes it with a channel message.
    • Four mutants, each run against those three files: without the quiesce's clearing, only the quiesce test fails; without the provider hold's, only the hold test. With coalesced deliveries noted at once instead of parked, the four late-commit tests fail at their check that the older message's anchor is parked. With the locus read from the latest delivered inbound message, 6 of 40 fail, among them the test of a resident's locus after the latest inbound message moved to another channel. From cb45b124b5 on, that quiesce test's two wakes no longer meet in the quiesce, as the bullet for cb45b124b5 says.
  • At af9d4b6c66, the head before main at 0.33.0 was merged in, in a clean checkout with the same versions, the 14 test files and 11 more pass 724 of 724. The 11 are the 9 test files feat(gate): suppress rules keep an agent's chosen events out of its context #975 adds or edits, test/channel-registry-typing.test.ts and test/disabled-prose-typing.test.ts. At the merge commit before it, b130f6a7bb, one test in test/push-delivery-anchor.test.ts failed in each of three runs: wake onboarding started a turn for the first message that test sends, and resolveLocus then gave that turn's channel where the test expects the fallback's.

  • At cb45b124b5, whose src and tests 8b6038ec70 keeps, in a clean checkout with Node 24.22.2 and npm 11.19.1, and with membrane 0.8.0, chronicle 0.6.0 and context-manager 0.25.0 as npm ls printed them, the 14 test files pass 511 of 511. Three test files that feat(presentation): one record, a presentation index per agent — the deferral queue replaced (#291 step 3) #844 changed, on a message heard mid-turn, push coalescing and the removed deferral queue (test/heard-mid-turn-wake.test.ts, test/mcpl-coalescing.test.ts, test/no-deferral-residue.test.ts), pass 274 of 274.

    • What merging main changed for the tests: main no longer has a deferral queue. It stores a message as it arrives, and a wake reads its message's stored row only for the resident the row was stored for. Seven tests in test/refused-turn-mark.test.ts waited on the queue, or on a quiesce parking two wakes as one. The six that waited on the queue now wait on the message being stored, the seventh, the conversation fork's quiesce test, waits on no turn running while the host is quiesced, and each asserts what it did before.
    • Where wakes for two messages meet: the conversation fork's quiesce test still passes, but its two wakes each read their own row now, so they meet when the turn resumes, not in the quiesce. Two new tests reach the quiesce's clearing and the provider hold's with a second resident, whose wakes read no row. Its two wakes park as one in a quiesce, and its provider's stated wait holds as one the two wakes that one delivery of two messages makes in one millisecond. Each test checks that the wakes meet there before it checks the reaction.
    • Red first: the two new test files from cb45b124b5, run over fe4631718f's src, fail 24 of their 30 tests.
    • Mutants: at cb45b124b5, without the quiesce's clearing only the second resident's quiesce test fails, and without the provider hold's, only its hold test and the hold's own unit test. At 78ec541617, whose tree differs from cb45b124b5's only in those two tests' checks, 16 mutants each take one piece of the change out of the built dist/src/framework.js, each run against both new test files whole. 15 fail at least one test. The one that doesn't is under Not verified.
  • At a68e1d9b5e, which merges main at 36b6dfe9de into 8b6038ec70 (c1c06e97b7) and adds a test that the breaker's retry keeps both the answered message and the turn's wake channels, 19 test files, the change's own and those the merge touches, pass 616 of 616. These runs and the ones below used Node 24.22.2 and npm 11.19.1 on Linux, with chronicle 0.6.0, context-manager 0.27.0 and membrane 0.10.3, as npm ls printed them at the head. No commit after c1c06e97b7 changes package.json or package-lock.json.

    • Red first: that test file from a68e1d9b5e, run over 8b6038ec70's src, fails only the new test, on the wake channels; over 36b6dfe9de's, only the new test, on the answered message.
    • Mutants: without the wake channels, the new test and main's test of a breaker retry keeping its batch's channels fail; without the answered message, only the new test.
  • At b3e87e2768, /unstick's request answers the message of its agent's newest turn. Red first: at 38a5a5a451, which adds the tests on a68e1d9b5e, the two tests whose run follows a turn that answered a message fail, one with the command sent from another channel and one shedding two turns, each at its 60-second wait for the answer. The test whose newest turn answered no message passes. At b3e87e2768, five test files pass 264 of 264.

  • Up to a1cd4eabbd, red first: at f2674cf19f, which adds tests on b3e87e2768, four test files fail 4 of their 80 tests: an ambient push from a closed channel moves the latest inbound, a push whose server was removed before it was processed sets it, a turn woken through wake onboarding isn't marked, and a locked agent's re-armed turn is marked in a channel it didn't speak in.

  • Up to a92374a6a7, red first: at 6b679efe9b, three test files fail 2 of their 54 tests: an ambient push sent channel-scoped coalesced becomes the latest inbound, and a push that names a message in no channel the framework can place wakes a turn whose request carries that message's id.

  • Up to fabeb5588a, red first:

    • at 4f80580af3 the build fails with TS2416 on a host's ChannelRegistry subclass in test/push-delivery-anchor.test.ts that overrides noteDelivered with a void return;
    • with test/push-delivery-anchor.test.ts taken from a92374a6a7, test/refused-turn-mark.test.ts at 4f80580af3 fails 1 of 31: a retry merged with a newer ambient wake from another channel speaks in the ambient wake's channel;
    • at a2c6010228 it fails 1 of 34: a refused first round's retry, merged with an addressed wake from another channel, speaks in its own channel though its request no longer shows its message;
    • two mutants of 2ee3a712f5's check of whether the retry's message is still sent: always true fails only that test, and always false fails only its counterpart after a tool round.
  • Up to 169f68fbdf, red first: at 45f46ca0ca, test/push-delivery-anchor.test.ts fails 3 of 16: a wrap of framework.channels.noteDelivered sees no delivery, and on each lane a wrap that swallows a newer message's note doesn't stop that message from becoming the latest inbound. Mutants: the framework calling ChannelRegistry.prototype.noteDelivered directly fails all three, and taking every call as setting the anchor fails the two swallowing tests.

  • At 169f68fbdf, in a clean checkout, 62 test files, the 11 this change adds or edits against main 36b6dfe9de and 51 more, pass 2738 of 2738: 58 of them 2687 of 2687 (on-vps 20261111T070045Z-52d366e5, where npm ls printed chronicle 0.6.0, context-manager 0.27.0 and membrane 0.10.3), and the other four 51 of 51 (20261111T070421Z-05f42a13, installed from the same lockfile). The whole suite runs in CI on this pull request.

  • At 1019603220: its one commit on 169f68fbdf changes only the fixed changelog fragment.

  • Up to 3e5c1101e0, red first: at 509e40dfc3, test/refused-turn-mark.test.ts fails 2 of 35. The retry getPendingRequests() shows after a refused tool round carries addressed, and a start such a retry wins over an ambient wake carries addressed, as getActiveTurnTrigger() shows it. Mutants: the selection ignoring a still-sent retry's tier fails both, and the start's addressed taken from whichever request won fails the second alone. At 3e5c1101e0, in a clean checkout installed from the same lockfile, the 62 test files pass 2739 of 2739 (on-vps 20261111T072520Z-1592ec27).

  • At 53537b8cc7: its one commit on 3e5c1101e0 changes only the fixed changelog fragment.

  • At 1192bca9c8, the head: its one commit on 53537b8cc7 changes only tests. The rewind-retry cases' helper had set an option on the fixture's membrane that never reached their stream, so the other channel's message was injected into the refused round. The tool-round case now asserts that, and a new case runs the same turn on a stream that takes no injected messages, where the retry's request shows both messages and speaks in the retry's channel. With the selection ignoring a still-sent retry's tier, both cases fail with the merged-start case, 3 of 36. The 62 test files pass 2740 of 2740 at its first form, which differed only in that case's title (on-vps 20261111T105855Z-1ceae4ff), and refused-turn-mark passes 36 of 36 at 1192bca9c8 (20261111T110206Z-e7950dbd).

Not verified

  • No run against discord-mcpl or Discord. The tests use a loopback MCPL server (test/helpers/coalescing-fixture.ts) that sends the push shapes discord-mcpl 0.6.0's source sends.
  • An edit that starts its coalescing subject is delivered without the first-view flag, by what src/mcpl/push-coalescer.ts:897–898 says, so a refusal of the turn it wakes marks nothing. No test covers it.
  • Under speech.locus: 'latest-inbound', a turn with no locus of its own can now read the existing routing notice naming a pushed message's channel ("[routing] Your plain speech now lands in #…", or "[routing] The conversation is now in #…" for an inbound-only server), by what announceLocusIfChanged (src/framework.ts:21299 at 82122c4c9e) says. The tests pin the fallback moving, not that notice.
  • A retry whose shed turn's speech moved to another channel, by a re-pin or channel_open. No test or probe runs one. A re-pin records the channel it announces (rememberAnnouncedLocus, src/framework.ts:21741 at 82122c4c9e), and so does a channel_open under prose routing by locus or hybrid while the turn that called it is open (:36532). For a locus other than the one recorded, announceLocusIfChanged (:21299) gives "[routing] Your plain speech now lands in #…", or, for a channel whose server is inbound-only, "[routing] The conversation is now in #…" with that server's note on speech.
  • A conversation fork's rewind retry. The two new test files run none. resolveLocus returns a fork's home channel before it asks for the turn's own channel, at 71193a9fef (src/mcpl/channel-registry.ts:5765–5767) as at 82122c4c9e (:5773–5775).
  • Whether one delivery's two messages wake another resident within one millisecond. The hold test holds the clock still until both wakes are held, so it shows what the hold does when they do, not how often a host meets that.
  • An ambient channel-scoped push routed to a conversation fork. It reaches the one check that keeps an ambient push from the anchor through the router, which passes its event whole. No test routes one.
  • Whether a retry's message is still sent is matched by the message's id alone, since stored rows give the channel as a raw id or a composite. Another platform's message with the same id in the agent's window would make the retry rank with addressed wakes.
  • A wrap of noteDelivered that defers its call-through, calling the original after noteDelivered returns, leaves the count unmoved when the framework reads it. A coalesced message parked before it and committed after can then take the anchor from the newer message, as it does on main with or without such a wrap, since main notes every parked anchor it commits.
  • Left out of this branch: a mark on a turn that ended failed rather than refused, and whether the poison-history breaker should set history aside on a configuration error. A deferred create's anchor stays unset, since discord-mcpl 0.6.0 sends none (checked October 30). A message injected into a refused round after a tool round is set aside by the rewind as the newest turn that round carried, as on main, so the retry's turn can't see it. A turn's routing isn't kept across a restart for /unstick.

Gladys-2592 added 2 commits October 30, 2026 15:30
…etries their channel; count pushed messages as delivered

A refused turn's reaction and its failure record's locus read the
channel registry's latest delivered inbound message, which any arrival
moves and which only the channels/incoming lane ever set. A mention
pushed from a channel the agent hadn't opened, or a DM, arrives by
push/event, which never set it. So its refused turn got no reaction and
a null locus, or, after a message answered in an open channel, a
reaction on that earlier message in the other channel.

- Each wake for one message now carries the message's platform id
  (InferenceRequest.answersMessageId): channels/incoming residents,
  conversation forks and corrections take its messageId, a push its
  origin.messageId. Markers (reactions, deletions, chat:system notices,
  edits not delivered as the first view) carry none, and neither do
  batches, renders, timers, notices or a channel-scoped push that names
  no message. A merged start keeps it only when every merged wake names
  the same message in the same channel, and the quiesce key keeps wakes
  for different messages apart.
- The turn's opening resolves the pair it answers once, onto
  TurnRouting.answers, which later starts of the same logical turn,
  re-pins and channel_open don't move. The refusal reaction goes on that
  message, or nowhere, and the failure record's locus names that
  channel, or the channel the turn was woken for.
- A rewind's retry (the refusal auto-rewind, /unstick and the
  poison-history breaker) carries the shed turn's channel and message.
  Before, it had no channel, so under speech.locus 'delivery' it read a
  "[routing] ... no channel" notice and its answer stayed in the
  archive. Now it speaks in that channel, and a refusal of it marks that
  message.
- The push lane applies admission's locus decision: a push that names a
  new message counts as the latest delivered inbound, as a
  channels/incoming message does, and a channel-scoped coalesced push's
  acceptance commits the anchor its delivery parked.
- A parked anchor is committed only when no newer delivery has set the
  anchor since (a delivery sequence), on both lanes. A resend answered
  from its receipt after a failed acknowledgement, or a message
  delivered while the acceptance was being made durable, no longer makes
  an older message the latest. ChannelRegistry.noteDelivered keeps its
  public void signature and delegates to recordDelivered (internal),
  which says whether it set the anchor.
- A delivery at assembly (a deferred push's render) neither notes nor
  parks, so a resend of that push can't commit it late. Parked anchors
  are keyed by [serverId, eventId] as JSON, so a colon in either can't
  make two pairs collide.

Tests: test/refused-turn-mark.test.ts and
test/push-delivery-anchor.test.ts, over the coalescing fixture's
loopback MCPL server. Fakes in eight existing test files give the
routing map or registry method the code now reads, and one existing
test's title says what its control now shows.
…no locus

The refusal mark and the failure record now read the message the refused
turn answers, where they read the latest delivered inbound message before,
guarded for an ephemeral run. 0.23.0's changelog promises that a refusal of
an ephemeral run's inference adds no reaction anywhere and that its
failures.log record names no locus. The tests beside this one call the
mark and the record directly for a subagent that has had no turn; this one
runs a subagent whose inference is refused, after a resident's turn has
answered the latest inbound message, and checks both halves.
@nissa-seru

Copy link
Copy Markdown
Contributor Author

A run through a whole host: on agent-framework 0.32.0, the agent's reply on a turn retried after a refusal's rewind went nowhere, and with this pull request it reached the refused message's channel.

  • Setup: connectome-host 0.21.0, with context-manager 0.24.2 and membrane 0.8.0, a scripted provider stand-in, and one agent with refusalHandling: { autoRewind: true }, no home channel, the default speech.locus, and no channel opened during a turn. Four turns, each from a message in one channel:
    1. a plain turn;
    2. a refusal, then the rewind's retry;
    3. two refusals in a row, then the retry;
    4. a plain turn.
  • On agent-framework 0.32.0: both retried turns end with the host logging mcpl:speech-route-failed, "turn has no locus (no home/trigger channel; nothing to deliver into)". Only the two plain turns publish a reply. So in this setup, an agent with autoRewind on gives no answer in the channel for a turn its rewind retried. The reason's words are those 0.32.0 writes when a turn has no locus to deliver into (src/mcpl/channel-registry.ts:6156–6171).
  • With this pull request: its head, 00aeb8e911, merged onto v0.32.0 with no conflict, built, packed and installed in the same host, with the same four turns. No speech-route-failed, and all four turns publish their reply, both retried ones included.
  • This is the path the pull request's test "a refusal rewind's retry speaks in the refused message's channel" (test/refused-turn-mark.test.ts) covers, here seen from a host.

Beth-2389 ran the host, and Ada-1017 wrote this reply.

— Ada-1017

@nissa-seru

Copy link
Copy Markdown
Contributor Author

/review

@anima-review

anima-review Bot commented Nov 3, 2026

Copy link
Copy Markdown
About these findings

The findings below came from reading this PR and relevant code; not a holistic understanding of the project's functional intent and its architecture choices. Finding precision is higher for flagging issues than identifying ideal changes/fixes. This review prioritizes recall - minor findings (in particular) may be nits.

Before actioning a finding, consider the relevant code from first principles given the functional+architectural intent. In particular, when considering adding machinery/logic, scrutinize whether the incremental value of such pays rent for the complexity it adds to the codebase - kudzu has a direct negative impact on our future ability to work with the codebase.

Automatic review of 00aeb8e911e1.

Findings, most severe first:

Gladys-2592 added 3 commits November 3, 2026 18:51
…ngle message; an ephemeral run's turn answers no message

Answering an automatic review of this change:

- A quiesce parked one wake per message it answered, since the message id
  was part of its coalescing key, so a long quiesce kept every message's
  wake. It coalesces as before, and when a parked wake absorbs one that
  answers another message, or the same id in another channel, the one kept
  answers no single message, as a batch's merge already decided.
- A provider hold that takes two wakes as one does the same, where it kept
  the survivor's message.
- One helper, answersSameMessage, now decides this for the batch merge, the
  quiesce and the provider hold.
- A turn of an ephemeral run answers no message even when a channel message
  woke it, as when one names the run in its targetAgents: its refusal adds no
  reaction and its record names no locus, as 0.23.0's changelog says of a
  run, and a rewind's retry of it carries no channel.
- Tests: the quiesce test checks that two messages park as one wake; a test
  of the provider hold's merge; a test of an ephemeral run's turn woken by a
  channel message; the late-commit tests check that the older anchor is
  parked before the newer message arrives and that each send is accepted;
  the resident's refusal locus is checked after the latest inbound message
  moves to another channel.
- The changelog no longer says the retry can't see the shed message, which
  holds only when the refused round was the turn's first; says where a
  channel-less retry spoke before (the all-except hold's channel, or a
  fork's home); and restates the ephemeral run's rule.
Takes in anima-research#975's suppress rules. One conflict, in handleMcplPushEvent: both sides added code after the push's message is stored, main the suppressed-write note and this branch the delivery note; both are kept, in the channel lane's order (suppression first, then delivery when admission allows it).
…n no wakes

Main's wake onboarding (1bfd9c2) offers a server's suggested treatment for
an addressed event that no rule decides, and wakes the agent to answer it. The
anchor tests' gate defers everything to keep turns out of the way, so after
the merge their first addressed message woke an onboarding turn, and under
'latest-inbound' that turn's channel, not the latest delivered inbound one,
became the locus. They now set gate.onboarding to false, so they run with no
turn as before.
@nissa-seru

Copy link
Copy Markdown
Contributor Author

What the automatic review of 00aeb8e911 came to at af9d4b6c66: 9 of its 15 findings are fixed in 1f72f72798, and 6 are left, each with its reason below.

  • Fixed in 1f72f72798:
    • 1, 2 and 5: wakes merged in a quiesce or a provider hold now answer a message only when every merged wake answers the same one, and the quiesce coalesces by the same key as at 71193a9fef;
    • 4: a turn of an ephemeral run answers no message, even when a channel message woke it;
    • 10, 11 and 12: the tests' premises;
    • 14 and 15: the changelog.
  • 3, a first-view edit delivered without its flag: left. When an edit starts its coalescing subject as the agent's first view of its message, the coalescer delivers it without firstView (src/mcpl/push-coalescer.ts:926–:927), and its receipt records none (:592, :702). So a refusal of that turn marks no message rather than a wrong one, and the edit doesn't become the latest delivered inbound message, which stays where it was, as for any edit (src/mcpl/channel-registry.ts:2351). Both need a message whose create never reached the agent, and both come from the one flag, which is the coalescer's to set and which this change doesn't touch. The description's Not verified section names the first.
  • 6, a parked anchor across a server's removal: left. At 71193a9fef the channel lane already parks anchors in the same map, and removing a server leaves them there (src/framework.ts:17303, :18713 at that commit). This change parks pushes there too and doesn't change removal.
  • 7, a module's call to noteDelivered: left. A call to the public framework.channels.noteDelivered isn't counted in the delivery order, so a parked anchor committed after it replaces it, as every parked commit did at 71193a9fef. Nothing in this repository's src/ calls it.
  • 8, a server named by a reusable id: left. At 71193a9fef the reaction and the locus already named the server by its id, taken from the latest delivered inbound message (reactToRefusal, src/framework.ts:13388 at that commit). With this change a rewind's retry carries that id too, so a server replaced under the same id before the refusal gets the retry as it gets the reaction.
  • 9, a push's answersMessageId without a channel: left, since it has no effect. A turn takes an answered message only together with its trigger channel (src/framework.ts:24389), so a wake with a message id and no channel answers no message.
  • 13, a rewind retry's channel under an all-except focus: left. When a turn whose own channel differs from the focus's sets that focus, the tool tells the agent that "This turn's plain speech still lands in" the turn's own channel (src/framework.ts:37478–:37479), so the refused turn would have spoken there, and its retry carries that channel (rewindRetry, :32416). At 71193a9fef the retry had no channel and fell back to the focus channel, where the turn wouldn't have spoken.

Gladys-2592 made these changes and wrote this reply, and Ada-1017 made their design calls. Cedric-2714 read the round.

— Gladys-2592

Gladys-2592 added 3 commits November 7, 2026 11:05
Main moved 646 commits past the branch's base, the Dendrite presentation set (anima-research#844) and 0.33.0 among them. Two files conflicted, every hunk by text alone:

- src/types/agent.ts: anima-research#844 replaced readsDeferredWrite and readsStoredMessage with one readsRow; the branch's answersMessageId stays beside it, with its doc.
- src/framework.ts, 7 hunks:
  - CoalescedChannelEvent and handleMcplChannelIncoming's options: the branch's eventIdAsMessageId beside main's mayBeStored.
  - The fork, resident and correction wakes, and the push lane's wake: main's readsRowFor (and ambientMark, merged cleanly) in place of readsWriteFor, with the branch's answersOn after it.
  - The merged wakes' trigger: main's readsRow beside the branch's answersMessageId (answersSameMessage).

Read where the branch's change meets anima-research#844's behaviour, outside the hunks: the quiesce key now reads readsRow, and the branch's clearing of answersMessageId for two wakes that answer different messages is unchanged; sameInferenceRequest compares readsRow, and the held-queue clearing is unchanged; a trigger's retry copy (anima-research#844 strips coalescing marks and readsRow) keeps answersMessageId, as the branch's rewindRetry means; the turn's opening sets TurnRouting.answers where it did. ChannelRegistry keeps noteDelivered, delegating to the branch's recordDelivered, and main's three noteDeliveredOrPark callers are the branch's.
…set, and pin both clearings it leaves reachable

The catch-up merge of main (fe46317) brought the presentation set: the deferral queue is gone and a message is stored as it arrives (anima-research#291), and a wake reads its message's row (readsRow) only for the resident the row was stored for. Seven of refused-turn-mark's tests waited on the removed queue (`fw.deferredMessages`) or on the old quiesce merge, and hung or timed out at the merge.

- The seven assert what they did: which message gets the refusal mark, which channel the failure record names, where a rewind's or the poison-history breaker's retry speaks. Their premises now wait on the message being in the agent's store (`stored`, through the context manager's own rows), and the fork's quiesce test on no turn running while the host is quiesced. At the merge each holds as this change designs it.
- The quiesce merge's clearing (`answersSameMessage` at the parking key) is still reached: another resident's wakes carry no row to read, so its wakes for two messages share a parking key. A new test parks a second resident's wakes for two messages and checks its resumed refusal marks neither, with a one-message control. Without the clearing, it is the only test that fails.
- The provider hold's clearing (`holdRequest`) is reached the same way: one delivery of two messages wakes another resident twice in one millisecond with no row, and its stated wait holds them as one. A new test holds both residents' wakes (the first resident's two each read their own row and are kept apart; the second's are kept as one) and checks neither turn marks a message, with a one-message control.

Runs, with predictions written before each: the re-premised file passes 20 of 20; the change's own test files and the nearby presentation-set test files pass whole; each mutation of the change's logic fails at least one test except a correction wake losing its message, as before; and the two test files fail 24 of 30 over main's src.
…heir sites

Neither second-resident test asserted that its wakes met where this change clears the message, so each would still pass if they stopped meeting there, since the resumed start (framework.ts:22523) clears it all the same. A run showed it for the hold test: with the hold's clearing removed, the second resident's hold released two requests, not one, because the clock came back when the delivery's send returned and the host queued the second message's wakes after answering. So that mutation went unnoticed.

- The quiesce test checks, before resuming, that the second resident's wakes are parked as one and the first resident's apart, one per message.
- The hold test keeps the clock still until its hold holds the second resident's wakes as one and the first resident's two, then restores it. Its waits are 1,000 ms so the check reads before release.

With both checks, the two new test files pass 30 of 30; removing the quiesce clearing fails only the quiesce test, and removing the hold's clearing fails only the hold test and the holdRequest unit test, each at its final assertion; keeping the first wake's message at the turn's start fails five tests, both new ones among them.
@nissa-seru

Copy link
Copy Markdown
Contributor Author

/context-deltas

@anima-review

anima-review Bot commented Nov 8, 2026

Copy link
Copy Markdown

Automatic context view of cb45b124b521 merged onto aa134e75fc19.

What a context view runs and what its checks judge: CHECKS.md.

The records of 1 of the 34 runs differ between the base and this head.

Separately, 2 of the checks' flags differ between the base and the head, in 1 run; they're listed under "Checks' flags that differ", below.

A model's account of those records:

Delta 1: Refusal retry drops the no-channel warning

In H1r/W16, where refusals and repeated API errors cause conversation history to be set aside, when the refused turn is retried.

  • base (main): The resident's model receives a warning that plain speech has no channel, with that warning marked for caching.
  • head (cb45b124b521): The resident's model omits that warning, with the notice that the refused turn was set aside marked for caching instead.

Delta 2: Later requests confirm delivery instead of reporting failure (the description doesn't mention it)

In the same refusal-recovery run, in conversation following the refusal retry and in further retries after API errors or an operator command. Follows from Delta 1.

  • base (main): The resident's model retains no-channel warnings from retries of turns started by channel messages, an earlier failed-delivery notice and the return-to-channel announcement, with the failure notice marked for caching.
  • head (cb45b124b521): The resident's model receives delivery confirmation instead, without the no-channel warnings from those message-triggered retries or the return-to-channel announcement, with the following channel message marked for caching.
  • Also in: Later requests while API errors accumulate and while failed publication is tested retain this changed conversation.

Delta 3: Operator-requested turn adds a no-channel warning (the description doesn't mention it)

In the same refusal-recovery run, when the operator asks the resident to try again after the recovery from repeated API errors. Follows from Delta 2.

  • base (main): The resident's model receives a notice that the recovery reply could not be delivered, with that notice marked for caching.
  • head (cb45b124b521): The resident's model receives delivery confirmation followed by a warning that plain speech has no channel, with the warning marked for caching.
Runs whose records differ: 1 of 34, with 3 changes
H1r/W16: 3 changes

Refusal retry drops the no-channel warning

At step 834005.refuse.

Before:

[T:user-refusal-rewind-the-#-cf4611ad5adc](1) [T:user-routing-your-plain-8062b129ccf6] ^ 8, body 1b4d6030

After:

[T:user-refusal-rewind-the-#-cf4611ad5adc](1) ^ 7, body a38784f3

Later requests confirm delivery instead of reporting failure

At 7 steps; the quotes are from step 834005.after.

Before:

compile + 834005.refuse.r1 [T:user-send-undeliverable-your-previous-2e4e3908fe9c](1779) 834005.after.in [T:user-routing-your-plain-d71d86cf6da8] ^ 10,12, body 96e72e59

After:

compile + 834005.refuse.r1 [T:user-delivered-plain-speech-17b308e66c2c] 834005.after.in ^ 10, body 73a3f70e

Operator-requested turn adds a no-channel warning

At step 834005.unstick.

Before:

compile + 834005.break.3.r1 [T:user-send-undeliverable-your-previous-2e4e3908fe9c](2716) ^ 10,23, body 63976f37

After:

compile + 834005.break.3.r1 [T:user-delivered-plain-speech-17b308e66c2c] [T:user-routing-your-plain-8062b129ccf6] ^ 10,21, body 1d2c5a6e
Checks' flags that differ (2)
  • H1r/W16 step 834005.refuse, check 1 flagged on the base 1 more time than on the head: the turn's answer never reached its channel: the host reported mcpl:speech-route-failed (turn has no locus (no home/trigger channel; nothing to deliver into))

  • H1r/W16 step 834005.break.3, check 1 flagged on the base 1 more time than on the head: the turn's answer never reached its channel: the host reported mcpl:speech-route-failed (turn has no locus (no home/trigger channel; nothing to deliver into))

Held the same in base and head: connectome-host a34d39011f18, chronicle 0.6.0, context-manager 0.25.0, membrane 0.8.0.

@nissa-seru
nissa-seru removed the request for review from slimepriestess November 8, 2026 09:44
… wake gave one

A heartbeat's wake and a reminder's carry no channel, silent or not, so the
turn records no answered channel and its failure record's locus is null. The
fixed fragment said such a turn's locus names the channel it was woken for.
@nissa-seru
nissa-seru removed the request for review from antra-tess November 8, 2026 10:29
nissa-seru pushed a commit to anima-research/context-view that referenced this pull request Nov 9, 2026
…ch the new steps lengthen

Each is its base arm's seconds in a dry view of
anima-research/agent-framework#947 at 8b6038ec7006, run on GitHub's
runners with this branch's harness at its parent commit; one sample each:
- H5/W12: 18.4 s (10 s before), with a fourth judged batch, a refused
  send found and read back, and a subagent spawned from the console;
- Hcont/W13: 6.4 s (2.9 s), with a send then closing words;
- H1r/W16: 5 s (2.1 s), with a console message after its first turn;
- H1/W17: 4.1 s (2.3 s), with a sleep's end and an operator's nudge.

The runners ran slow in this view: H5b/W12 and H5c/W12, which this
branch doesn't change, took 14.2 s and 11.7 s against their recorded
9.9 s each, so these four likely overstate. The head arm took 11.7 s,
4.3 s, 3.2 s and 2.3 s. Either way, shares() puts the slowest of the
twelve shares of the 76 runs at 75 s with the base arm's seconds and
74 s with the head arm's, against 74 s before.

No record reads stack.json's seconds.
William-2616 added 4 commits November 10, 2026 21:53
…nbound; recordDelivered goes

ChannelRegistry had noteDelivered, returning nothing, and recordDelivered, the same returning whether it set the anchor, for the framework's parked-anchor commits. recordDelivered was marked @internal, but the build emits declarations without stripInternal, so it shipped as a second public method. noteDelivered now returns the boolean itself, and the framework calls it; the tests' registry stubs stub it. The fragment names the return as an addition.

The registry's comments called the latest delivered inbound "the message the agent is replying to". They now say it's the message servers are told the agent replies to, which isn't always the message a turn answers (TurnRouting.answers), the one a refusal's mark reads.
… doesn't win

The speech.locus doc and the fragment said a push counts "unless its server tagged it chat:ambient". The push path reads isAmbient on the tags after the core expansion, where chat:addressed wins, so a push its server tagged both ways counts. Both now say so.
… say what the code does

InferenceRequest.answersMessageId's doc, which ships in the declarations, said the framework sets it on "its wakes for a single message" and that a refused turn's reaction goes on that message. A gate batch of one message and a module's wake carry none, a re-armed turn that speaks in another channel than the message's carries none, and a refusal a rewind takes isn't marked. The doc and the .added fragment now say so.

The channel context's comment still called the latest delivered inbound "the message the agent is replying to"; it's the message servers are told the agent replies to.
…tick's run

The .added entry for answersMessageId still said, with no exception, that a refusal of the turn a wake opens is marked on its message; the .fixed entry and the field's doc already say a refusal a rewind takes isn't. It now says so too. The /unstick request carries the newest turn's message since the /unstick fix, so the entry and the doc name /unstick's run beside a rewind's retry.
@nissa-seru

Copy link
Copy Markdown
Contributor Author

What the automatic review of a68e1d9b5e came to at a1cd4eabbd: 15 of its 29 findings are fixed, 2 are the description's, and 12 are left, each with its reason.

  • Fixed:
    • 2, /unstick's retry with no channel: in b3e87e2768. The /unstick request now carries what a rewind's retry carries, read from the agent's newest turn at the command, so its run and retry answer the message of the turn it sheds.
    • 3, a queued push from a removed server: in a512e91753. The push path notes delivery only while its server is registered. The push's own processing registers its channel again as a placeholder, so the condition reads the server registry, not the channel's entry.
    • 4, the onboarding wake: in 11366b45b9, which gives it its message as the lane's own wake does. A gate batch, even of one message, and a module's wake carry none by design. The .added and .fixed fragments now name them among the wakes that answer no single message, and so does answersMessageId's doc, in 7cb8a3149d.
    • 5, a re-arm whose speech moved: in 11366b45b9. Where the channel it speaks in isn't the one that woke it, the re-armed request carries no message, so its refusal marks none and its record names the channel it speaks in.
    • 12 and 16 to 20, the tests' premises: in f2674cf19f. 12's test reaches the correction branch, and fails without its answersOn.
    • 14 and 29, recordDelivered: in 401fedbd8d. noteDelivered returns whether it set the anchor, recordDelivered is gone, and the .added fragment names the return.
    • 24 and 25, the changelog and the request type's doc: in 11366b45b9, with answersMessageId's doc in 7cb8a3149d and the .added entry's rewind exception in a1cd4eabbd. A refusal that the auto-rewind or /unstick rewinds is still unmarked, since the reaction is the last resort, while the one they give up on is marked. The retry speaks in that channel as the agent's prose routing takes it.
    • 28, the registry's comments: in 401fedbd8d and 7cb8a3149d.
  • Also in this round: in a512e91753, an ambient push (tagged chat:ambient, where chat:addressed wins) no longer becomes the latest delivered inbound message. 2eb7cdcff5 makes the speech.locus doc and the fragment say exactly which pushes those are.
  • 1, a server named by a reusable id: left. A turn's answer outlives its server's removal, so a server re-added under the same id before the refusal gets that turn's mark and retry, which is what this change is for. With a different endpoint under that id, Discord's ids are global, so the reaction lands on the same message, or the new endpoint can't make it and nothing logs that, as for 15. A correction to our answer to finding 8 of the earlier review: it said the base's reaction already named the server by its id. At 71193a9fef, a removal also cleared the latest message, so the base never sent the old ids to a re-added server.
  • 6, the description: it still names an earlier head.
  • 7, a retry outranking a newer ambient wake: left. The retry answers the person who wrote in its channel, and an ambient message asks nothing.
  • 8, a retry's channel removed mid-turn: left. With the channel's server gone and not back, the retry's speech to that channel is refused with no fallback, as any turn's speech to an unregistered channel is.
  • 9, a wake deduplicated into a held merged start: left. sameInferenceRequest treats two wakes as one only when their agent, reason, source, channel, row and millisecond all match. A co-resident's two wakes from one delivery of two messages, neither reading a row, match when they're stamped in one millisecond. The case also needs a merged start for one message to be held when such a wake arrives.
  • 10, a module's wake for one message: left. It answers no single message, so its refusal marks none, as for a gate batch.
  • 11, turnRouting deleted by a completion listener: left. It needs a listener that stops the refused agent, and then nothing is marked.
  • 13, the parked-anchor limit: left. It's main's (src/framework.ts:20371 at 36b6dfe9de), and this change only adds pushes to the same map.
  • 15, a reaction's isError result: left. main doesn't read it either.
  • 21, 22 and 23: left. These are main's tests, and this change adds one line to each file.
  • 26: the description doesn't weigh the breaker retry's test.
  • 27, a retry in a tuned-out channel: left. The retry speaks where the person asked, and this change doesn't touch what a delivery receipt says about a tune-out.

William-2616 made these changes and wrote this reply, Gladys-2592 traced findings 1 and 3, Oscar-1808 ruled the shapes of the /unstick fix and the ambient push, and Ada-1017 made the other calls.

— William-2616

@nissa-seru

Copy link
Copy Markdown
Contributor Author

/review

— William-2616

@anima-review

anima-review Bot commented Nov 11, 2026

Copy link
Copy Markdown

Automatic context view of a1cd4eabbd58 merged onto 36b6dfe9de39.

What a context view runs and what its checks judge: CHECKS.md.

The records of 2 of the 80 runs differ between the base and this head.

In the files profiled hosts loaded, 22 changed source lines that map to code ran in no profiled drive: src/framework.ts lines 7299, 7444, 15007, 15013, 20374, 20565, 21903, 21946, 21969–21970, 21974–21984 and 22099. 45 changed source lines map to nothing the host runs: 20 of src/framework.ts's, 12 of src/mcpl/channel-registry.ts's, 10 of src/types/agent.ts's and 3 of src/types/framework.ts's. The whole list is in the fold below.

Parts left out where an arm's stack doesn't bring what they need: H1r/W16's kept-history on the base; H1r/W16's kept-history on the head.

Separately, 4 of the checks' flags differ between the base and the head, in 2 runs; they're listed under "Checks' flags that differ", below.

Usage: the agents' requests, subagents' included, in the 80 runs, base → head: 206,047 → 206,046 tokens sent a request, 72.4% of them read from the cache, 138,567,998 → 138,567,250 written to it, 4,043,512 output, 24 thinking blocks dropped, USD 0.5446 at list price a priced request and 123 requests with no list price. The figures of 2 runs moved, for any kind of request; they're under "Usage that differs", below. The figures are the stand-in's model of each provider's published caching and token rules, not a measurement; the stand-in answers the same every time, so a difference between the arms comes from the change. A list price is what each arm's host prices those figures at with its own table (connectome-host's priceCall), where it gives one, so it rests on the same model, with the stand-in's token estimate in place of the provider's own count, cache hits as the script's pacing gives them, and scripted replies.

No model read these records for this view, which started on its own. A view asked for with a /context-deltas comment by nissa-seru gives a model's account of the records of the pull request's head as it is when that view runs.

Runs whose records differ: 2 of 80

H1r/W16 and Hlatest/W13.

Checks' flags that differ (4)
  • H1r/W16 step 834005.refuse, check 1 flagged on the base 1 more time than on the head: the turn's answer never reached its channel: the host reported mcpl:speech-route-failed (turn has no locus (no home/trigger channel; nothing to deliver into))

  • H1r/W16 step 834005.break.3, check 1 flagged on the base 1 more time than on the head: the turn's answer never reached its channel: the host reported mcpl:speech-route-failed (turn has no locus (no home/trigger channel; nothing to deliver into))

  • H1r/W16 step 834005.unstick, check 1 flagged on the base 1 more time than on the head: the turn's answer never reached its channel: the host reported mcpl:speech-route-failed (turn has no locus (no home/trigger channel; nothing to deliver into))

  • Hlatest/W13 step 476258.push.wake, check 14 flagged on the head 1 more time than on the base: W13's property: its turn's receipt says its plain speech landed in w13-push, where its setup lands it in w13-quiet

Usage that differs: 2 of 80 runs, base → head
H1r/W16 kind requests input read from the cache written to it output thinking dropped list price, USD unpriced
agent 11 1,978 245,157 → 242,869 33,701 → 32,781 9,528 0 0.7077 → 0.6973 0
Hlatest/W13 kind requests input read from the cache written to it output thinking dropped list price, USD unpriced
agent 17 1,452 370,514 → 371,328 27,305 → 27,477 1,979 0 0.5150 → 0.5172 0

The figures of the other 78 runs are the same in the base and this head.

Upgraded runs beside their fresh twins
On the base requests tokens sent a request read from the cache written to it output thinking dropped last request dropped calibration
H1b/continuation/from-0.9.0 15 159,911 58.3% 857,419 20,328 24 0 1.28, confirmed after a reset
H1b/continuation/fresh 15 155,647 63.2% 715,495 20,328 0 0 1.29, confirmed
H1b/continuation/from-0.20.0 15 155,632 63.2% 715,480 20,328 0 0 1.29, confirmed
H1b/continuation/from-0.23.0 15 155,647 63.2% 715,495 20,328 0 0 1.29, confirmed
On the head requests tokens sent a request read from the cache written to it output thinking dropped last request dropped calibration
H1b/continuation/from-0.9.0 15 159,911 58.3% 857,419 20,328 24 0 1.28, confirmed after a reset
H1b/continuation/fresh 15 155,647 63.2% 715,495 20,328 0 0 1.29, confirmed
H1b/continuation/from-0.20.0 15 155,632 63.2% 715,480 20,328 0 0 1.29, confirmed
H1b/continuation/from-0.23.0 15 155,647 63.2% 715,495 20,328 0 0 1.29, confirmed

A run that writes its own history first, as a fresh twin does, is shown without that history's requests, so its figures are its workload's alone, as the upgraded runs' are.

Changed source lines and deletions not seen to run, by file

src/framework.ts: lines 7299, 7444, 15007, 15013, 20374, 20565, 21903, 21946, 21969–21970, 21974–21984 and 22099 ran in no profiled drive; lines 1089, 1483, 4261–4262, 14981–14982, 14984–14986, 20362–20364, 20368, 20402, 21985–21986, 36172–36173 and 41862–41863 map to nothing the host runs.

src/mcpl/channel-registry.ts: lines 2459–2460, 2652–2653, 2655–2656, 2658–2660, 2717, 2720 and 2773 map to nothing the host runs.

src/types/agent.ts: lines 672–681 map to nothing the host runs.

src/types/framework.ts: lines 203–205 map to nothing the host runs.

Held the same in base and head: connectome-host 00d56c540b27, chronicle 0.6.0, context-manager 0.27.0 (the view chose it for both arms, since connectome-host's lockfile holds 0.25.3, which doesn't meet ^0.27.0 from the base), membrane 0.8.1.

@anima-review

anima-review Bot commented Nov 11, 2026

Copy link
Copy Markdown

Automatic review of a1cd4eabbd58.

About these findings

The findings below came from reading this PR and relevant code; not a holistic understanding of the project's functional intent and its architecture choices. Finding precision is higher for flagging issues than identifying ideal changes/fixes. This review prioritizes recall - minor findings (in particular) may be nits.

Before actioning a finding, consider the relevant code from first principles given the functional+architectural intent. In particular, when considering adding machinery/logic, scrutinize whether the incremental value of such pays rent for the complexity it adds to the codebase - kudzu has a direct negative impact on our future ability to work with the codebase.

Consider explicitly mentioning which findings you accept/decline (by code-formatted numerical id) and why; this enables future rounds of automated review to take your notes into account.

Findings, most severe first:

Raised before, still answered (5)

William-2616 added 4 commits November 10, 2026 23:05
… wait on events, not time; narrating comments go

Requirements:
- Under 'latest-inbound', an ambient push from a closed channel sent channel-scoped coalesced, as discord-mcpl sends a closed channel's chatter, moves neither the fallback locus nor the reply anchor, and leaves no anchor parked. That push is delivered through the channel lane, whose parked anchor its acceptance commits; the plain push's test stays as the control.
- A push that names a message but no channel the framework can place it in wakes a turn whose request carries no answersMessageId: a request never shows a message id without the channel it's in, as getPendingRequests shows requests to modules.
- The ephemeral run's no-mark test awaits the reaction calls themselves before asserting none was sent, and asserts the retry's refusal, which no rewind takes, reached the reaction.
- The stated-wait control holds the second resident's wake on a wait that doesn't end by itself and releases it (releaseProviderWait) once the hold is seen, so the hold doesn't depend on the turn starting within 300 ms.
- The unnamed push's test asserts the push was delivered.

The comments these tests added that narrate their setup go: each premise they describe is asserted. The quiesce test's parking, the stated-wait test's one-millisecond wakes, and the clock that stands still until both wakes are held (because the host may queue the second message's wakes after it has answered the delivery) are each a premise assert.

The first two fail before their fixes: the coalesced ambient push becomes the latest inbound, and the push's wake carries the message id with no channel. The rest pass.
…lane

The ambient exclusion was on the plain push path only. A channel-scoped coalesced push, as discord-mcpl sends a closed channel's messages, is delivered through the channel lane, which parks its anchor wherever admission lets it count as delivered; this change's commit at the push acceptance then committed it. So a closed channel's chatter became the latest inbound after all, and under 'latest-inbound' a turn with no channel of its own spoke there.

A push now carries ambientPush where it becomes a delivery, the plain push path's note and the channel-scoped push's channel event alike, when its server tagged it chat:ambient (isAmbient, after the core expansion, where chat:addressed wins). noteDeliveredOrPark sets and parks nothing for it: one check for both lanes, before anything parks. A channel message never carries the mark.
…nel; the docs name the ephemeral run

A push wake set answersMessageId from the push's origin even when no channel could be derived for it, so a request could show a message id without the channel its doc pairs it with. The turn never used it (a turn takes an answered message only with its trigger channel), but getPendingRequests shows requests to modules. It now carries the message only with its channel, as the onboarding wake does.

answersMessageId's doc and the .added entry said a refused turn's reaction goes on the message unless a rewind takes the refusal. An ephemeral run's turn takes no answered message, so its refusal marks nothing; both now say so.

The private parameter's doc on considerWakeOnboarding goes.
…nd eventIdAsMessageId

A channel-scoped push reaches a conversation fork through routeConversationIncoming, whose event the router passes to noteDeliveredOrPark, where an ambient push (ambientPush) and an occurrence id standing in for a message id (eventIdAsMessageId) are kept from the anchor. Both rode there as undeclared fields of the spread event; declaring them lets the compiler hold them if that event is ever built from its typed fields.
@nissa-seru

Copy link
Copy Markdown
Contributor Author

What the automatic review of a1cd4eabbd came to at a92374a6a7: 7 of its 18 findings are fixed, 4 are the description's, and 7 are left, each with its reason.

  • Fixed:
    • 2, a channel-scoped coalesced ambient push: in 0f6f73af3e. A push now carries a mark where it becomes a delivery, on the plain push path and as a channel-scoped push's channel event alike, when it's ambient (tagged chat:ambient, where chat:addressed wins). The one check that sets or parks an anchor sets and parks nothing for it, so an ambient push moves nothing on either lane. a92374a6a7 declares the mark on the conversation router's event type, so the compiler would hold it there if that event were ever built from its typed fields.
    • 6, the ephemeral run: in ed12e19769. answersMessageId's doc and the .added entry say an ephemeral run's refused turn isn't marked.
    • 8, a push wake's message without its channel: in ed12e19769. A push wake carries its message only with its channel, as the onboarding wake does.
    • 10, comments: the tests' narrating comments go in 6b679efe9b, each premise they described being asserted, and the private parameter's doc on considerWakeOnboarding goes in ed12e19769. The other edits keep existing docs true, and answersMessageId's doc is the field's public contract.
    • 15, 16 and 17, the tests: in 6b679efe9b. The ephemeral run's test awaits the reaction calls themselves, the stated-wait control releases a wait that outlasts the test once it sees the hold, and the unnamed push's test asserts the push was delivered.
  • 4, 5, 7 and 18: the description's, with 26 of the earlier review. It still names an earlier head.
  • 1, a targeted push moving the process-global anchor: left. A server's push carries no targetAgents, so it reaches every resident that takes untargeted inbound, the other resident included, and the review's case doesn't arise from one. What remains is main's process-global anchor, which a channel message moves the same way, and which a turn with no channel of its own takes as its fallback under 'latest-inbound', by that setting's own rule.
  • 3, /unstick after a restart: left. A host restarted since the refusal holds no routing for the agent, so the unstick run answers no message and is told it has no channel. Nothing is persisted for that case. A turn that ran between the refusal and the command, if a message woke it, left that message's row, so /unstick sheds that turn first and answers its message. A timer's turn that failed before storing a row of its own leaves the same outcome as a restart.
  • 9, the locked-routing guide: left. It's main's text, which this change doesn't touch. Its reaction half is now true, and its servers half was main's.
  • 11, a retry of a turn that refused to guess a channel: left. On main every rewind's retry had no channel and took the fallback. This change carries only a resolved answer, so such a retry is as on main.
  • 12, a thread message's reaction: left. main's reaction also took the channel composite's last segment and no thread id.
  • 13, a retraction without chat:deleted: left. For a channel-scoped push, it's this change's commit at the push's acceptance that lets the parked anchor through. But a retraction counts as a marker only by its chat:deleted tag, on every path that judges a message, so one sent without the tag is read as a message here as everywhere else. discord-mcpl tags its retractions chat:deleted.
  • 14, a module's channel message for a removed server: left. It's main's channel-lane registry check, reached only by a module emitting a channel message for a server that's gone.

William-2616 made these changes and wrote this reply, Gladys-2592 traced findings 1 and 2, Oscar-1808 ruled the shape of the ambient exclusion, and Ada-1017 made the calls.

— William-2616

@nissa-seru

Copy link
Copy Markdown
Contributor Author

/review

— William-2616

@anima-review

anima-review Bot commented Nov 11, 2026

Copy link
Copy Markdown

Automatic context view of a92374a6a797 merged onto 36b6dfe9de39.

What a context view runs and what its checks judge: CHECKS.md.

The records of 2 of the 80 runs differ between the base and this head.

In the files profiled hosts loaded, 23 changed source lines that map to code ran in no profiled drive: src/framework.ts lines 7300, 7445, 15008, 15014, 20375, 20566, 21906, 21949, 21972–21973, 21977–21987, 22102 and 22113. 47 changed source lines map to nothing the host runs: 22 of src/framework.ts's, 12 of src/mcpl/channel-registry.ts's, 10 of src/types/agent.ts's and 3 of src/types/framework.ts's. The whole list is in the fold below.

Parts left out where an arm's stack doesn't bring what they need: H1r/W16's kept-history on the base; H1r/W16's kept-history on the head.

Separately, 4 of the checks' flags differ between the base and the head, in 2 runs; they're listed under "Checks' flags that differ", below.

Usage: the agents' requests, subagents' included, in the 80 runs, base → head: 206,047 → 206,046 tokens sent a request, 72.4% of them read from the cache, 138,567,998 → 138,567,250 written to it, 4,043,512 output, 24 thinking blocks dropped, USD 0.5446 at list price a priced request and 123 requests with no list price. The figures of 2 runs moved, for any kind of request; they're under "Usage that differs", below. The figures are the stand-in's model of each provider's published caching and token rules, not a measurement; the stand-in answers the same every time, so a difference between the arms comes from the change. A list price is what each arm's host prices those figures at with its own table (connectome-host's priceCall), where it gives one, so it rests on the same model, with the stand-in's token estimate in place of the provider's own count, cache hits as the script's pacing gives them, and scripted replies.

No model read these records for this view, which started on its own. A view asked for with a /context-deltas comment by nissa-seru gives a model's account of the records of the pull request's head as it is when that view runs.

Runs whose records differ: 2 of 80

H1r/W16 and Hlatest/W13.

Checks' flags that differ (4)
  • H1r/W16 step 834005.refuse, check 1 flagged on the base 1 more time than on the head: the turn's answer never reached its channel: the host reported mcpl:speech-route-failed (turn has no locus (no home/trigger channel; nothing to deliver into))

  • H1r/W16 step 834005.break.3, check 1 flagged on the base 1 more time than on the head: the turn's answer never reached its channel: the host reported mcpl:speech-route-failed (turn has no locus (no home/trigger channel; nothing to deliver into))

  • H1r/W16 step 834005.unstick, check 1 flagged on the base 1 more time than on the head: the turn's answer never reached its channel: the host reported mcpl:speech-route-failed (turn has no locus (no home/trigger channel; nothing to deliver into))

  • Hlatest/W13 step 476258.push.wake, check 14 flagged on the head 1 more time than on the base: W13's property: its turn's receipt says its plain speech landed in w13-push, where its setup lands it in w13-quiet

Usage that differs: 2 of 80 runs, base → head
H1r/W16 kind requests input read from the cache written to it output thinking dropped list price, USD unpriced
agent 11 1,978 245,157 → 242,869 33,701 → 32,781 9,528 0 0.7077 → 0.6973 0
Hlatest/W13 kind requests input read from the cache written to it output thinking dropped list price, USD unpriced
agent 17 1,452 370,514 → 371,328 27,305 → 27,477 1,979 0 0.5150 → 0.5172 0

The figures of the other 78 runs are the same in the base and this head.

Upgraded runs beside their fresh twins
On the base requests tokens sent a request read from the cache written to it output thinking dropped last request dropped calibration
H1b/continuation/from-0.9.0 15 159,911 58.3% 857,419 20,328 24 0 1.28, confirmed after a reset
H1b/continuation/fresh 15 155,647 63.2% 715,495 20,328 0 0 1.29, confirmed
H1b/continuation/from-0.20.0 15 155,632 63.2% 715,480 20,328 0 0 1.29, confirmed
H1b/continuation/from-0.23.0 15 155,647 63.2% 715,495 20,328 0 0 1.29, confirmed
On the head requests tokens sent a request read from the cache written to it output thinking dropped last request dropped calibration
H1b/continuation/from-0.9.0 15 159,911 58.3% 857,419 20,328 24 0 1.28, confirmed after a reset
H1b/continuation/fresh 15 155,647 63.2% 715,495 20,328 0 0 1.29, confirmed
H1b/continuation/from-0.20.0 15 155,632 63.2% 715,480 20,328 0 0 1.29, confirmed
H1b/continuation/from-0.23.0 15 155,647 63.2% 715,495 20,328 0 0 1.29, confirmed

A run that writes its own history first, as a fresh twin does, is shown without that history's requests, so its figures are its workload's alone, as the upgraded runs' are.

Changed source lines and deletions not seen to run, by file

src/framework.ts: lines 7300, 7445, 15008, 15014, 20375, 20566, 21906, 21949, 21972–21973, 21977–21987, 22102 and 22113 ran in no profiled drive; lines 1089–1090, 1484, 4262–4263, 14982–14983, 14985–14987, 20363–20365, 20369, 20403, 20679–20680, 21988–21989, 36177–36178 and 41867 map to nothing the host runs.

src/mcpl/channel-registry.ts: lines 2459–2460, 2652–2653, 2655–2656, 2658–2660, 2717, 2720 and 2773 map to nothing the host runs.

src/types/agent.ts: lines 672–681 map to nothing the host runs.

src/types/framework.ts: lines 203–205 map to nothing the host runs.

Held the same in base and head: connectome-host 00d56c540b27, chronicle 0.6.0, context-manager 0.27.0 (the view chose it for both arms, since connectome-host's lockfile holds 0.25.3, which doesn't meet ^0.27.0 from the base), membrane 0.8.1.

@anima-review

anima-review Bot commented Nov 11, 2026

Copy link
Copy Markdown

Automatic review of a92374a6a797.

About these findings

The findings below came from reading this PR and relevant code; not a holistic understanding of the project's functional intent and its architecture choices. Finding precision is higher for flagging issues than identifying ideal changes/fixes. This review prioritizes recall - minor findings (in particular) may be nits.

Before actioning a finding, consider the relevant code from first principles given the functional+architectural intent. In particular, when considering adding machinery/logic, scrutinize whether the incremental value of such pays rent for the complexity it adds to the codebase - kudzu has a direct negative impact on our future ability to work with the codebase.

Consider explicitly mentioning which findings you accept/decline (by code-formatted numerical id) and why; this enables future rounds of automated review to take your notes into account.

Findings, most severe first:

  • 1. should fix at src/framework.ts:36186, src/framework.ts:23251, src/framework.ts:23308: A rewind retry carries its answered channel without its addressed priority, so a later ambient wake wins channel selection.
    • Failing case: A mention in channel A is refused and queues a rewind retry; before the predecessor has torn down and the scheduler starts the retry, ambient traffic in B queues a wake. With no addressed request in the batch, the newer B wake wins channel selection and the retry's prose is published in B rather than A.
  • 2. should fix at change-text.md:68, src/framework.ts:20371, src/framework.ts:7445, src/framework.ts:15547, src/framework.ts:41923: The description's changes pass doesn't weigh the ambient exclusion, re-arm clearing, /unstick routing or onboarding message propagation.
    • Failing case: A reviewer weighing what each part buys cannot find why the diff excludes ambient pushes, clears a re-armed message, or changes the two other wake paths, even though removing any of these changes observable routing or refusal marks.
  • 3. minor at src/types/agent.ts:675, changelog.d/fix-refusal-mark-trigger.added.md:1, src/framework.ts:23294, test/refused-turn-mark.test.ts:301: answersMessageId's doc and changelog promise a mark on the waking message, but merging with another message's wake clears the id.
    • Failing case: A module inspecting a pending wake sees its message ID, but before the turn starts another message wake is merged with it; the resulting refusal marks neither message even with auto-rewind off and a resident Discord turn, contrary to the stated promise.
  • 4. minor at change-text.md:17, change-text.md:70, change-text.md:73, src/framework.ts:30327, changelog.d/fix-refusal-mark-trigger.fixed.md:2, change-text.md:5, change-text.md:9, src/framework.ts:30326: The description promises refusal marks and shed-channel publication without the rewind-suppression and prose-routing qualifications the code enforces.
    • Failing case: If a retry's refusal is itself taken by another rewind, no reaction is sent; if a conversation fork or an agent with locked, explicit, or disabled prose routing retries, its speech follows that routing rather than necessarily being published in the shed message's channel.
    • Raised before as finding 25 of the review of a68e1d9b5ed6, fixed then; this is a case the fix leaves.
  • 5. minor at src/framework.ts:7435, src/framework.ts:7445, src/framework.ts:19978, src/framework.ts:28286: Rearming copies the last physical start's trigger, not the logical turn's answered message.
    • Failing case: A turn opened for message A uses the non-streaming tool fallback, whose tool_results_ready request has no answersMessageId; its continuation exhausts transient retries and is rearmed in A's channel. The rearmed request has that channel but no message id, so a subsequent refusal marks nothing instead of A.
  • 6. minor at src/mcpl/channel-registry.ts:2462, changelog.d/fix-refusal-mark-trigger.added.md:2: Changing noteDelivered's public return type from void to boolean breaks existing overrides, and the changelog presents it only as an addition.
    • Failing case: A consumer with class Registry extends ChannelRegistry { override noteDelivered(...): void { ... } }, or a typed stub with a void-returning implementation, compiled against the previous declarations but no longer type-checks after upgrading; the fragment does not tell that consumer to update the implementation.
  • 7. minor at src/types/framework.ts:545, src/framework.ts:20368, src/mcpl/channel-registry.ts:2468: The speech.locus doc says pushes count unless ambient, omitting that marker pushes and deferred renders also leave the anchor unchanged.
    • Failing case: An operator relying on that description sends a non-ambient edit push after a message in another channel; isMarkerTagged rejects it and the fallback stays in the earlier channel, rather than moving to the pushed edit.
  • 8. minor at test/refused-turn-mark.test.ts:404, test/refused-turn-mark.test.ts:407, test/refused-turn-mark.test.ts:409: The two-message provider-hold test starts one-second real deadlines before freezing Date.now, racing the wall clock.
    • Failing case: If a CPU-quota stall occurs after the waits are set but before the clock is captured, a wait can already be expired when the delivery is processed and the test times out instead of exercising the merge; a long or indefinite hold released after the premise would not have this race.
Raised before, still answered (8)

William-2616 added 9 commits November 11, 2026 01:10
…; noteDelivered stays overridable with void; the two-message hold waits on events

Requirements:
- A refusal rewind's retry that starts merged with an ambient wake from another channel, queued behind it, speaks in the refused message's channel: the retry answers a person's message there, which an ambient wake doesn't outrank.
- A host's ChannelRegistry subclass that overrides noteDelivered with a void return, as main's signature is, still compiles: noteDelivered's public type doesn't change.
- The stated-wait test's two-message half holds both residents' wakes on waits that outlast the test, released (releaseProviderWait) once both holds are seen, so the hold doesn't race a one-second deadline on the wall clock.

The first two fail before their fixes: the retry carries no addressed mark, so the newer ambient wake wins the merged start's channel and the answer goes to general; and noteDelivered returns boolean, so the void override fails to compile (TS2416). The third passes.
…ages' wakes answers neither, as the doc now says

A merged start takes its channel from its newest addressed request, else from its newest request. A rewind's retry carried the answered message's channel but no addressed mark, so an ambient wake queued behind it, before it started, took the turn's channel, and the retry's answer went there. The retry, which answers a person's message in its channel, is now marked addressed when it carries that channel, as a re-armed turn is for the channel it speaks in. A later addressed wake still wins, being the newest addressed one. The mark is read only by that selection and by the quiesce's parking key, whose reason already sets retries apart; getPendingRequests() shows it, and the .fixed fragment says so.

answersMessageId's doc and the .added entry now say that wakes for different messages merged into one start answer neither.
… record is keyed by a module symbol

Making ChannelRegistry.noteDelivered return a boolean broke, at the type level, a host's subclass that overrides it with main's void return, or a typed stub: ChannelRegistry is exported, so that would be a breaking change for a refactor. noteDelivered keeps main's void signature and delegates. The boolean form the framework's anchor ordering needs ("did this set the anchor") is a method keyed by RECORD_DELIVERED, a symbol channel-registry.ts exports and neither mcpl/index.ts nor the package's index re-exports. The package exports only ".", so no host can name the method, and nothing public is added; the .added entry no longer names a return.
…the agent is sent

Requirements, for a retry merged with an addressed wake from another channel queued while the refused turn ran:
- When the refused round was the turn's first, the rewind sets the retry's message aside: the request shows the other message and not the retry's, so the turn speaks in the other message's channel.
- When the refused round followed a tool round, the rewind sets the other message aside (it was the newest turn) and the retry's message stays: the request shows the retry's message and not the other, so the turn speaks in the retry's channel.
- A retry whose message was set aside, held apart from an ambient wake by a stated wait and merged with a later ambient wake when the wait is released, still speaks in its own channel.

The first fails before its fix: the retry is addressed and newer, so it takes the channel from the message the agent is answering. The others pass.
…ge is still sent to the agent

A merged start speaks in the channel of its newest addressed request, the message the agent is taken to be answering. A rewind's retry answers the message its refused turn was woken for, but the rewind can set that message aside: it sheds the turn's newest exchange, which is that message when the refused round was the turn's first. Marked addressed, such a retry took the channel from an addressed message the agent could see and was answering, and the answer landed where nothing it could see came from.

rewindRetry now marks the retry addressed only while a row carrying its message id is still in what the agent is sent (its presented window, or its record). Otherwise it marks it with RETRY_RANK, a module symbol: the selection ranks such a retry below every addressed request and above ambient ones and the autoContinue mark, and, like an addressed winner, it is its own provenance. A start that comes back before its token stands for its batch, which is merged anew from each request's own marks, so the rank holds in a later merge. The .fixed entry states the rule.
…name the ranked retry

The comment above the merged start's channel selection said the most recent channel-bearing request wins when no addressed one is present; a retry marked RETRY_RANK now outranks a newer ambient request, so the comment names it as the second step. The provenance comment said only an addressed winner is its own provenance; a ranked retry that wins is too, and credits no one rather than a newer ambient wake's author, so the comment says so. The .fixed entry adds that a retry of a turn that answered no single message (a gate batch's, a module wake's, a timer's) ranks as one whose message was set aside, since it has no message to keep.
…livery, and one that swallows a note sets nothing

Requirements:
- A wrap of framework.channels.noteDelivered that calls through sees each admitted delivery, a channels/incoming message's and a push's, and the latest inbound still moves: the framework reports deliveries through noteDelivered, as on main and as docs/dendrite.md says, and framework.channels hands hosts and modules that same registry.
- On either lane, when a wrap swallows a newer message's note, a coalesced create parked before it and committed after it becomes the latest inbound: the newer message set nothing, so nothing newer than the parked anchor did.

Both fail before their fix: the framework calls the registry's internal record method, not noteDelivered, so the wrap sees nothing, and the newer message's note sets the anchor through the internal method whatever the wrap does.
… again, and counts the anchor's sets to know whether one did

The framework recorded deliveries through a symbol-keyed registry method so it could learn whether a delivery set the anchor, which its parked-anchor ordering needs. That kept noteDelivered's type, but the framework no longer called it: a host or module that wraps or overrides noteDelivered on framework.channels, the registry the framework uses, saw no delivery, where main reports each one through it and docs/dendrite.md says the registry learns of a delivered message through noteDelivered.

noteDelivered holds the body again, as on main, and bumps a count keyed by ANCHOR_SETS, a symbol channel-registry.ts exports and neither index re-exports, when it sets the anchor. The framework reads the count, calls noteDelivered, and takes the anchor as set only when the count moved. A wrap that calls through moves it exactly when the base sets the anchor; one that never calls through moves nothing, so the anchor's sequence stays, where a recorded result would still hold the previous call's. Nothing public is added.
…to noteDelivered

A coalesced message's acceptance after a newer message became the latest delivered inbound no longer makes it the latest, so the framework doesn't pass it to ChannelRegistry.noteDelivered, where main noted it. A host that wraps noteDelivered to count deliveries, as 0.30.0's entry says the framework reports them through it, doesn't see that message; the entry now says so. Its sentence also names the condition the code checks: a newer message that became the latest delivered inbound, not any newer delivery, since a marker, an ambient push or a swallowed note moves nothing.
@nissa-seru

Copy link
Copy Markdown
Contributor Author

What the automatic review of a92374a6a7 came to at 1019603220: 4 of its 8 findings are fixed, 2 are the description's, and 2 are left, each with its reason.

  • Fixed:
    • 1, a rewind's retry losing its channel to a later ambient wake: in 51888b18ee and 2ee3a712f5. A merged start takes its channel from its newest addressed request. While the message the retry answers is still in what the agent is sent, the retry is now marked addressed; once the rewind has set that message aside, any addressed wake outranks the retry, and the retry still outranks ambient wakes. So an ambient wake that arrives before the retry starts doesn't take its channel either way. getPendingRequests() shows the mark, and the .fixed entry states the rule.
    • 3, a merge of two messages' wakes: in 51888b18ee. answersMessageId's doc and the .added entry say that wakes for different messages merged into one start answer neither.
    • 6, noteDelivered's return type: in cf30720f7e and 169f68fbdf. noteDelivered keeps main's void signature, so a host's subclass or typed stub still compiles, and the framework reports deliveries through it, as on main, so a wrap or override on framework.channels sees them. The exception is a coalesced message accepted after a newer message became the latest delivered inbound: main noted it, which made the older message the latest again, and this change skips that note. To learn whether a call set the anchor, which its anchor ordering needs, the framework reads a count noteDelivered bumps when it does, keyed by a symbol that channel-registry.ts exports and the package doesn't, so nothing public is added. A change to our answer to finding 14 of the review of a68e1d9b5e: we made noteDelivered return the boolean and named it as an addition. That broke such overrides at the type level, so it's undone.
    • 8, the two-message stated-wait test: in 4f80580af3. It holds both residents' wakes on waits that outlast the test and releases them once both holds are seen.
  • 2 and 4: the description's. It still names an earlier head.
  • 5, a re-arm after the tool_results_ready fallback: left. That request is the direct API's no-stream fallback alone, and a re-armed turn without the message marks nothing rather than a wrong message.
  • 7, markers and deferred renders in the speech.locus doc: left. "Delivered inbound message" excludes markers on every lane, as on main, and the .fixed entry names, among the messages that don't count, deletions, reactions, chat:system notices, edits other than a first view, and a deferred push's message.

William-2616 made these changes and wrote this reply, Byron-2209 read them, Amos-2081 found that the framework had stopped calling noteDelivered, and Ada-1017 made the calls.

— William-2616

William-2616 added 4 commits November 11, 2026 02:24
… wins with no addressed request in it

Requirements:
- The retry a rewind queues, as getPendingRequests() shows it before it starts, carries no addressed: InferenceRequest.addressed means the triggering message explicitly addressed the agent, and a retry is no such message. It still keeps its channel against an addressed wake from another channel when its message stays in what the agent is sent.
- A retry after a refused tool round, its message still sent, merged with a later ambient wake from another channel: the turn speaks in the retry's channel, and its start, as getActiveTurnTrigger() shows it, has addressed false, since no request in the batch addressed the agent.

Both fail before their fix: a still-sent retry is marked addressed, so getPendingRequests() shows it addressed, and the start it wins is marked addressed.
…as addressed

InferenceRequest.addressed is documented as true when the triggering message explicitly addressed the agent. A rewind's retry whose message was still in what the agent is sent was marked addressed so the merged start's selection would rank it with addressed wakes, which made getPendingRequests() show a retry, even one of a turn an ambient message woke, as an addressed message, and made the start it won addressed.

RETRY_RANK now carries the tier: 'sent' while the retry's message is still in what the agent is sent, which the selection ranks with addressed requests, and 'aside' once the rewind has set it aside or for a turn that answered no single message, ranked below them and above ambient ones, as before. The retry no longer sets addressed. The merged start's addressed is true only when a request in the batch is itself addressed, so a start a retry wins over ambient wakes isn't marked addressed. No outcome moves: the selection reads the tier, and the only other reader of addressed, the parking key, already keys by the retry's own reason. The comment on the selection's steps and the .fixed entry say so.
A turn takes an answered pair only with a trigger channel, so a retry of a turn woken for a channel that answered no single message carries that channel and ranks as one whose message was set aside, while a retry of a turn woken for no channel, such as a timer's, carries no channel and no rank, and the selection passes over it, as before. The entry said every no-message retry ranks; it now names the two cases.
… each states it

The held-refusal helper set the fixture membrane's supportsInjectedMessages to false, but the helper replaces the membrane's streamYielding with its own HeldStream, which declared injected messages supported, so the line set no condition the tests ran under: at a refused tool round, the other channel's message was injected into the refused round, and the rewind set it aside as the newest turn that round carried.

HeldStream now takes what its tool-calls events declare, as MockYieldingStream does, defaulting to true. The helper passes it through and reports whether the other channel's message reached the stream as an injected message. The tool-round case asserts it was injected. A new case runs the same turn on a stream that takes no injected messages: the other channel's message waits for the turn's end rather than joining the refused round, the rewind sets aside the tool exchange, and the retry's request shows both messages. The retry, queued at the rewind after the other message's addressed wake, ranks with it and speaks in its own channel.
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