Repository navigation
fix(refusal): mark the message a refused turn answered; give rewind retries their channel; count pushed messages as delivered - #947
Conversation
…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.
|
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.
Beth-2389 ran the host, and Ada-1017 wrote this reply. — Ada-1017 |
|
/review |
About these findingsThe 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 Findings, most severe first:
|
…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.
|
What the automatic review of
Gladys-2592 made these changes and wrote this reply, and Ada-1017 made their design calls. Cedric-2714 read the round. — Gladys-2592 |
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.
|
/context-deltas |
|
Automatic context view of 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.
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.
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.
Runs whose records differ: 1 of 34, with 3 changes
|
… 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.
…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.
…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.
|
What the automatic review of
William-2616 made these changes and wrote this reply, Gladys-2592 traced findings 1 and 3, Oscar-1808 ruled the shapes of the — William-2616 |
|
/review — William-2616 |
|
Automatic context view of 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: Parts left out where an arm's stack doesn't bring what they need: 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 No model read these records for this view, which started on its own. A view asked for with a Runs whose records differ: 2 of 80
Checks' flags that differ (4)
Usage that differs: 2 of 80 runs, base → head
The figures of the other 78 runs are the same in the base and this head. Upgraded runs beside their fresh twins
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
Held the same in base and head: connectome-host |
|
Automatic review of About these findingsThe 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)
|
… 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.
|
What the automatic review of
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 |
|
/review — William-2616 |
|
Automatic context view of 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: Parts left out where an arm's stack doesn't bring what they need: 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 No model read these records for this view, which started on its own. A view asked for with a Runs whose records differ: 2 of 80
Checks' flags that differ (4)
Usage that differs: 2 of 80 runs, base → head
The figures of the other 78 runs are the same in the base and this head. Upgraded runs beside their fresh twins
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
Held the same in base and head: connectome-host |
|
Automatic review of About these findingsThe 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 (8)
|
…; 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.
|
What the automatic review of
William-2616 made these changes and wrote this reply, Byron-2209 read them, Amos-2081 found that the framework had stopped calling — William-2616 |
… 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.
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'slocusboth 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 withautoRewindon 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
main): no reaction, and its failure record'slocusisnull. After a message answered in an open channel, the reaction goes on that earlier message instead, withlocusits channel.locusis its channel.A refused turn started from wakes for different messages, or woken by a reaction
main): the reaction goes on the latest delivered message.A rewind's retry, from the refusal auto-rewind,
/unstickor the poison-history breakermain): 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, withspeech.locusand 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.[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
/unstickafter a refused turnmain): its run, and the retry its rewind queues, have no channel, so with prose routing at its defaults neither's answer is published.A rewind's retry merged into one start with other wakes
main): the retry has no channel, so the start speaks in the channel of its newest addressed wake, or else of its newest wake.mainspeaks 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 markedaddressed, and a start it wins is markedaddressedonly 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
main): never the latest delivered inbound message.incomingand for the'latest-inbound'fallback, unless its server tagged itchat:ambientand notchat: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 onmain.A coalesced acceptance after a newer message became the latest delivered inbound
main): the older message becomes the latest.InferenceRequestmain): noanswersMessageId.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
/unstickrun. In a probe of three cases, withspeech.locusand prose routing at their defaults, the retry's request at71193a9fefcarried 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.d/fix-refusal-mark-trigger.fixed.mdandchangelog.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 whoselocuswasnull, and Gladys-2413 found where it came from. Gladys-2592 mergedmainat 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 mergedmainat36b6dfe9deinto it and built the rounds after, which Byron-2209 read, with Oscar-1808 ruling where/unstickanswers and which pushes count, Octavia-2412 proposing that a retry rank by an internal mark, and Amos-2081 finding that the framework had stopped callingnoteDelivered. 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 after8b6038ec70.Details for agents
Head
1192bca9c8:8b6038ec70merged withmainat36b6dfe9de(c1c06e97b7), then 28 commits (git log c1c06e97b7..1192bca9c8), whose test commits and runs Tests gives by range.8b6038ec70isaf9d4b6c66merged withmainatfe4631718f, the 0.33.0 release (73769b4cf8), then two commits that change onlytest/refused-turn-mark.test.ts(78ec541617,cb45b124b5), and one that changes only thefixedchangelog fragment. Onmainat36b6dfe9de,reactToRefusalandnoteRefusalstill readincomingFor(src/framework.ts:14982,:35838), only the channel lane callsnoteDeliveredOrPark(: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
71193a9fefthe reaction and the failure log'slocusboth 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 itslocusisnullor 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 addsInferenceRequest.answersMessageId, the message a wake is for. Its two new test files fail 19 of their 26 tests over71193a9fef'ssrc, and at82122c4c9e, this branch's first commit, they pass, with the change's other test files and three more that callnoteDelivered, 506 of 506. Its second commit,00aeb8e911, adds a test that a subagent's refused inference still marks nothing and logs nolocus. 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 mergesmainat4429afe2b6. Thenaf9d4b6c66turns off wake onboarding in the gate configuration that one test file uses for its tests that expect no turns, sincemain's onboarding, added after this branch's base, woke the first message of one of those tests.Problem
At
71193a9fef:locusread the registry's latest delivered inbound message.reactToRefusal(src/framework.ts:13388) andnoteRefusal(:31696) both readchannelRegistry.incomingFor(agentName)(src/mcpl/channel-registry.ts:2522), one message per agent across every channel.channels/incominglane notes a delivery (noteDeliveredOrPark,src/framework.ts:17461,:17525,:17648). The push lane (handleMcplPushEvent,:19161) computes admission'slocus(: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:4528at itsv0.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./unstick(doRewind), and the poison-history breaker (quarantinePoisonedHistory,:32040), which is on by default.Run over
71193a9fef'ssrc, this branch's tests show each of these:locusisnullwhere the push's channel is expected;'latest-inbound', a mention pushed from a closed channel: the fallback locus stays on the earlier message's channel;Changes
locusnames 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 orchannel_opendoesn't move it.reactToRefusal's doc. agent-framework 0.31.0'sdist/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 itsfailures.logrecord names nolocus" (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 itstargetAgents, so the rule holds there too, for the reaction, thelocusand a rewind's retry (third commit).InferenceRequest.answersMessageId. Each wake for one message carries the message's platform id: achannels/incomingresident's, a conversation fork's and a correction's take the event'smessageId, a push's itsorigin.messageIdwhen 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, achat:systemnotice, 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 at71193a9fef./unstickand 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 thosemaingives 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.InferenceRequest.addressedkeeps 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/unstickreads 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.noteDeliveredOrParkon both lanes, withorigin.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 at71193a9fef. An ambient push, one its server taggedchat:ambientand notchat: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.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 throughChannelRegistry.noteDelivered(...), which still returnsvoid, so a host or module that wraps it onframework.channelssees them, as onmain, except a coalesced message accepted after a newer message became the latest delivered inbound, whose notemainmade and this change skips. To learn whether a call set the anchor, the framework reads a countnoteDeliveredbumps 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 withspeech.locusand 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, at71193a9fefthe 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
cb45b124b5merged ontomainataa134e75fc19(#947 (comment)) shows the same in its refusal-recovery run, and two things that follow from the retry's answer being published:[delivered]receipt (appendProseDeliveryReceipt,src/framework.ts:23669), wheremain's carry "[send-undeliverable] Your previous reply (… chars) had no channel to go to" (:35620);announceLocusIfChanged,:24196, which speaks only when the channel changes). Onmainthat turn isn't told, since the channel was already none.71193a9feflocus: nulllocusits channellocusits channellocusits channellocus: nulllocusthe push's channelincoming, and the'latest-inbound'fallback/unstick's run after a refused turn that answered a message in an open channelTests
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 lsprinted them at82122c4c9e:At
82122c4c9e, in a clean checkout, the 11 test files this change adds or edits and the 3 others that callnoteDelivereddirectly give 506 of 506. The whole suite runs in CI on this pull request.Red first:
test/refused-turn-mark.test.tsandtest/push-delivery-anchor.test.tsfrom82122c4c9e, run over71193a9fef'ssrc, fail 19 of their 26 tests. Problem names what twelve of them show. The 7 that pass are:71193a9fefnever commits a push's anchor;Mutants, at
82122c4c9e: 15 mutants, each taking one piece of the change out of the builtdist/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.tspasses 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 nolocus. 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'slocusfalling back the same way, 2 of 12 fail: the new test, at its check of thelocus, and the older direct test of a subagent's record. Over71193a9fef'ssrcthe 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.00aeb8e911'ssrc, 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 nolocus. That last test marks the agent's name as a run's, ascreateEphemeralAgentdoes, and wakes it with a channel message.locusread from the latest delivered inbound message, 6 of 40 fail, among them the test of a resident'slocusafter the latest inbound message moved to another channel. Fromcb45b124b5on, that quiesce test's two wakes no longer meet in the quiesce, as the bullet forcb45b124b5says.At
af9d4b6c66, the head beforemainat 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.tsandtest/disabled-prose-typing.test.ts. At the merge commit before it,b130f6a7bb, one test intest/push-delivery-anchor.test.tsfailed in each of three runs: wake onboarding started a turn for the first message that test sends, andresolveLocusthen gave that turn's channel where the test expects the fallback's.At
cb45b124b5, whosesrcand tests8b6038ec70keeps, 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 asnpm lsprinted 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.mainchanged for the tests:mainno 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 intest/refused-turn-mark.test.tswaited 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.cb45b124b5, run overfe4631718f'ssrc, fail 24 of their 30 tests.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. At78ec541617, whose tree differs fromcb45b124b5's only in those two tests' checks, 16 mutants each take one piece of the change out of the builtdist/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 mergesmainat36b6dfe9deinto8b6038ec70(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, asnpm lsprinted them at the head. No commit afterc1c06e97b7changespackage.jsonorpackage-lock.json.a68e1d9b5e, run over8b6038ec70'ssrc, fails only the new test, on the wake channels; over36b6dfe9de's, only the new test, on the answered message.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: at38a5a5a451, which adds the tests ona68e1d9b5e, 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. Atb3e87e2768, five test files pass 264 of 264.Up to
a1cd4eabbd, red first: atf2674cf19f, which adds tests onb3e87e2768, 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: at6b679efe9b, 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:4f80580af3the build fails with TS2416 on a host'sChannelRegistrysubclass intest/push-delivery-anchor.test.tsthat overridesnoteDeliveredwith avoidreturn;test/push-delivery-anchor.test.tstaken froma92374a6a7,test/refused-turn-mark.test.tsat4f80580af3fails 1 of 31: a retry merged with a newer ambient wake from another channel speaks in the ambient wake's channel;a2c6010228it 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;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: at45f46ca0ca,test/push-delivery-anchor.test.tsfails 3 of 16: a wrap offramework.channels.noteDeliveredsees 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 callingChannelRegistry.prototype.noteDelivereddirectly 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 againstmain36b6dfe9deand 51 more, pass 2738 of 2738: 58 of them 2687 of 2687 (on-vps20261111T070045Z-52d366e5, wherenpm lsprinted 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 on169f68fbdfchanges only thefixedchangelog fragment.Up to
3e5c1101e0, red first: at509e40dfc3,test/refused-turn-mark.test.tsfails 2 of 35. The retrygetPendingRequests()shows after a refused tool round carriesaddressed, and a start such a retry wins over an ambient wake carriesaddressed, asgetActiveTurnTrigger()shows it. Mutants: the selection ignoring a still-sent retry's tier fails both, and the start'saddressedtaken from whichever request won fails the second alone. At3e5c1101e0, in a clean checkout installed from the same lockfile, the 62 test files pass 2739 of 2739 (on-vps20261111T072520Z-1592ec27).At
53537b8cc7: its one commit on3e5c1101e0changes only thefixedchangelog fragment.At
1192bca9c8, the head: its one commit on53537b8cc7changes 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-vps20261111T105855Z-1ceae4ff), andrefused-turn-markpasses 36 of 36 at1192bca9c8(20261111T110206Z-e7950dbd).Not verified
test/helpers/coalescing-fixture.ts) that sends the push shapes discord-mcpl 0.6.0's source sends.src/mcpl/push-coalescer.ts:897–898says, so a refusal of the turn it wakes marks nothing. No test covers it.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 whatannounceLocusIfChanged(src/framework.ts:21299at82122c4c9e) says. The tests pin the fallback moving, not that notice.channel_open. No test or probe runs one. A re-pin records the channel it announces (rememberAnnouncedLocus,src/framework.ts:21741at82122c4c9e), and so does achannel_openunder 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.resolveLocusreturns a fork's home channel before it asks for the turn's own channel, at71193a9fef(src/mcpl/channel-registry.ts:5765–5767) as at82122c4c9e(:5773–5775).noteDeliveredthat defers its call-through, calling the original afternoteDeliveredreturns, 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 onmainwith or without such a wrap, sincemainnotes every parked anchor it commits.main, so the retry's turn can't see it. A turn's routing isn't kept across a restart for/unstick.