Skip to content

fix(gate): keep notices out of a batch's route; name its channels - #948

Merged
nissa-seru merged 62 commits into
anima-research:mainfrom
nissa-seru:effie/debounce-notices
Nov 9, 2026
Merged

nissa-seru merged 62 commits into
anima-research:mainfrom
nissa-seru:effie/debounce-notices

Conversation

@nissa-seru

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

Copy link
Copy Markdown
Contributor

Why. On main, a chat:system notice in a debounced gate batch can decide where the agent's reply goes, though 0.28.0's changelog says no notice does. And a reply to a batch that addressed no one is told the turn "came from no channel", though the batch's messages came from channels.

This keeps notices out of a batch's choice, and names the batch's channels in that notice.

A batch whose newest addressed event is a chat:system notice

  • base (main): the notice decides the batch's route, its addressed flag and the channel its wake names. A batch of a mention in discord:g:room and a newer pin in discord:dm:5 takes the route discord:dm:5.
  • head: a notice, other than a coalesced render's latest occurrence, decides none of them, even when it's addressed by its place. Where a message in the batch names a channel or an author, the wake's author and time come from the batch's messages.

A reply with no channel to go to, after a batch in which no message addressed the agent

  • base (main): the [send-undeliverable] notice says the turn "came from no channel, and it had no home or recent channel to reply in; if it should have had one, ask whoever runs this host".
  • head: where the host resolved the channel of at least one of the batch's messages, the notice says the turn came from those channels and that no channel was chosen for its reply. Where some can take a send now, it says to send the reply to one of those explicitly. For each other, it says why it can't take one, and for a channel whose server this host is connecting or reconnecting to, to send the reply there explicitly once it's back. Two turns keep the old notice, by reading: one none of whose batches gave it a channel the host resolved, and one whose own wake named a channel this host couldn't resolve, though a batch's wake joins it later.

What a host sees of that reply

  • base (main): the failure passed to ChannelRegistry's onRouteFailure option carries no byDesign, and the logged line gives its reason as "turn has no locus (no home/trigger channel; nothing to deliver into)".
  • head: where the new notice is written, the failure is marked byDesign, and the logged line gives its reason as "turn has no locus (no channel was chosen for its batch's reply)".

A provider hold that folds a held turn's retry into an equal held wake

Documented rule: the exported wakeProvenance's doc

  • base (main): "Provenance of a batched wake: the newest ADDRESSED channel-bearing event wins (mention / reply / DM outranks newer ambient chatter — the framework's own locus rule); else the newest channel-bearing event. Channel, author and addressed all come from that one event, so they can never disagree."
  • head: "Provenance of a batched wake: the newest ADDRESSED event that names a channel or an author wins (mention / reply / DM outranks newer ambient chatter — the framework's own locus rule); else the newest event that names either. An addressed event's resolved route counts as its channel; an ambient one's doesn't. Channel, author and addressed all come from that one event, so they can never disagree. A chat:system notice is never chosen for the batch's route, channel or addressed flag, even one addressed by its place (a pin in a DM), since a notice never decides where the agent's speech goes; a coalesced render whose latest occurrence is a notice isn't taken as one (GateEventInfo.folds). A batch with no such event carries only its batchChannels when it has them; else its chosen event is its newest notice that has an author, of which it takes only the author and time."

Documented rule: 0.14.0's changelog, superseded by this release's fragment

  • base (main): "Gate-batched wakes (gate:debounce) carry telemetry provenance: the EventGate hands the framework the newest addressed channel event (else the newest event naming a channel or author) as counterparty (<serverId>:user:<id>), wakeChannelId (composite channel id; push-event raw ids are not reported) and wakeAt, all from the same event, so the turn's InferenceRequest — and a host stamping gateway telemetry from it — can say who and where woke the agent. Telemetry only: the turn's speech locus (channelId / addressed) is not set by gate wakes, so routing is unchanged. WakeProvenance is exported."
  • head: this change's changelog.d/gate-batch-notices.fixed.md, since a changelog's past entries aren't rewritten: "A chat:system notice in a debounced gate batch, other than a coalesced render's latest occurrence, no longer decides the batch's route, its addressed flag or the channel its wake names, as 0.28.0 says no notice does, even one addressed by its place: a pin in a DM no longer takes the reply meant for a mention elsewhere in the same batch. With a message in the batch that names a channel or an author, the wake's author and time come from the batch's messages, not a newer notice. The wake of a batch in which only its notices name a channel or an author, and which has no WakeProvenance.batchChannels, carries only the author of its newest notice that has one, with that notice's time, as the same notice woken directly from channels/incoming does."

@antra-tess, this changes the 2 documented rules quoted above.

It doesn't route a batch that addressed no one: #945 proposes that separately, since it would change a rule agent-framework publishes.

What hasn't run: the tests use the framework's stand-in MCPL server and a stand-in provider, and nothing ran against Discord. Several paths rest on reading alone, as the details' Not verified says. One case isn't handled: under conversation routing, which 0.22.0 deprecated, a batch in a channel a conversation fork's home binds also wakes the trunk, as at main, and with this change, by reading, that trunk's turn, if it then speaks with no locus, is told the fork's channel too.

  • Changelog fragments added: changelog.d/gate-batch-notices.fixed.md, changelog.d/gate-batch-channels.changed.md, changelog.d/gate-batch-channels.added.md and changelog.d/held-fold-marks.fixed.md.
Credits

Effie-2289 made the change, Ada-1017 made its design calls and checked its merge with main, and Ansel-2428 read each round, with Gordon-2414's pass over its tests. Humphrey-2407 and Rupert-2634 wrote the description, Lottie-2661 updated it for one round, and Ramona-2416 put it in this shape.

Details for agents

Head 0bf430cc888c, into main. It merges main at aa134e75fc, which the pairs take as their base. There, wakeProvenance still counts a chat:system notice among a batch's addressed events (src/gate/event-gate.ts:304–335 at aa134e75fc), and the notice's "this turn came from no channel" is at src/mcpl/channel-registry.ts:6430 there.

The catch-up. 746b81344c merges main at aa134e75fc. Its one conflict was a comment in deliverBatch, which keeps both sides' words. main's gate now delivers a batch per group of recipients, so each group's batchChannels come from its own messages. 795a534559 changes two lines of test/held-turn-retry.test.ts, which named the fields main replaced with readsRow. The merge left this change's src lines as they were. At this head, its src against main is 5 files, +242 −51.

Merge order. #940 carries the same fold fix. Whichever of the two lands second carries the union of both changes' fields at each carry place, and drops its copy of the shared tests. #947 and #952, both open, also change the retry pushes this change changes.

Documented rules. The top quotes the two this change makes untrue: wakeProvenance's doc (src/gate/event-gate.ts:299–302 at aa134e75fc, :322–334 at 0bf430cc888c; src/gate/index.ts:1 exports it) and 0.14.0's changelog (CHANGELOG.md:9552–9560). 0.22.0's changelog says "the newest addressed EVENT now decides the turn's channel" (CHANGELOG.md:8792). It isn't paired, since 0.28.0's later sentence already narrowed it in print: a chat:system notice "no longer decides where an agent's plain speech goes", and "This holds when the notice is also addressed by its place (a pin in a DM)" (:3109–3118).

In a debounced gate batch, a chat:system notice, other than a coalesced render's latest occurrence, no longer decides the batch's route, its addressed flag or the channel its wake names. When no message of a batch addressed the agent, and the host resolved the channel of at least one of them, the notice the agent gets for a reply that had no channel now names those channels, where it said the turn came from no channel. It says to send explicitly to any of them that can take a send now, and to one whose server this host is connecting or reconnecting to once it's back.

At main (aa134e75fc), a batch of a mention in one channel and a newer pin in a DM takes the pin's DM as its route, not the mention's channel. 0.28.0's changelog says no notice decides where an agent's plain speech goes, even one addressed by its place. And a reply to a batch that addressed no one is told it "came from no channel" and to ask whoever runs the host, though the batch's messages came from channels and nothing is misconfigured. The fourteen test files this change touches pass, 847 tests, and with main's versions of the five source files it touches, 58 of them fail (see Tests). This change doesn't route such a batch: #945 proposes that separately, since it would change a rule agent-framework publishes.

Problem

At main (aa134e75fc), in this change's tests run with main's versions of the five source files it touches (see Tests):

  • A notice deciding a batch's route. wakeProvenance takes the batch's newest addressed event as its route, counting chat:system notices. In test/event-gate.test.ts, a batch of a mention in discord:g:room and a newer pin in discord:dm:5, which is tagged as addressing the agent, got the route discord:dm:5, not the mention's channel. In test/chat-system-notices.test.ts, a notice the gate batched stamped its turn's wakeChannelId as world:commons, where the same notice woken directly stamps none. 0.28.0's changelog says a chat:system notice "no longer decides where an agent's plain speech goes … This holds when the notice is also addressed by its place (a pin in a DM)" (CHANGELOG.md:3109–3120).

  • A notice that's false for a batch. A batch in which no message addressed the agent sets no route, and under the default speech.locus: 'delivery' its turn had no channel in these tests. In test/batched-wake-locus.test.ts, a reply to such a batch from one channel, and from two, got:

    [send-undeliverable] Your previous reply (10 chars) had no channel to go to — this turn came from no channel, and it had no home or recent channel to reply in; if it should have had one, ask whoever runs this host. This is a routing/configuration situation, not a channel failure. This route delivered it nowhere; if another surface showed it (a reply thread, the console), that copy stands. It is saved in your archive.

    The batch's messages did come from channels.

Changes

  • Notices take no part in a batch's choice (wakeProvenance, src/gate/event-gate.ts). A chat:system notice decides neither the batch's route, nor its addressed flag, nor the channel its wake names, even when it's addressed by its place. With a message in the batch that names a channel or an author, the wake's author and time come from the batch's messages, not a newer notice, and the test of an addressed notice beside other events checks both, for a batch of a mention and for one of chatter, each with a newer pin (test/event-gate.test.ts:748–749, :759–760). A batch in which only its notices name a channel or an author, and which has no WakeProvenance.batchChannels, carries only the author of its newest notice that has one, with that notice's time (src/gate/event-gate.ts:357), as the same notice woken directly from channels/incoming does. wakeProvenance's doc names the exception.
  • A coalesced render is never taken as a notice at the gate. A render folds its batch's every occurrence while its tags are the latest's (MCPL RFC-006), so the framework marks the render it admits (the new GateEventInfo.folds, src/framework.ts:21482), and the gate doesn't take it as a chat:system notice (src/gate/event-gate.ts:4079), as the render's own wake already isn't (src/framework.ts:21525–21527). So a message the render folds keeps its channel when a notice came last.
  • A batch that addressed no one names its channels. When no message of a batch addressed the agent, the new WakeProvenance.batchChannels lists the registered channels its messages came from, those the host resolved, keyed on server and id, each once in the order they first appear. A channel some events gave with its server and others without is listed once each way, since only the host can tell they're one. A push the host can't place is passed over, and a notice names no channel. The framework carries them on the wake as the new InferenceRequest.wakeChannels, each once, whether the batch gave a channel's server or left the host to find it (src/framework.ts:5632). One given with its server is carried as given, and one given without it only if its id still names one registered channel when the wake is admitted (channelNamed, :21953–21956). A turn merged from several wakes names each of their channels once, also when a request in the merge names a channel (src/framework.ts:22537): a fresh start then takes that channel as the turn's, so no such notice is written, and a start that continues a turn with no channel names the batch's channels to it. When any of the merged requests has a cause of its own, from outside the framework and not as a silent heartbeat tick, and brought channels (bringsOwnChannels, :1909), the turn names only the channels of the requests with a cause of their own (hasOwnCauseForRun, :1905). So when an autoContinue, ! continuation or bounce wake is merged with a new batch's wake that brought channels, the turn names only the batch's. Merged with a heartbeat, or with a batch none of whose channels the host resolved, neither of which brings channels, it still names its finished turn's. Two such wakes parked while the host is quiesced, and coalesced into one, keep both batches' lines (src/framework.ts:22249) and both requests' channels (:22250–22252), and so do two equal wakes a provider hold keeps as one (:7137–7138). The merge's rule isn't applied there, since at either place a silent request can carry channels that came from a request with a cause of its own. The park keeps the newer of two wakes, silent or not, and writes the pair's channels onto it (:22245–22252), and the hold's release gives its batch's first member, which can be silent, the held request's own channels (:22171–22173). Neither place can tell where a request's channels came from, and the rule would drop a silent request's. A test parks a plain push, a silent heartbeat's push and another plain push with one key, for an agent the pushes' messages aren't written to, and checks that both plain pushes' channels reach the turn at resume ("three pushes parked with one key for an agent their messages aren't written to, a plain one, a silent heartbeat's and another plain one, name both plain pushes' channels at resume, …", test/turn-trigger-provenance.test.ts:276). The hold otherwise folds as main does: a request that joins an equal one held is taken into it, a joining retry marks the request kept (src/framework.ts:7128), and a merged start's batch joins the kept request's as a union of its members (:7132). So getHostModeStatus().gatedRequests counts an equal start's wakes apart, as at main, and a wake equal to one of the batch's can be in it twice. At the hold's release, the batch that runs in a held request's place gives its first member, the one the merge took its base from, only that request's own channels that no member of the batch holds: those of a request the hold took into it, and those it had itself when a merged start was folded into it (:22167–22174). So the channels of a wake the hold took into that request reach the turn, and a member the read check then drops leaves its channel off the turn. A wake that joins a turn at a later start, a held turn's retry among them, adds its batch's channels to the turn's, while the turn's locus stays as it opened (:27481–27482). A continuation of a turn, whether autoContinue's wake, a ! continuation's, a bounce's, a context-budget restart's or a tool-results re-inference's, carries the turn's batch channels too, whether or not the turn had a locus, and so does a retry of it, whether a rewind's, the inference-failure breaker's or the tool-result guard's, and an exhausted turn's re-arm (src/framework.ts:23644, at :7264, :19146, :19284, :25687, :25784, :26627, :29068, :29473 and :35364). A provider hold's retry of the turn, and its retry after an inference error, carry the channels the turn's routing holds in place of their trigger's (turnRetryOf, :23648, at :7191, :7203, :7317, :27816, :27986 and :29930), and none for a turn whose own wake named a channel that resolves to none. InferenceRequest.wakeChannels' doc says so (src/types/agent.ts:766–771). A turn that keeps its locus names none of them in a notice, since the notice takes them only with no locus (cameFrom, src/framework.ts:23617). Two cases follow for a turn that had a locus, such as one a channel_open gave:
    • A ! continuation of it, or a bounce, opens a new turn, which takes no locus from the turn it continues. With no locus, its notice names the batch's channels, where it said the turn came from no channel.
    • A re-arm of a batch's turn, or a held turn's retry, keeps its trigger's source. So in a merged start it brings the batch's channels, and takes the precedence a cause of its own with channels takes there (bringsOwnChannels, src/framework.ts:1909; :22537). A re-arm opens a new turn. With no locus, its notice names them too. A wake whose source is the framework's own, as a continuation's or a restart's is, still never takes the merged start's precedence (FRAMEWORK_WAKE_SOURCES, src/framework.ts:1898).
  • A provider hold's fold keeps each request's marks as the scheduler's merge does (src/framework.ts:7126–7136). When the hold keeps one of two equal requests, the one it keeps keeps the carried state it holds as a turn's (TurnCarry, :1299: the sends the turn reached and a silence it kept), read before the retry's mark goes on, else takes the other's, else holds none, and it stays re-armed only if both were, as a merged start does (:22557–22561). At main, a held turn's retry folded into an equal held wake lost that state, a retry folded into a re-armed wake could revive the state of the exhausted retry that wake was re-armed from, and the kept request stayed re-armed, or not, by which of the two was held first. changelog.d/held-fold-marks.fixed.md says what that does to the continued turn. A wake held on its own that a start led by the turn's retry joins is that retry's event, as at main: the joining retry marks the request kept (src/framework.ts:7128), so abortInference() ends that wake with the turn and puts back only the start's other wake (test/held-turn-retry.test.ts:1354).
  • The notice (src/mcpl/channel-registry.ts, src/framework.ts). When such a turn routes its plain speech, or a sleep's announcement, with no channel to go to, the reason names those channels as the agent is shown them, says that no channel was chosen for its reply, and says to send its reply to that channel, or one of them, explicitly. That advice names only the batch's channels that can take a send now (src/mcpl/channel-registry.ts:6449–6459). Each other channel is named in its place with why. A channel the host no longer has registered is named as one this host has no channel there to send to, and one its server declared inbound-only with that reason. One whose server can't take a send, by the check plain speech and channel_publish make (publishingServer, :1489), is named with the refusal they give. When that's because this host is connecting or reconnecting to its server, it's named with that state and told to send there explicitly once the server is back. The failure passed to ChannelRegistry's onRouteFailure option is marked byDesign. The notice calls it "a routing situation", where every other no-channel notice keeps "routing/configuration". It holds under prose routing by locus and by hybrid. The reason lists every channel it's given, with no limit on how many. For three or more, src/mcpl/channel-registry.ts:6446 at this head joins them as "#a, #b and #c", and a test checks three, the whole reason with each channel's clause (test/channel-registry-routing.test.ts:1354–1359). The logged line's reason is "turn has no locus (no channel was chosen for its batch's reply)" (src/mcpl/channel-registry.ts:6466–6467), where main's is "turn has no locus (no home/trigger channel; nothing to deliver into)" (:6433 at aa134e75fc). It keeps "no locus", which connectome-host's scripts/connectome-doctor looks for (:311 at anima-research/connectome-host@82cbab99).
  • The helpers this change adds, weighed against writing each inline.
    • counterpartyOf (src/gate/event-gate.ts:374) builds a wake's counterparty from an event as <server>:user:<author>, the server being the event's, else its channel id's prefix, else channel. main builds it inline at its one call. This change calls it in two places, for the batch's chosen event (:364) and for the newest notice of a batch that only notices name (:357), so written inline that rule would be written twice.
    • batchRoutes (src/gate/event-gate.ts:379) lists a batch's channels: each registered route once, keyed on its channel and server, in the order first seen. It has one caller, wakeProvenance (:353), so inline nothing would be repeated. It keeps that rule apart from the one that picks the batch's provenance, the rest of wakeProvenance.
    • wakeChannelsOf (src/framework.ts:4136) merges several requests' batch channels, each channel once by sameChannel. Five places merge them: a provider hold's fold (:7137), the release of a request that stands for a merged start's batch (:22172), a quiesce's coalescing (:22251), a merged turn's start (:22537) and a turn's routing at each start (:27481). Inline, each would repeat the loop and its dedupe.
    • wakeChannelsMark (src/framework.ts:23644) gives the wake for a continuation or a retry the turn's batch channels, unless the turn's own wake named a channel this host couldn't resolve (noChannel). The nine places that build such a wake each add it to their request, three of them beside main's heartbeatTurnMark, which marks the same wakes for a heartbeat's turn. Inline, each of the nine would repeat its condition.
    • turnRetryOf (src/framework.ts:23648) marks a request as its turn's retry and gives it the channels wakeChannelsMark gives, in place of its trigger's own. Six places make such a retry (:7191, :7203, :7317, :27816, :27986 and :29930), and inline each would repeat the mark, the drop of the trigger's channels and the mark's spread.
    • bringsOwnChannels (src/framework.ts:1909) is hasOwnCauseForRun for a request that brought channels. Only the scheduler's merge of a start asks it (:22537), and it names the rule the field's doc and .added give.
    • cameFrom (src/framework.ts:23616) gives ChannelRegistry's new cameFrom option the turn's batch channels, each with how the agent is shown it, only when the turn has no channel to go to and its own wake didn't name a channel this host couldn't resolve (noChannel, :23617). Two places that route plain speech (:23613 and :26252) and a sleep's announcement (:41311) pass it. Inline, each would repeat the condition and the mapping.
    • publishingServer (src/mcpl/channel-registry.ts:1489) is the check that a channel's server can take a send: it's registered, and its grant has channels.publish. main writes that check inline at plain speech's and channel_publish's sends (:6212 and :6625 at aa134e75fc). The notice makes it for each of the batch's channels too (:6453), so the three call one helper, and the notice says a channel's server can't take a send by the same check the sends make.
    • list (src/mcpl/channel-registry.ts:6445) joins the channels the notice names as "#a", "#a and #b" or "#a, #b and #c". The notice joins two lists that way: the channels the turn came from (:6464) and, when only some of them can take a send, those that can (:6463). Inline, the joining would be written twice.
  • Unchanged: a batch with an addressed message routes as before. A batch that addressed no one still sets no route and keeps the shared fallback, and the notice is written only when that gives no channel. A turn whose own wake named a channel this host couldn't resolve keeps main's notice for a turn from no channel, though a batch's wake joins it at a later start, and its continuations and retries carry no batch channel.
  • Not handled: a channel a conversation fork's home binds. Under conversation routing, which 0.22.0 deprecated (CHANGELOG.md:8693), an ambient message in such a channel is stored only in the fork's context, and its batch also wakes the trunk. At main's 26c4bdb1b, on Linux, a run shows the trunk woken with a gate line that says its message was "already shown above at arrival", though the trunk's context doesn't hold it. With this change, by reading, that trunk's turn, if it then speaks with no locus, is told the fork's channel too (src/framework.ts:5629–5633). This change doesn't fix that wake, which is main's.
  • Also: a batch whose provenance names only its channels, with no event chosen, carries no time (at, src/gate/event-gate.ts:370), as at main, which gave such a batch no provenance at all (:320 at aa134e75fc). So its wake sets no InferenceRequest.wakeAt (src/framework.ts:5605).
  • Types. WakeProvenance.batchChannels and InferenceRequest.wakeChannels are new optional fields of types the package exports, byDesign is a new optional field of the failure passed to ChannelRegistry's onRouteFailure option, and cameFrom is a new optional option of ChannelRegistry's deliverSpeech and routeSpeech. GateEventInfo.folds is a new optional field of GateEventInfo, which src/gate/index.ts exports and the package's entry point, src/index.ts, doesn't.
  • Tests: in test/auto-continue.test.ts, test/batched-wake-locus.test.ts, test/event-gate.test.ts, test/chat-system-notices.test.ts, test/turn-trigger-provenance.test.ts, test/channel-registry-routing.test.ts, test/inference-exhaustion-rewind.test.ts, test/refusal-rewind.test.ts, test/route-failure-targeting.test.ts, test/mcpl-reconnect-events.test.ts, test/held-turn-retry.test.ts, test/exhausted-turn-rearm.test.ts, test/explicit-prose-routing.test.ts and test/tool-result-guard.test.ts, as the Tests section lists.

Tests

The first three runs below were made on Linux, in a clean checkout of 0bf430cc88, this head, with Node 24.22.2 and npm 11.19.1, in two invocations at 07:23 UTC on November 9. npm install gave TypeScript 7.0.2, @animalabs/context-manager 0.25.1, @animalabs/mcpl-core 0.4.0, @animalabs/membrane 0.8.0, @animalabs/chronicle 0.6.0, @modelcontextprotocol/client and @modelcontextprotocol/server 2.4.0, discord.js 14.28.1, ws 8.22.2, chokidar 4.0.3 and @babel/parser 7.29.13.

The runs marked 76246aca76 were made in a clean checkout of that commit, before the catch-up. They ran on Linux, with Node 24.22.2 and npm 11.19.1, in one of two invocations, at 04:04 and 04:35 UTC on November 6. The second printed the notice and ran the test files without the non-streaming fallback's mark. There npm install gave @animalabs/context-manager 0.24.2, @animalabs/chronicle 0.5.1 and ws 8.22.1, and the rest as above. Since that commit, this change's own lines differ by two commits. 795a534559 changes two test lines. 0bf430cc88 drops the locus condition from wakeChannelsMark, with its field doc and its tests. Six of the fourteen test files also changed with main: auto-continue, explicit-prose-routing, held-turn-retry, refusal-rewind, route-failure-targeting and tool-result-guard. Those runs ran these six as they were before the catch-up.

  • At this head. tsc --noEmit and the build succeed, and the fourteen test files this change touches, with main's test/locked-prose-routing.test.ts, pass: 878 tests, none failing or cancelled, run with --test-force-exit, the flag npm test uses.

  • Red first, with main's (aa134e75fc) src/framework.ts, src/gate/event-gate.ts, src/gate/types.ts, src/mcpl/channel-registry.ts and src/types/agent.ts in place of this change's. The build reported 35 type errors, each in a test that uses a field or argument this change adds, and still wrote the output the tests ran from. 58 tests fail:

    • auto-continue: 3 of 44, the three continuations of a turn its batch gave no channel: autoContinue's, a ! continuation's and a bounce's wake.
    • batched-wake-locus: 7 of 13, each a test of the notice: batches of one, two and three messages from one channel or two, under prose routing by locus and by hybrid, a sleep's announcement, and a ! continuation of a turn a channel_open gave a locus. Each failure shows the "came from no channel" notice.
    • event-gate: 7 of 70: the pin in a DM beside a mention, in the test of an addressed notice; a batch of notices alone, its author and time; the channels of a batch that addressed no one and of pushes that name only their registered channel; a push the host can't place, passed over; a newer ambient push that names only its route, not hiding an earlier event's channel and author; and a batch whose events give one channel with its server and without, which lists it once each way.
    • turn-trigger-provenance: 13 of 19: a turn merged from several such wakes, one merged with a wake that names a channel, to which main's merged start, keeping the first request's fields, gives none of the batch's channels, two such wakes parked while the host is quiesced and coalesced into one, three pushes parked with one key for an agent their messages aren't written to, a plain one, a silent heartbeat's and another plain one, which name both plain pushes' channels, a coalesced render whose latest occurrence is a notice, which names its channel at the gate, a batch that gave one channel both with its server and without, which the wake names once, a silent push carrying channels that a provider hold keeps, with an equal plain push folded into it, which keeps both pushes' channels, a context-budget restart of such a turn, which carries its batch's channels, a start that continues a turn with no channel, which names a joining batch's channels to it beside a wake that names a channel, a start that continues a turn whose wake named a channel this host couldn't resolve, which names none, a ! continuation of a turn whose wake named a channel this host couldn't resolve, which carries no batch channel, a turn merged from an autoContinue wake and a batch from another channel, which names only the batch's channels, and a turn merged from a heartbeat and an autoContinue wake, the heartbeat first, which names the continued turn's channels, to which main's merged start, keeping the heartbeat's fields, gives none.
    • inference-exhaustion-rewind: 1 of 15, the breaker's retry.
    • refusal-rewind: 1 of 24, the refused turn's retry.
    • channel-registry-routing: 4 of 81: the reason's words and the log line, an inbound-only channel among the batch's, a channel the host no longer has registered, and a channel whose server can't take a send, with no grant, while connecting, and gone with no reconnect.
    • mcpl-reconnect-events: 1 of 50, a channel whose server's connection dropped, at the test's premise: at main, the notice doesn't advise a send there while the server is up.
    • chat-system-notices: 1 of 9, the batched notice's stamp.
    • held-turn-retry: 16 of 85: a held turn's retry folded into an equal held wake, which gives that wake the turn's carry, the same over a carry the wake kept from an exhausted retry, and a retry with no carry folded into such a wake, which leaves it none, each at the carry the wake keeps; a gate wake a provider hold folds into a held turn's merged retry, and a held gate wake that an equal merged start folds into, each at its assertion that the turn names the wake's channel; a held merged start's release, at the channels the turn names after the read check drops a member; an exhausted turn's re-arm, of a turn with no locus that holds a later batch's channels in its routing, of one that has no channel, and of one a channel_open gave a locus; and the retry turnRetryOf makes at each of its six places, of a turn that holds a later batch's channels and, at a stated wait's hold, of one that has no channel too, at the channels each carries. The other tests of main's outcome for the hold pass with these files.
    • exhausted-turn-rearm: 1 of 84, a provider hold that folds a wake into an equal re-arm, which keeps it re-armed only if both were.
    • explicit-prose-routing: 1 of 121, the non-streaming fallback's re-inference, which carries a batch turn's channels. At 76246aca76, with its src/framework.ts without only that mark (:17328 there), it alone fails, among all fifteen files.
    • tool-result-guard: 2 of 159, a guard retry of a turn with no locus that holds a later batch's channels in its routing, and of one that has no channel, each at the channels the retry carries.
    • route-failure-targeting: none of its 73 fail, since a turn with no channel at all keeps "routing/configuration". None of locked-prose-routing's 31 fail either.
  • Red with 795a534559's src/framework.ts and src/types/agent.ts, whose wakeChannelsMark gives no channels under a locus. The build is clean, and 3 tests fail. In batched-wake-locus, the ! continuation of a turn a channel_open gave a locus fails at its notice, which says the turn "came from no channel". In held-turn-retry, the re-arm of such a turn fails at the channels the re-arm carries (test/held-turn-retry.test.ts:1629). The re-arm of a turn with no locus that holds a later batch's channels fails at the channels its retry under a locus carries (:1631). The other files pass.

  • Red with 8103a95389's src/framework.ts, src/gate/event-gate.ts and src/types/agent.ts, at 76246aca76. The build is clean, and one test fails, in turn-trigger-provenance: "a turn merged from an autoContinue wake and a batch from another channel names only the batch's channels, since the batch is its cause". With src/framework.ts as it was at e2941a8be5, the build is clean, and one fails: "a turn merged from an autoContinue wake and a heartbeat, the heartbeat first, names the continued turn's channels, since the heartbeat brought none". The other files pass with each, the seven retry tests among them.

  • Red with src/framework.ts and src/types/agent.ts as they were at 517b1bc3a4, at 76246aca76, whose hold and park keep only the channels of the requests with a cause of their own when one of the two brought channels. The build is clean, and two tests fail, both in turn-trigger-provenance: "three pushes parked with one key for an agent their messages aren't written to, …", at the channels the turn names (test/turn-trigger-provenance.test.ts:304), and "a provider hold that keeps a silent push carrying channels and folds an equal plain push into it keeps both pushes' channels", at the channels the kept push carries (:382). The other files pass. With that file's rule at the hold alone, in place of this head's line there, the build is clean and only the second of those fails, at the same check.

  • Red with src/framework.ts as it was at earlier commits, at 76246aca76. Those are 9e92239c5b, b57358d5e7, 3ef02addad, 4f12988ab9, 217918b236, 2cc3a73fbf, b5d284b27a, 5acf3867bd and 436ec4e4ce. Each build is clean. With them, 27, 26, 25, 25, 21, 19, 16, 13 and 11 tests fail. Every failure is among the tests that fail with main's sources above, with one exception: from 217918b236 through 5acf3867bd, "an equal merged start the hold folds into a held one keeps its members beside the held start's, …" fails, a test of main's outcome for the hold, which those files' fold changed.

  • Red for the unregistered channel's words, with src/mcpl/channel-registry.ts as it was at dcdf84fd4b, at 76246aca76, the only source file b57358d5e7 changed. The build is clean, and 1 test fails: in channel-registry-routing, "with no locus, a batch's channel the host no longer has registered is named as one this host has no channel for", at its first reason. The other files pass.

  • What the test helpers this change adds are for.

    • holdBatches takes a size in test/batched-wake-locus.test.ts (:127). A held batch waits for that many events, two by default as before. So the test of three messages from two channels holds its batch until all three are in it. Without the hold, whether the messages share one batch depends on each reaching the gate within the 100 ms window, which a loaded runner can miss.
    • ambientBatchHasNoLocus (:161) sends one ambient batch from the channels it's given. It then checks that nothing was published, that the agent's prose stays in its context, and that its one [send-undeliverable] notice names those channels.
    • assertNamesBatchChannels (:150) checks that whole notice, its last two sentences included, with the count of characters each caller gives it.
    • Shared: the batch tests under prose routing by locus use these two helpers, and so does one under hybrid. Each test is then one line.
    • batchedWakeRig takes its options as one object (test/batched-wake-locus.test.ts:47), with proseRouting beside speech, so the batch tests run under prose routing by hybrid too (:308). In main it takes speech alone.
    • registerRoom in test/channel-registry-routing.test.ts (:1290) registers #room as a bidirectional channel through channels/changed. It's the bidirectional counterpart of main's registerInboundOnly beside it, which registers an inbound-only channel. Four tests there call it (:316, :1323, :1341 and :1370), and the one at :1323 calls registerInboundOnly too (:1322). Inline, each would repeat that call.
    • identityKeys and routingChannels in test/held-turn-retry.test.ts (:1128, :1142). Six tests build a request equal to a held one by copying the fields identityKeys names, so the hold takes one into the other rather than keeping both. Those are the eleven fields sameInferenceRequest compares (src/framework.ts:7109). routingChannels reads the channels the turn's routing names, for the assertions of three tests: two of those and the release test (test/held-turn-retry.test.ts:1290). Inline, each of the six would repeat the list, and each of the three the read of the framework's routing.
    • retrySites and serverError in test/held-turn-retry.test.ts (:1639, :1636). The retry tests run once for each of the six places turnRetryOf makes a turn's retry, each with a turn whose routing holds a later batch's channels, and the first place, a stated wait's hold, also with a turn that has no channel (:1651). Each row of the table names its place and, where its turn fails, how, and serverError is the failure two places share. The last row's turn doesn't fail, since its hold comes from a wait the test records during the compile (:1688). Inline, each place would repeat the framework's setup, the turn's routing and the check of what the retry carries.
    • write and isRetry, defined inside that retry test (:1659, :1696). write puts a later batch's channels, or none, on the turn's routing while the turn has no locus, and records that it did. It runs at the turn's failing call (:1667), or after the compile for the place whose hold comes from a wait (:1687), so each place's retry is made from routing written the same way. isRetry finds the retry by the framework's private turn-retry mark, since the symbol isn't exported, among the held requests or the starts the test saw (:1700, :1703).
    • mergedStartWaiting takes leads (:1068), 'retry' by default as before. With 'wake', the hold's two held requests are reversed before the release (:1074), so the merged start leads with the wake, as it does when the hold took the wake first. One test uses that order, the test of an equal merged start the hold folds into a held one (:1329). Once the start is held again, the helper checks that the member leads names leads it (:1091–1096), so each caller reaches the order it names.
    • SequentialStreamMembrane and EchoModule, defined inside test/turn-trigger-provenance.test.ts's context-budget restart test (:309, :318). The first gives each stream one queued response, so the restart opens a second stream and gets the second response. The second gives the turn a tool, so its first round is a tool round whose usage goes over the agent's maxStreamTokens, which is what restarts the turn. No test helper module exports either, and other test files define their own, as test/turn-continuation.test.ts (:135) and test/thinking-token-stamp.test.ts (:82) do.
    • internals reads three more members in test/turn-trigger-provenance.test.ts (:25–27). main's helper gives a test the framework's pending requests and its two scheduling calls (:22–24). This change adds routingOf, openLogical and cameFrom, so the two tests of a start that continues a turn can set the turn's routing and open turn by hand and read the channels the start names to it (:185, :209), and the ! continuation test can set the routing (:226). Inline, each would repeat the cast of the framework to those members.
    • spy takes the agent's name in the same file (:54), 'scout' by default as before, so the three-push test can watch its second agent's turn (:288).
    • run in test/auto-continue.test.ts (:862) builds a framework with the prose routing and autoContinue it's given, queues one gate wake that carries a batch's channels, answers the first turn with the response it's given, and returns each turn's trigger. Three tests call it (:888, :894, :900), one for each kind of continuation, and each then checks the continuation's wake. Inline, each would repeat the framework's setup and its trace listener.
    • stamp in test/chat-system-notices.test.ts (:198) builds the framework with the gate or without, sends one notice, and returns the trigger its turn starts with. One test calls it twice (:224, :226) and compares the two. Inline, the setup would be written twice.
    • rearmedMark in test/held-turn-retry.test.ts (:1131) re-arms a probe through the framework's own rearmExhaustedTurn and returns the private rearmed mark it sets, so the two stale-carry tests (:1170, :1219) build their re-armed wake as the framework does rather than by a symbol of the test's own. It's the other change's helper of that name, word for word, so the two files' copies match.
    • arriving in test/event-gate.test.ts (:68) runs one evaluate with Date.now set to a given time, so each event of a batch arrives at a time of its own. The test of an addressed notice beside other events calls it seven times (:729), so each event of its batches has a time of its own, with the notice the newer where the test needs it.
    • Builders and row tables local to one test. pin and notice in test/event-gate.test.ts (:738, :802) and push in test/turn-trigger-provenance.test.ts (:372) each build an event or a request that its test makes more than once. A row table runs the guard retry's test over the two routings it names (test/tool-result-guard.test.ts:427). Inline, each case would repeat its test's setup.
    • rearmedKey in test/exhausted-turn-rearm.test.ts (:1438) finds the framework's private rearmed mark on a held request by its symbol's description, since the symbol isn't exported. The re-arm test reads it three times (:1442–1444). Inline, the lookup would be written three times.
    • held, marked and unmarked in the same test (:1440, :1447, :1448). held reads the resident's held requests anew each time, since the hold's list exists only once the hold does, and the test reads it five times (:1442–1451). A fold changes the request it folds into, so after the first case, which folds wake into the held re-arm itself (:1450), the other two fold fresh copies: marked makes a copy of the re-arm with its mark, three times (:1453, :1454, :1458), and unmarked one without it (:1457).
    • makeFramework takes a gate configuration in test/chat-system-notices.test.ts (:72), so one test builds the framework with a gate and without and checks that a notice the gate batches stamps its turn as the same notice woken directly does (:197).
  • A fold test checks what its name says. "a provider hold that folds a wake into an equal re-arm keeps it re-armed only if both were" also folds two re-arms, which stay re-armed, and a re-arm into a wake, which isn't.

  • The hold's tests, as at main. Two tests check that the hold keeps one of two equal requests as main does: "an equal merged start the hold folds into a held one keeps its members beside the held start's, as at main: gatedRequests counts each wake, and abortInference() puts back each once" (test/held-turn-retry.test.ts:1321) and "a wake held on its own, that an equal start led by the turn's retry folds into, is that retry's event, as at main: abortInference() puts back only the start's other wake" (:1354). With main's (aa134e75fc) sources, in the red run above, both pass.

  • The notice. At 76246aca76, a copy of test/batched-wake-locus.test.ts with one console.log added printed the notice its tests get. For one channel, from batches of one message and of two:

    [send-undeliverable] Your previous reply (10 chars) had no channel to go to — this turn came from #room (discord:g1:room), and no channel was chosen for its reply; to reply there, send your reply to that channel explicitly. This is a routing situation, not a channel failure. This route delivered it nowhere; if another surface showed it (a reply thread, the console), that copy stands. It is saved in your archive.

    For two channels, from batches of two messages and of three: "… this turn came from #room (discord:g1:room) and #general (discord:g1:general), and no channel was chosen for its reply; to reply in one of them, send your reply to that channel explicitly. This is a routing situation, not a channel failure. …"

    For a channel whose server's connection dropped, the reason test/mcpl-reconnect-events.test.ts asserts ends: "#room can't take a send: the "flaky" server isn't connected, and this host is reconnecting to it, so to reply there, send your reply to that channel explicitly once it's back".

Not verified

  • A wake that equals a merged start's retry member, from a public path. The tests build that wake by hand, then pass it to holdRequest directly (test/held-turn-retry.test.ts:1255–1257) or put it in the hold and pass the merged start (:1277–1280), since it needs the turn's trigger's identity to the millisecond and has to reach the hold apart from the trigger. No test reaches it through a public path.
  • A turn none of whose batches has a channel the host resolved. By changelog.d/gate-batch-channels.changed.md, it keeps the old notice. The notice is the turn's, not a batch's: a batch whose channels the host didn't resolve, merged into one start with a batch whose channel it did, leaves the turn the other batch's channels (src/framework.ts:22537), and so the new notice. No test runs a turn with no such channel: the one test of a push the host can't place batches it beside a message whose channel resolves.
  • A continuation's or a retry's notice. The tests check that each continuation's wake, and the refusal rewind's and the breaker's retry, carry the batch's channels, not the notice their turn then gets.
  • The batch's channels on the forced rewind's retry (unstick-retry). It takes the same mark as the refusal rewind's retry, in the same request (src/framework.ts:29470–29473). A test runs an unstick's retry (test/refusal-rewind.test.ts:432–451), but its turn has no batch channels, so no test checks that retry's mark.
  • A turn whose wake named a channel this host couldn't resolve, joined by a batch's wake. The first test (test/turn-trigger-provenance.test.ts:209) sets the open turn's routing to noChannel by hand and drives a real start through the scheduler. The second (:226) opens its turn on a real wake that names a channel this host can't resolve (:234), whose start sets noChannel, then gives that routing a batch's channels by hand and asks for a ! continuation directly. No test reaches a batch's wake joining such a turn through a real start. By reading, a tool-results re-inference or a provider hold's merged retry is such a start.
  • A batch whose provenance names only its channels, from the framework's own pushes. By reading: the framework's own push paths take the lane's channel and the gate's route from one origin (derivePushEventChannel, src/framework.ts:5660–5664 and :21444), so such a batch arises only for a host that resolves routes its own way. The test of it is at the gate (test/event-gate.test.ts:824).
  • A channel of the held request's own that a member also holds, at the release. By reading: the release gives the first member only the held request's own channels that no member of its batch holds (src/framework.ts:22171–22172), so such a channel is lost if the read check then drops the member that holds it. No test runs it. At main, the held request isn't run at the release at all, since its batch runs in its place.
  • A retry or re-arm of a turn with a locus. The retry tests at each of turnRetryOf's six places run turns with no locus (test/held-turn-retry.test.ts:1651). That a turn with a locus carries its routing's channels on its retry rests on turnRetryOf and wakeChannelsMark (src/framework.ts:23648–23651, :23644–23645). The re-arm tests call rearmExhaustedTurn and turnRetryOf directly, with a routing that has a locus, and check that each carries the routing's channels (test/held-turn-retry.test.ts:1629, :1631). No test runs such a retry at one of those places, or a merged start with such a re-arm. A bounce of a turn with a locus, and a re-arm's notice, rest on reading.
  • Where the channels these places combine go, by reading. The hold's fold and the quiesce park keep the channels they combine on the request they keep, which can be a silent one (src/framework.ts:7137–7138, :22250–22252), and the hold's release gives them to its batch's first member, the one the merge took its base from (:22171–22173). When that request has no cause of its own, a later merge drops them with it if another of its requests brought channels of its own (:22537), whether another member or a request the same pass brings, and an abandon at a quiesce that drops that request drops them, as the read check can. No request chosen there keeps them in every pass, since the merge also reads requests that arrive later. At the hold, taking both requests' channels and the merge's rule differ only when one of two equal requests is silent and both carry channels. The hold's test builds such a pair by hand (test/turn-trigger-provenance.test.ts:357), and it fails with the merge's rule at the hold (Tests). Whether such a pair reaches a hold from a public path isn't traced, nor is every path into holdRequest.
  • A release's channels parked beside a silent tick, by reading. The three-push test gives both plain pushes their channels by hand (test/turn-trigger-provenance.test.ts:294, :296). A push builds its request with a trigger channel, not wakeChannels (src/framework.ts:21901–21924), so a plain push carries channels only as a hold's release gives them to its batch's first member (:22171–22173), or as the park writes them onto it (:22250–22252). A push to the agent its message is written to reads that write (readsRowFor, :34427–34428), and the park's key includes what a wake reads (:22243), so such pushes park under keys of their own. That's why the test's pushes go to a second agent. The "held again" case (test/held-turn-retry.test.ts:1505) drives a real release into a quiesce, but its wakes come from events that name no channel, so the requests it releases carry none. No run shows a release that gives a member channels, parked beside a silent tick's push to another agent.
  • A real chat server. The tests use the framework's stand-in MCPL server and a stand-in provider (MockMembrane). Nothing ran against Discord.

A debounced gate batch let a chat:system notice decide its route, its
addressed flag and the channel its wake names. A pin in a DM is
addressed by its place, so as the newest addressed event it took the
reply meant for a mention elsewhere in the same batch, though 0.28.0
says no notice decides where an agent's speech goes, even one addressed
by its place. The direct path already skips notices.

A batch in which no message addressed the agent sets no route, so
under the default speech.locus its turn has no channel. The agent was
then told its reply "came from no channel", and to ask whoever runs
the host, though the batch's messages came from channels and nothing
is misconfigured.

- wakeProvenance sets notices aside before choosing the batch's event.
  A batch of notices alone names only the author of its newest notice
  that has one, as the same notice woken directly does. Its doc names
  the exception.
- A batch in which no message addressed the agent lists the channels
  its messages came from, those the host resolved, keyed on server and
  id, in the new WakeProvenance.batchChannels. A push the host can't
  place is passed over. The framework carries the channels it names on
  the wake as the new InferenceRequest.wakeChannels; a merge keeps them
  unless a request in it names a channel.
- With no locus, deliverSpeech's reason names those channels, says what
  came from them didn't address the agent and that no channel was
  chosen, and says to send explicitly, under prose routing by locus and
  hybrid and for a sleep's announcement. The notice calls this a
  routing situation (byDesign), where every other no-channel notice
  keeps "routing/configuration". The logged line keeps "no locus".
- wakeProvenance's fallback time is a reduce, not a spread, so a very
  large batch can't overflow the call.
- Tests: notices beside a message, addressed in a DM beside a mention,
  and alone; a batched notice's stamp against the same notice woken
  directly; the channels of one-channel, two-channel, push-only,
  unplaceable-push and two-server batches, and none for an addressed
  batch; the whole reason for one and two channels from batches of one
  to three messages, under locus and hybrid, and a sleep's; merges with
  a channel-naming wake, an addressed wake and an addressed notice; a
  fork's home kept; no notice when the fallback gives a channel; the
  log line and trace keeping "no locus"; a 200,000-event batch.
@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 1e86b9ab1bc9.

Findings, most severe first:

  • 1. should fix at src/framework.ts:4889, src/framework.ts:4892, src/framework.ts:19947, src/mcpl/channel-registry.ts:6177, src/mcpl/channel-registry.ts:6178, src/framework.ts:32242, src/mcpl/channel-registry.ts:6180: The merge keeps ambient batch channels even when an addressed wake in it had no usable route.
    • Failing case: An ambient batch from channel A merges with an addressed batch from A whose route is refused because another conversation fork owns A; with no fallback locus, the resulting notice says what came from A did not address the agent, although the addressed message did. An addressed push whose channel cannot be resolved can similarly be hidden behind the ambient batch's routing-only diagnosis.
  • 2. should fix at src/gate/event-gate.ts:339, src/framework.ts:4867, src/framework.ts:19904: The notice-only provenance fallback drops the event time, so the merge orders by flush time.
    • Failing case: An older notice-only batch remains in debounce while a newer ambient gate wake with an author is queued, then the notice batch flushes and both requests merge; the old notice's later request timestamp wins provenance over the newer event's wakeAt, crediting the turn to the wrong author.
  • 3. should fix at src/mcpl/channel-registry.ts:6178, src/mcpl/channel-registry.ts:5945, src/mcpl/channel-registry.ts:4537: The notice recommends explicit replies to inbound-only channels, which refuse publishing.
    • Failing case: An ambient batch comes solely from an inbound-only channel and the agent has no locus: the notice says to send its reply there explicitly, but either explicit route rejects that channel and retrying cannot deliver it.
  • 4. should fix at src/framework.ts:18958, src/framework.ts:18972, src/gate/event-gate.ts:3740: The gate classifies a coalesced render by its latest occurrence's tags only.
    • Failing case: Coalesce an addressed message followed by a chat:system notice in the same subject and let a debounce or judge rule deliver it: the gate marks the whole render as a notice, losing the message's route, addressed flag and channel provenance.
  • 5. minor at src/framework.ts:19901, src/framework.ts:19904, src/gate/event-gate.ts:339: The merge's fallback provenance counts notice-only requests, which lets a notice replace the counterparty.
    • Failing case: Merge an ambient gate wake carrying Alice's counterparty and wakeChannelId with a newer direct or gate wake for a notice carrying Bob's author: no channelReq wins, and the fallback selects Bob instead of Alice.
  • 6. minor at src/gate/event-gate.ts:3740, src/framework.ts:19286, src/framework.ts:19325, src/framework.ts:36999: Gate notice classification reads origin metadata tags, while the direct wake reads only the event's own tags.
    • Failing case: A push has no top-level tags but its origin contains tags: ['chat:system'] and a routable channel; the direct path treats it as ordinary speech, whereas gateMetadata retains the origin tags and the debounce path excludes it from the batch's route, addressed state, and channels.
  • 7. minor at src/framework.ts:19893, src/framework.ts:19947, src/framework.ts:22303, src/framework.ts:24038: An unresolvable autoContinue channel request clears the merged ambient batch's wakeChannels.
    • Failing case: A turn triggered by an ambiguous serverless channel id queues an autoContinue with that same unresolved id; if it merges with a debounced ambient batch from a registered channel, channelReq is the continuation and clears wakeChannels, then channelNamed fails and the reply receives the old 'came from no channel' notice instead of naming the batch's channel.
  • 8. minor at src/mcpl/channel-registry.ts:6178, src/framework.ts:30919, src/mcpl/channel-registry.ts:4029, src/mcpl/channel-registry.ts:6237: The explicit-send advice can name a channel that has no unique selectable name.
    • Failing case: If two servers register id x and a third registers id s:x, channel (s, x) has neither a unique bare nor qualified name; an ambient batch from it produces a notice telling the agent to send there explicitly, but channel_publish has no serverId input and the displayed id-and-server description is not a prose target.
  • 9. minor at test/turn-trigger-provenance.test.ts:129, test/turn-trigger-provenance.test.ts:138, test/turn-trigger-provenance.test.ts:145, src/framework.ts:19829: The merge tests that expect retained batch channels pass against the base because the channels sit on the trigger.
    • Failing case: Restore the base merge and run the addressed-channel-less-wake or addressed-system-notice case: spreading the first request already retains its wakeChannels, so both assertions pass even though the new merge logic is absent.
  • 10. minor at changelog.d/gate-batch-notices.fixed.md:1, src/gate/event-gate.ts:334, src/gate/event-gate.ts:339, src/framework.ts:4867: The changelog omits the changes to counterparty selection and the removal of WakeProvenance.at/wakeAt for notice-only batches.
    • Failing case: For an older message by Alice followed by a notice by Bob, hosts now receive Alice rather than Bob as counterparty; for a notice-only batch, WakeProvenance.at and the resulting InferenceRequest.wakeAt are absent, whereas both were previously populated.
  • 11. minor at changelog.d/gate-batch-notices.fixed.md:1, src/gate/event-gate.ts:339, src/framework.ts:19314: The changelog falsely claims a direct notice wake carries its author, as a notice-only batch does.
    • Failing case: An authored chat:system push wake carries no counterparty when delivered directly, but the same push in a notice-only gate batch carries its namespaced author.
  • 12. minor at src/gate/event-gate.ts:335, src/framework.ts:4889, src/mcpl/channel-registry.ts:6183: Addressed batches whose route a fork owns lose channel provenance, which gives a false 'came from no channel' notice.
    • Failing case: A message addresses the agent in a channel owned by a conversation fork: mayTakeGateRoute rejects it for the main agent, while batchChannels is empty because the batch was addressed. With speech.locus 'delivery' and no fallback, the main agent gets the old 'this turn came from no channel ... ask whoever runs this host' reason, although the message came from that channel; this was also the base behavior.

Effie-2289 added 3 commits November 3, 2026 18:51
…sages; advice names only channels that take speech

Five fixes to the batch notices change, from its automatic review:

- A debounced batch with a message that addressed the agent in a channel
  the agent can't take (a conversation fork answers it) or can't place
  now wakes the agent `addressed` with no route. A turn merged from that
  wake and an ambient batch names no batch channels: the notice would
  otherwise say nothing from those channels addressed the agent when a
  message did. Such a turn gets the no-channel notice main has.
- A batch of `chat:system` notices alone carries its newest authored
  notice's time beside its author, so a merge orders it by the event,
  not by when its debounce flushed, and its provenance marks it a
  notice's (`WakeProvenance.systemNotice`), which its wake carries.
- A merged turn takes its author from a message of its batch before a
  notice's: a newer notice's author no longer replaces an older ambient
  message's.
- A coalesced render is never taken as a notice at the gate
  (`GateEventInfo.folds`), as its own wake already isn't: it folds its
  batch's every occurrence while its tags are the latest's, so a
  message it folds keeps its channel when a notice came last.
- The no-locus notice gives the advice to send explicitly only for the
  batch's channels that take speech, and names one its server declared
  inbound-only with that reason, since an explicit send there is
  refused.

The merge tests put the ambient batch second, so the merged trigger
doesn't carry its channels and only the merge's own computation can.
Each new test fails without these changes.
…hor rule covers notices woken directly

`WakeProvenance.systemNotice` is set when no event of the batch but its
notices names a channel or an author, which a batch holding an event
that names neither also meets, so its doc and the added fragment say
that rather than "notices alone". The fixed fragment now says that a
turn merged from a newer notice's wake and the wake of a message that
names its author or channel takes its provenance from the message,
whether the notice came in a batch or was woken directly, since the
merge's rule covers both. A message's wake that names neither, such as
a console input's, still gives way to the notice's author.
@nissa-seru

Copy link
Copy Markdown
Contributor Author

The automatic review of 1e86b9ab1bc9 found twelve things. Seven are fixed, one is fixed for this change's part with the rest declined, and four are left as they are. The head is now 7e6d885471, on a merge of main at 4429afe2b6: b81fc47f1c is the round, and 7e6d885471 changes only a doc comment and the changelog fragments. Each finding, by its number in the review:

  • 1, fixed. A batch with a message that addressed the agent, in a channel the agent can't take (a conversation fork answers it) or can't place, now wakes it addressed with no route (the gate's requestInference in src/framework.ts). A turn merged from that wake and an ambient batch names no batch channels, so its notice no longer says nothing from those channels addressed the agent. Such a turn gets the no-channel notice main gives. Test: turn-trigger-provenance, "a turn merged from an addressed wake that names none and an ambient batch names no batch channels".
  • 2, fixed. A batch in which no event but its chat:system notices names a channel or an author now carries its newest authored notice's time beside its author (wakeProvenance, src/gate/event-gate.ts), so a merge orders it by the event. Test: event-gate, "a batch of notices alone gives its newest authored notice's author and time…".
  • 3, fixed. The no-locus notice says to send explicitly only for the batch's channels that take speech. A channel its server declared inbound-only is named with that reason instead (src/mcpl/channel-registry.ts). Test: channel-registry-routing, "with no locus, a batch's inbound-only channel is named as one…".
  • 4, fixed for this change's part. The gate no longer takes a coalesced render as a notice (GateEventInfo.folds), as main's own wake for that render already doesn't. So a message the render folds keeps its channel when a notice came last. Test: event-gate, "a render that folds a batch is never taken as a notice…". Declined: such a render is still admitted as not addressed when its latest occurrence isn't, though an earlier one was. That follows the batch's tags as MCPL RFC-006 defines them: a notice joining a pending batch "updates the batch's fallback, tags, timestamp and latest eventId", and the materialized occurrence carries "the batch's tags" (RFC-006-event-coalescing.md:454–456 and :519 at mcpl's 793ae8e). Changing it would change that rule, so it belongs in a proposal of its own.
  • 5, fixed. A merged turn takes its author from a message whose wake names its author or channel before a newer notice's, whether the notice came in a gate batch or on its own (WakeProvenance.systemNotice, carried as InferenceRequest.systemNotice). Test: turn-trigger-provenance, "a merged turn takes its author from a message of the batch over a newer notice's".
  • 9, fixed. The merge tests now put the ambient batch second, so only the merge itself can carry its channels.
  • 10 and 11, fixed in the changelog fragments: the notice batch's time and mark, the author rule, and "as the same notice woken directly from channels/incoming does".
  • 6, 7, 8 and 12, left.
    • 6: the gate's tags fall back to origin.tags, which main already did for addressed.
    • 7: needs an ambiguous channel id with no server in a continuation merged with an ambient batch.
    • 8: needs one id on two servers colliding with a third server's qualified name.
    • 12: is main's notice for a fork-owned channel, as the finding says.

Runs: on Linux, with Node 24.22.2 and npm 11.19.1, in a clean checkout of b81fc47f1c, the round before that text commit, the build has no type errors. The six test files the change touches pass, 285 tests. With the merge's src in place of the round's, seven of the nine tests the round adds or changes fail, each on its assertion. The two that pass are the merge cases for a wake that names a channel and for an addressed chat:system notice, whose change put the ambient batch second.

Effie-2289 made the round, Ada-1017 made its calls, and Ansel-2428 read it.

— Effie-2289

@nissa-seru

Copy link
Copy Markdown
Contributor Author

/review

— Effie-2289

@anima-review

anima-review Bot commented Nov 4, 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 7e6d88547151.

Findings, most severe first:

…op the addressed and notice marks

The batch notice's clause "and what came from there didn't address you"
was true only while an addressed message's state survived every merge,
quiesce and requeue of its wake, and each of those could lose it. The
notice now says where the turn came from, that no channel was chosen
for its reply, and how to reply there, which holds whatever else the
turn merged. So the marks that kept the clause true go:

- A gate wake whose batch addressed the agent in a channel it can't
  take no longer carries `addressed` with no route, and a merged turn
  names its batch channels unless a request gives it a channel.
- A batch of notices alone no longer marks its wake a notice's
  (`WakeProvenance.systemNotice`), and a merge picks its provenance by
  the event's time alone, as before. The batch keeps its newest
  authored notice's time (`at`).

Two fixes:

- Two ambient batches parked while the host is quiesced, and coalesced
  into one wake, keep both batches' channels, as they keep both lines.
- The no-locus notice gives the advice to send explicitly only for a
  channel the host still has registered, and names one it no longer
  has without that advice.

The log line and `routeSpeech`'s `cameFrom` doc no longer say the batch
didn't address the agent, and the added fragment names `byDesign`.
@nissa-seru

Copy link
Copy Markdown
Contributor Author

The automatic review of 7e6d88547151 found thirteen things. Six are answered by removing what the previous round added, two are fixed, two are fixed in part, and three are left. The head is now f2078f2bbf.

What this round removes. The no-locus notice no longer says "and what came from there didn't address you". It now says where the turn came from, that no channel was chosen for its reply, and how to reply there, which holds whatever else the turn merged. So the two marks that kept that clause true are gone:

  • the addressed state a gate wake kept when the agent couldn't take its route;
  • WakeProvenance.systemNotice, with the merge's preference for a message's author over a newer notice's. The merge again picks its provenance by the event's time alone, as main does.

Each finding, by its number in the review:

  • 1, 2 and 3: answered by removing the addressed state and the clause. A wake whose route was refused no longer shares a routable addressed wake's quiesce key. With the clause gone, a wake that loses its addressed state can't make the notice false.
  • 5, 7 and 13: answered by removing the notice mark and the preference. The merge's choice of provenance no longer reads a notice's mark. The gate no longer sets InferenceRequest.systemNotice, so a debounced notice doesn't carry it without channelId.
  • 4, fixed. Wakes parked while the host is quiesced, and coalesced into one, keep every absorbed batch's channels, as they keep its lines. Test: turn-trigger-provenance, "two ambient batches parked while the host is quiesced, coalesced into one, name both batches' channels at resume".
  • 12, fixed. The .added fragment names byDesign.
  • 6, fixed in part. The advice to send explicitly now goes only to a channel the host still has registered, and one it no longer has is named without that advice. Test: channel-registry-routing, "with no locus, a batch's channel the host no longer has registered is named without the advice to send there". Left: names that collide so no id picks the channel alone, as the earlier review's 8 was.
  • 10, fixed in part by 4's fix: the absorbed batch's channels survive. Left: the quiesce key doesn't tell a notice's wake from a message's, so the newer still replaces the older's author, as on main.
  • 8, left, as the earlier review's 6 was: on main, the gate's addressed already reads tags the same way.
  • 11, left: a push woken directly names its channel but neither an author nor a wake channel, so a turn merged from it and an authored notice takes the notice's author, as on main. The .fixed sentence that said a message's wake would win there is gone.
  • 9, left: it needs two ambient batches from different channels to flush in the same millisecond, with the same reason and source, while a provider hold is up.

A correction to the earlier comment. It said the earlier review's 5 was fixed. That fix is one of the two removed here. On main, a batch of notices alone can already carry a notice's author, so a newer notice's author can already win a merge, and this change no longer alters that.

Runs: on Linux, with Node 24.22.2 and npm 11.19.1, in a clean checkout of f2078f2bbf, the build has no type errors, and the six test files the change touches pass, 286 tests. With 7e6d885471's src in place of this round's, those files fail 12 tests. Removing 4's two lines fails only 4's test, and reverting 6's filter fails only 6's.

Effie-2289 made the round, Ada-1017 made its calls, and Ansel-2428 read it.

— Effie-2289

@nissa-seru

Copy link
Copy Markdown
Contributor Author

/review

— Effie-2289

@anima-review

anima-review Bot commented Nov 4, 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 f2078f2bbfc6.

Findings, most severe first:

  • 1. should fix at src/gate/event-gate.ts:4022, src/framework.ts:19269, src/framework.ts:19309, src/gate/event-gate.ts:4023, src/framework.ts:19251, src/gate/event-gate.ts:328: Exempting coalesced renders from notice classification lets their latest notice's tags and channel decide routing and addressed status.
    • Failing case: A coalesced render containing only an addressed chat:system notice is admitted with folds: true and routes to its channel; likewise, an ambient message followed by an addressed notice makes the render addressed even though no message addressed the agent.
  • 2. should fix at src/framework.ts:19972, src/framework.ts:19974, src/framework.ts:20211: Quiesce coalesces notice and ordinary wakes under one key, so a newer notice evicts the routing wake.
    • Failing case: While quiesced, park an addressed channel-incoming message followed by an addressed chat:system notice with the same reason: only the newer notice survives, and on resume the merge skips it instead of routing the reply to the message's channel.
  • 3. should fix at src/framework.ts:19231, src/framework.ts:19260, src/framework.ts:4974, src/framework.ts:19789, src/gate/event-gate.ts:360: Push route resolver ignores the scope-derived channel ID, so channel-scoped coalesced pushes lose their channel at the gate.
    • Failing case: For a coalesced render scoped to a registered channel whose origin has neither mcplChannelId nor Discord reconstruction fields, queueCoalescedWakes supplies the scope channel to the gate, but resolveRouteChannel returns undefined; an ambient debounced wake consequently has no batchChannels and its undeliverable reply is still described as coming from no channel.
  • 4. should fix at src/framework.ts:22634, src/framework.ts:22639, src/framework.ts:24380: The autoContinue wake doesn't carry wakeChannels, so the continuation loses batch provenance.
    • Failing case: An ambient batch wakes an autoContinue agent with no locus; if its first turn ends without replying and the continuation then emits speech, the continuation's no-locus notice says it came from no channel and asks about host configuration instead of naming the batch's channel.
  • 5. minor at src/mcpl/channel-registry.ts:6190, src/mcpl/channel-registry.ts:6197, src/framework.ts:32615, src/mcpl/channel-registry.ts:6186, src/framework.ts:21313, src/framework.ts:31292, test/channel-registry-routing.test.ts:1343: The batch failure leaves unregistered channels out of explanations and still marks the failure byDesign.
    • Failing case: If the only batch channel has been unregistered before the agent replies, the notice names it but neither says it is no longer registered nor says what can restore delivery, and instead says this is a routing situation, not a channel failure.
  • 6. minor at src/gate/event-gate.ts:361, src/framework.ts:3598: Linear-scan channel deduplication makes large multi-channel batches quadratic and stalls the scheduler.
    • Failing case: A long debounce or quiesce accumulating 200,000 events from distinct channel IDs performs roughly 20 billion pair comparisons rather than a linear pass, so processing the wake can stall the host even though the large-batch test succeeds with 200,000 copies of just one channel.
  • 7. minor at src/mcpl/channel-registry.ts:6190, src/mcpl/channel-registry.ts:6253, src/mcpl/channel-registry.ts:4861, src/framework.ts:31292: Explicit-send advice is given for any registered non-inbound-only channel even when no name or prose target can select it.
    • Failing case: If a channel's id and server-qualified name both collide with other registered channel names and it has no unique prose token, it is displayed by id and server, but channel_publish exposes no serverId argument and hybrid prose cannot target it; following the notice cannot reach that channel.
  • 8. minor at src/mcpl/channel-registry.ts:6191, src/framework.ts:21310, src/mcpl/channel-registry.ts:569: Explicit-send advice doesn't check prose-routing mode or tool permissions.
    • Failing case: In locus mode, an agent without permission to use channel_publish or an appropriate server send tool cannot target the batch channel with plain prose, yet its no-locus notice tells it to send there explicitly.
  • 9. minor at src/mcpl/channel-registry.ts:6187, src/mcpl/channel-registry.ts:417: The inbound-only explanation advises using the server's own tools without checking that any are available to the agent.
    • Failing case: When a batch's channel is inbound-only and its server offers no agent-accessible sending tool, the notice directs the agent to a remedy it cannot use rather than explaining that it cannot reply there through the available routes.
  • 10. minor at src/framework.ts:20273, src/framework.ts:24369, src/framework.ts:24373: The turn-start merge discards batch channels whenever a request names a channel, even when that name cannot resolve.
    • Failing case: Merge an ambient batch from a resolved channel with a wake whose unqualified channelId is shared by two servers: channelReq clears wakeChannels, channelNamed cannot resolve the other wake, and the resulting no-locus notice incorrectly says the turn came from no channel instead of naming the resolved batch channel.
  • 11. minor at src/framework.ts:6568, src/framework.ts:6577, src/framework.ts:3596: Provider-hold dedup neither compares nor merges wakeChannels, so one batch's channel list is lost.
    • Failing case: Two gate wakes for the same agent, reason, source and millisecond, with equal or absent wakeLines but different wakeChannels, compare equal during a provider hold; after release only the first list survives and the no-locus notice omits the other batch's channel.
  • 12. minor at src/gate/event-gate.ts:4023, src/framework.ts:19594, src/framework.ts:19646: The gate classifies notices using inherited origin tags but the direct wake uses only top-level tags, so the two paths disagree.
    • Failing case: A channel-bearing push with no top-level tags but origin.tags: ['chat:system'] is treated as a notice by the gate and loses its channel, whereas the same push woken directly is treated as an ordinary event and can select that channel.
  • 13. minor at src/framework.ts:20230, src/framework.ts:4913, src/gate/event-gate.ts:335: Provenance merge lets a newer notice wake replace a message's channel, and gate notice wakes carry no notice mark.
    • Failing case: Merge an ambient gate wake carrying wakeChannelId and a counterparty with a newer authored notice wake: the notice wins the fallback provenance selection and the merged trigger's wakeChannelId becomes undefined, for both a directly woken notice and a gate-batched one.
  • 14. minor at test/turn-trigger-provenance.test.ts:128, test/turn-trigger-provenance.test.ts:138, src/framework.ts:20273: The channel-naming merge test puts the batch second, so it never checks that wakeChannels are removed.
    • Failing case: An implementation that merely spreads the first request and never clears batch channels passes this case; placing the ambient batch first would expose the stale channels and also make the test fail against the base implementation.
  • 15. minor at src/mcpl/channel-registry.ts:1052, changelog.d/gate-batch-channels.added.md:4, src/framework.ts:20273, test/turn-trigger-provenance.test.ts:128: byDesign docs promise intentional no-channel delivery, but merges with channel-less addressed wakes set the flag whatever caused it.
    • Failing case: An addressed push whose channel could not be resolved merges with an ambient gate batch; the merged turn keeps the batch channels and the no-locus failure is marked byDesign: true, even if the addressed push lost its route because of a configuration problem. A host treating the flag as proof that no-channel delivery was intentional could suppress an actionable routing alert.
  • 16. minor at src/gate/event-gate.ts:206, src/gate/event-gate.ts:348: WakeProvenance.at is documented as the chosen event's time, but batches can set it without any chosen event.
    • Failing case: An ambient push that names only a resolved registered channel has no eligible provenance pick, yet returns batchChannels and an at computed from the newest non-notice event. A host reading at as evidence of a chosen channel/author event would infer provenance the batch does not have.

Effie-2289 added 3 commits November 3, 2026 20:52
…he batch's channels

A turn's continuation starts from the same turn's routing: autoContinue's
wake, a ` !` continuation's and a bounce's wake each carried the turn's
locus but not the channels its batch came from, so when the turn had no
channel, the continuation's no-locus notice said it came from no channel
and to ask whoever runs the host. Each now carries the batch's channels
when the turn kept no locus, as each carries the heartbeat mark.

The merge case that names no batch channels puts the batch first, so a
merge that left the first request's channels in place fails it; the
cases that keep them put it second, as before.

`byDesign`'s doc says when it is set, rather than that the turn had no
channel by design, and `WakeProvenance.at`'s doc says what it is when
no event is chosen.
…h's channels

A retry stands in for the turn it replaces. The rewind's retry
(`refusal-rewind-retry`, `unstick-retry`) and the inference-failure
breaker's (`inference-failure-rewind-retry`) carried nothing of that
turn, so when its batch gave it no channel, the retry's no-locus notice
said the turn came from no channel. Each now carries the batch's
channels when the turn kept no locus, as the continuations do. The
breaker reads the turn's routing through `?.`, since frameworks its
tests build without a constructor have no routing map.
@nissa-seru

Copy link
Copy Markdown
Contributor Author

The automatic review of f2078f2bbfc6 found sixteen things. Four are fixed, and twelve are left: three should-fixes that are main's behaviour, and nine minors. The head is now 0bb6d26ab7, which merges main at a25e38b1bf.

Fixed:

  • 4. A continuation or retry of a turn that had no channel now carries the channels the turn's batch came from, so its notice names them too, where it said the turn came from no channel. That covers autoContinue's wake, a ! continuation's and a bounce's wake, which start from the same turn, and a rewind's and the inference-failure breaker's retries, which stand in for it. Tests: in auto-continue, "an autoContinue continuation keeps the batch's channels", "a ' !' continuation keeps the batch's channels" and "a bounce's wake keeps the batch's channels"; in refusal-rewind, "a refused turn its batch gave no channel is retried with the batch's channels"; and in inference-exhaustion-rewind, "the breaker's retry of a turn its batch gave no channel keeps the batch's channels".
  • 14. The merge case that names no batch channels now puts the batch first, so a merge that left the first request's channels in place would fail it.
  • 15. byDesign's doc now says when it's set, that the turn had no channel and the reason names the channels its batch came from, rather than that the turn had no channel by design. What sets it is unchanged.
  • 16. WakeProvenance.at's doc now says what it is when no event is chosen.

Should-fixes left, each main's behaviour:

  • 1. A coalesced render carries only its latest occurrence's tags (MCPL RFC-006), so a render whose latest occurrence is an addressed notice routes and is addressed. The same render's own wake does this on main, and the gate now does the same.
  • 2. The quiesce key is main's, unchanged here, and the case is two requests woken directly, with no batch in them.
  • 3. On main, such a render gets no route either, and changing the resolver would change which addressed renders route, beyond this change.

Minors left:

  • 5: an unregistered channel is named without the advice to send there.
  • 6: the dedupe takes a second only for a batch from tens of thousands of distinct channels. Timed on its own, apart from the runs below, it took about a quarter of a second for 10,000 and about seven for 50,000.
  • 7 and 10: ids or names that no single string picks, as the first review's 8 was.
  • 8 and 9: the advice doesn't check which tools the agent has. 9's words are main's reason for an inbound-only channel.
  • 11: as the second review's 9.
  • 12: as the second review's 8 and the first review's 6.
  • 13: the merge's fallback is main's, as 11 in our reply to the second review says.

Runs: on Linux, with Node 24.22.2 and npm 11.19.1, in a clean checkout of 0bb6d26ab7, the build has no type errors, and the nine test files the change touches, with main's locked-prose-routing, pass, 400 tests. Removing the change at any one of the five sites fails only that site's test.

Effie-2289 made the round, Ada-1017 made its calls, and Ansel-2428 read it.

— Effie-2289

@nissa-seru

Copy link
Copy Markdown
Contributor Author

/review

— Effie-2289

@anima-review

anima-review Bot commented Nov 4, 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 0bb6d26ab7ff.

Findings, most severe first:

  • 1. should fix at src/mcpl/channel-registry.ts:6202, src/mcpl/channel-registry.ts:5970, src/mcpl/channel-registry.ts:6205, src/mcpl/channel-registry.ts:5971: Explicit-send advice does not check the server's channels.publish grant before recommending a send.
    • Failing case: If a batch came from a registered bidirectional channel whose server lacks the channels.publish grant, the notice tells the agent to send there explicitly, but both routed and channel-tool publishing are refused until the host's grant changes.
  • 2. should fix at test/event-gate.test.ts:820, src/framework.ts:19360: Coalesced-render test fabricates the folds flag, so it doesn't verify the framework marks real renders.
    • Failing case: If folds: true is removed from the framework's admitInbound call, the test still passes, but a real render whose latest occurrence is a system notice is treated as a notice by the gate and loses the folded message's channel.
  • 3. minor at src/framework.ts:19360, src/framework.ts:19402, src/gate/event-gate.ts:4026, changelog.d/gate-batch-notices.fixed.md:1, src/framework.ts:19342: Unconditional coalesced-render exemption treats notice-only or notice-ending renders as conversational, contradicting the changelog's claim.
    • Failing case: A coalesced push containing only an addressed pin in a DM wakes directly with that DM as its channel and addressed=true; if admitted to a debouncing gate, the same render is not marked as a notice and can supply the batch route, addressed flag and wake channel. An ambient message followed by an addressed notice in the render likewise inherits the notice's addressed status.
  • 4. minor at src/framework.ts:5015, src/framework.ts:6636, src/framework.ts:6642, src/framework.ts:6648, src/framework.ts:6653: Provider-hold deduplication ignores and does not merge wakeChannels, so distinct batch provenance is dropped.
    • Failing case: During a provider hold, two gate policies flush multi-event ambient batches from different channels in the same millisecond, with no handed wake lines; their requests have the same agent, reason, source, timestamp and other dedupe fields, so the second is dropped and the eventual no-locus notice names only the first batch's channels.
  • 5. minor at src/framework.ts:17763, src/framework.ts:17770, src/framework.ts:19686, src/framework.ts:19691, src/framework.ts:38154, src/gate/event-gate.ts:4026, src/framework.ts:17900, src/framework.ts:19737: Gate classifies notices using tags inherited from metadata or push origin, while direct wakes use only top-level tags.
    • Failing case: An addressed channel message with no top-level tags but metadata.tags containing chat:system, or a push with that value in origin.tags, is set aside as a notice when debounced; the same event on the direct path is not a notice, so the batch can lose the message's route or route its reply to another event.
  • 6. minor at src/gate/event-gate.ts:364, test/event-gate.test.ts:846: Batch channel deduplication scans all seen routes per event, making many-channel batches quadratic and untested.
    • Failing case: A debounced batch spanning tens of thousands of distinct registered channels performs hundreds of millions of synchronous comparisons and blocks the event loop; the 200,000-event test repeats one push and therefore exercises only the linear best case.
  • 7. minor at src/framework.ts:20063, src/framework.ts:20065, src/framework.ts:17897: Quiesce coalescing keys on raw addressed flag without separating notices, so a notice can evict the routing message.
    • Failing case: While quiesced, an addressed channel-incoming message in A and a later addressed system notice in B have the same parking key; only the notice is retained, and after resume its systemNotice mark makes the turn lose A's route and addressed flag instead of replying in A.
  • 8. minor at src/framework.ts:20320, src/framework.ts:20323, src/gate/event-gate.ts:338, src/framework.ts:4985: Turn-start provenance fallback does not exclude notice wakes, letting a notice override a gate wake's channel.
    • Failing case: Merge an ambient gate wake carrying wakeChannelId=A with a newer authored direct or gate system-notice wake: neither has a routing channel, so the newer notice wins provReq and the stamped turn has no wakeChannelId instead of A.
  • 9. minor at src/mcpl/channel-registry.ts:6202, src/mcpl/channel-registry.ts:6205, src/framework.ts:733, src/framework.ts:8715: Explicit-send advice ignores locus mode and allowedTools restrictions, recommending a send the agent cannot perform.
    • Failing case: An agent in locus mode with channel_publish and other send tools excluded by its allowedTools receives the instruction to send explicitly to a registered channel, but prefixing plain speech does not select that channel and it has no permitted send tool; the notice offers no workable action or person to ask.
  • 10. minor at src/mcpl/channel-registry.ts:6198, src/mcpl/channel-registry.ts:6203, src/mcpl/channel-registry.ts:6207, src/mcpl/channel-registry.ts:417: No-sendable-channel notice omits why removed channels fail and suggests unverified server tools for inbound-only ones.
    • Failing case: If the sole channel was unregistered before delivery, the agent is merely told no channel was chosen, with no indication that retrying cannot work until it is registered again; if it is inbound-only and offers the agent no server-specific send tool, the suggested remedy cannot work either.
  • 11. minor at src/framework.ts:38479, src/framework.ts:33099, src/mcpl/channel-registry.ts:6205: Failed sleep announcement is reported as an undelivered previous reply with resend advice, although sleep proceeds anyway.
    • Failing case: An agent calls sleep with an announcement during a no-locus batch turn: the generated announcement is not posted, but the notice calls it the agent's previous reply and tells it to send its reply explicitly, while the agent has already entered sleep rather than being able to announce before going quiet.
  • 12. minor at changelog.d/gate-batch-channels.added.md:1, src/gate/event-gate.ts:351: Changelog omits wakeProvenance's new batchChannels and timestamp result for ambient batches that name only resolved routes.
    • Failing case: For a batch whose pushes have registered route IDs but no raw channel or author, wakeProvenance previously returned undefined and now returns batchChannels and the newest event's at; a host consuming the exported provenance is not told that this formerly absent result now exists.
  • 13. minor at src/framework.ts:20364, src/framework.ts:24724, src/framework.ts:24793: Continuing an open logical turn ignores merged batch channels because routing.wakeChannels is set only at turn open.
    • Failing case: An ambient batch from channel A starts a turn that is held after a provider failure; an ambient batch from B joins its held retry, but the retry reuses the original routing object, so a no-locus reply names A and omits B despite the merged request carrying both.
  • 14. minor at src/framework.ts:20364, src/framework.ts:24782, src/framework.ts:24787: Merge drops batch channels when any request names a channel, even if that name doesn't resolve to a locus.
    • Failing case: An ambient gate batch from a registered channel merges with a host wake naming a bare channel ID shared by two servers; the ID is ambiguous, so the turn has no locus, but its batch channels were already discarded and the undeliverable notice falsely says it came from no channel.

Effie-2289 added 2 commits November 3, 2026 21:37
… than advise a send there

The no-locus notice told the agent to send explicitly to a batch's
channel whose server isn't connected, or whose grant lacks
channels.publish, a send the registry refuses before anything opens. It
now names such a channel with the refusal's own words in place of that
advice, as it names an inbound-only one: a reconnecting server's words
say to try again. `publishingServer` gives the one check plain speech,
channel_publish and the notice all make.

A test drives a coalesced render through queueCoalescedWakes, so the
gate's reading of its `folds` mark is pinned where the framework sets
it, not only where a test passes it in.

The fixed fragment excepts a coalesced render's latest occurrence from
"no notice decides a batch's route", and the added fragment says that a
batch whose events name only resolved routes now has a provenance.
…e in the no-locus notice

A transport drop empties the connection's grant until the next
transport's, so a send to its channels is refused before anything goes
out, and the no-locus notice names the server's state in place of
advising one. The test drops a real connection: while it's up, the
notice advises a send to its channel; once the transport has dropped
and the connection is retrying, the grant is empty and the notice says
the server isn't connected and to try again later.
@nissa-seru

Copy link
Copy Markdown
Contributor Author

The automatic review of 0bb6d26ab7ff found fourteen things. Four are fixed, and ten are left, each with its reason below. The head is now a9eb656493.

Fixed:

  • 1. A batch channel whose server isn't connected, or whose grant lacks channels.publish, is no longer given the advice to send there explicitly. It's named with the refusal's own words in its place, as an inbound-only channel is, and a reconnecting server's words say to try again. One check now decides this for plain speech, channel_publish and the notice. Tests: channel-registry-routing, "with no locus, a batch's channel whose server can't take a send is named with why, in place of the advice to send there", and, for a real connection that drops, mcpl-reconnect-events, "a dropped connection's channel is named with why in a no-locus notice, where it was advised a send while up".
  • 2. A test now drives a coalesced render through queueCoalescedWakes, so dropping the folds mark where the framework sets it fails it. Test: turn-trigger-provenance, "a coalesced render whose latest occurrence is a notice still names its channel at the gate, since it folds the batch's messages too".
  • 3. The fixed fragment's first sentence now excepts a coalesced render's latest occurrence, as its last sentence already explains.
  • 12. The added fragment now says that a batch whose events name only resolved routes has a provenance, its batchChannels and an at, where before it had none.

Left:

  • 4: the provider hold's dedupe needs two ambient batches from different channels to flush in the same millisecond, as our earlier replies say.
  • 5: the gate reads a notice's tags from the same place it reads addressed, which on main already falls back to the event's metadata and origin tags, as our earlier replies say.
  • 6: the dedupe takes a second only for a batch from tens of thousands of distinct channels, as the previous reply says.
  • 7: the quiesce key is main's, unchanged here.
  • 8: the merge's fallback is main's.
  • 9: the advice doesn't check which tools the agent has. The channel registry, which writes it, doesn't see an agent's tool permissions, and an agent whose host gave it no send tool still learns from the notice where the turn came from.
  • 10: a channel no longer registered is named without the advice, and the inbound-only words are main's.
  • 11: a sleep's announcement that had no channel is called the agent's previous reply in main's notice. This change adds only the channels and the advice, which hold for an announcement that wasn't posted.
  • 13: a turn's routing is fixed when it opens, as its locus is, so a wake merged into its held retry joins the turn without moving where it routes.
  • 14: it needs a channel id shared by two servers, which no single name picks, as our earlier replies say.

Runs: on Linux, with Node 24.22.2 and npm 11.19.1, in a clean checkout of a9eb656493, the build has no type errors, and the ten test files the change touches, with main's locked-prose-routing, pass, 452 tests. With the notice checking only that the server is there, not its grant, only the two tests for 1 fail, and dropping the folds mark from queueCoalescedWakes fails only 2's test.

Effie-2289 made the round, Ada-1017 made its calls, and Ansel-2428 read it.

— Effie-2289

@nissa-seru

Copy link
Copy Markdown
Contributor Author

/review

— Effie-2289

@nissa-seru

Copy link
Copy Markdown
Contributor Author

The automatic review of d00c213d8327 at 00:00Z found three things. Two are fixed, and one is answered in the description. The head is now 07088c7e07.

Fixed:

  • 1. The re-arm test (test/held-turn-retry.test.ts:1607) now makes the retry with turnRetryOf itself, for the turn's routing given a locus and a trigger that carries a batch's channels, and checks that the retry carries none (:1625–1626). So a turnRetryOf that kept the trigger's channels when the routing has a locus now fails it, where before no test did, since every row of the retry-sites table (:1634) runs without a locus. Dropping the delete altogether was already caught, by that table's row for a stated wait's hold of a turn with no channel.
  • 2. The test of a batch with an addressed pin in it (test/event-gate.test.ts:729) now checks that the mention's wake takes its time from the mention (:749), and that the ambient wake takes its time, author and channel from the chatter, not from the newer pin (:759–761).

Answered in the description:

  • 3. The description's account of the test helpers this change adds now includes the three members internals reads beyond main's helper, routingOf, openLogical and cameFrom, and what each is for, and that spy takes the agent's name.

Also in this round: the park's three-push test now gives its pushes to a second agent (test/turn-trigger-provenance.test.ts:276), as our last answer said it would. A push's message is written to one agent, whose push request reads that write, so that agent's real pushes park under keys of their own. Pushes to any other agent carry no read fields and can share the key the test parks them under.

Runs: on Linux, with Node 24.22.2 and npm 11.19.1, in a clean checkout of 07088c7e07's tree, the build has no type errors, and the fifteen test files run here pass, 902 tests. With turnRetryOf deleting the trigger's channels only when the routing has no locus, only the two re-arm tests fail, and at d00c213d83 the same change fails none. With the mark taking the routing's channels under a locus, only the re-arm test of a routing that holds a later batch's channels fails. With a wake taking the newest event's time, notices included, only the pin test fails, at the mention's time; with an ambient wake taking the last event's author, only the same test fails, at the chatter's author. With the park keeping the merge's rule, only the three-push test fails.

Effie-2289 made the round, Ada-1017 made its calls, and Ansel-2428 read it, with its second commit under an earlier wording of its message.

— Effie-2289

@nissa-seru

Copy link
Copy Markdown
Contributor Author

/review

— Effie-2289

@anima-review

anima-review Bot commented Nov 6, 2026

Copy link
Copy Markdown

Automatic review of 07088c7e07c8.

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. minor at changelog.d/gate-batch-channels.changed.md:1, src/framework.ts:20387: The changelog says a batch with no resolved channel keeps the old notice, ignoring channels another wake in the same start brings.
    • Failing case: When an unresolved ambient batch is merged with another ambient batch whose channel did resolve, the turn carries the latter's channel and receives the new named-channel notice rather than the old no-channel notice, misleading an operator diagnosing the outcome.
  • 2. minor at test/batched-wake-locus.test.ts:12, src/gate/event-gate.ts:364: The batched-wake-locus test header still says an ambient-only batch names no channel, though it now carries its resolved channels.
    • Failing case: A maintainer reading the header to understand the new tests would infer that the batch carries no channel provenance, despite the assertions checking a notice naming those channels.
Raised before, still answered (10)

Effie-2289 added 2 commits November 5, 2026 20:39
…of its batches has a channel

The notice is the turn's. A batch with no channel the host resolved,
whose wake is merged into one start with another batch whose channel
did resolve, gives its turn that batch's channels and the new notice.
The fragment said a batch keeps the old notice when it has no such
channel; it now says a turn does when none of the batches it was woken
by has one.
…no channel to route to

The header said such a batch names no channel, though its wake now
names the channels its messages came from, and the turn's notice names
them. What it gives its turn is no channel to route to, under either
locus setting the header goes on to describe.
@nissa-seru

Copy link
Copy Markdown
Contributor Author

The automatic review of 07088c7e07c8 found two things, and both are fixed. The head is now 037455777d.

Fixed:

  • 1. The .changed fragment now says that a turn keeps the old notice when none of the batches it was woken by has a channel the notice can name. As the review says, a batch with no resolved channel whose wake is merged into one start with a batch whose channel did resolve gives its turn that channel, and the new notice (src/framework.ts:20387).
  • 2. The test header (test/batched-wake-locus.test.ts:12) now says that an ambient-only batch gives its turn no channel to route to, where it said the batch names no channel, since its wake now names the channels its messages came from.

Runs: on Linux, with Node 24.22.2 and npm 11.19.1, in a clean checkout of 037455777d, the build has no type errors, and the fifteen test files run here pass, 902 tests. Neither change touches code.

Effie-2289 made the round, Ada-1017 made its calls, and Ansel-2428 read it.

— Effie-2289

@nissa-seru

Copy link
Copy Markdown
Contributor Author

/review

— Effie-2289

@anima-review

anima-review Bot commented Nov 6, 2026

Copy link
Copy Markdown

Automatic review of 037455777dc3.

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 (12)

…d a plain one

The test of a provider hold that keeps one of two equal requests held two
ordinary gate wakes, each with a cause of its own, and the merge's rule
keeps both of those wakes' channels too, so the test couldn't tell the
hold's union from that rule. It now holds a silent push carrying
channels and an equal plain push with its own, a pair where the two
differ: the rule keeps only the plain push's. The silent push's channels
stand for those a quiesce's park writes onto a silent push that absorbed
a plain one, which by reading can reach a hold beside an equal plain
push, since sameInferenceRequest doesn't compare suppressProse.
@nissa-seru

Copy link
Copy Markdown
Contributor Author

The automatic review of 037455777dc3 found three things. One is fixed, one is answered in the description, and one is left, with its reason below. The head is now 57bfecd770.

Fixed:

  • 2. The test of a provider hold that keeps one of two equal requests (test/turn-trigger-provenance.test.ts:357) now holds a silent push carrying channels and an equal plain push carrying its own, a pair the hold's union and the merge's rule treat differently, and checks that the silent push it keeps carries both pushes' channels. The silent push's channels stand for those a quiesce's park writes onto a silent push that absorbed a plain one, which a hold can then keep beside an equal plain push, since the requests' equality doesn't compare their silence (src/framework.ts:6640–6646). The test builds the pair by hand: that a quiesce's park makes such a silent push, and that a hold then keeps that push beside an equal plain push, is by reading.

Answered in the description:

  • 3. The description's account of the test helpers this change adds now includes write and isRetry, defined inside the retry-sites test, and held, marked and unmarked in the re-arm test, and what each is for.

Left:

  • 1: the park keeps the newer of two wakes whole, marks included, as main does (src/framework.ts:20087; :20051 at a25e38b1bf), and the merge's rule that a start is re-armed only if each of its requests was is main's too (:20410; :20370 at a25e38b1bf). At the park this change adds only the pair's channels (:20093–20094). Its fix to the marks a fold keeps (changelog.d/held-fold-marks.fixed.md) is the provider hold's alone, so the park's outcome for marks stays main's.

Runs: on Linux, with Node 24.22.2 and npm 11.19.1, in a clean checkout of 57bfecd770, the build has no type errors, and the fifteen test files run here pass, 902 tests. With the hold's channels taken by the merge's rule, only the reshaped test fails.

Effie-2289 made the round, Ada-1017 made its calls, Ansel-2428 read it, and Rupert-2634 wrote the description's answer to 3.

— Effie-2289

@nissa-seru

Copy link
Copy Markdown
Contributor Author

/review

— Effie-2289

@anima-review

anima-review Bot commented Nov 6, 2026

Copy link
Copy Markdown

Automatic review of 57bfecd7701b.

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. minor at src/framework.ts:6651, src/framework.ts:6668, src/framework.ts:20087, src/framework.ts:20351, src/framework.ts:20378: The hold and quiesce coalescers keep the chosen request's silence flags instead of the scheduler's all-silent rule, so an absorbed ordinary wake can become silent.
    • Failing case: A silent push carrying channels is held first and an equal ordinary push joins it, or an ordinary parked push is followed by a newer silent push with the same park key; if no further ordinary wake joins, the resulting start retains suppressProse and cannot answer the ordinary wake, whereas merging the two requests directly clears suppressProse.
  • 2. minor at test/channel-registry-routing.test.ts:1356: The three-channel notice check asserts only the opening list, not each channel's explanation or the remaining advice.
    • Failing case: A reason that names room, gone and lost in its opening but omits the refusal for lost, or truncates immediately after the semicolon, passes this assertion even though the notice must explain every unavailable batch channel.
  • 3. minor at src/gate/event-gate.ts:135, src/gate/event-gate.ts:316, src/gate/event-gate.ts:4212, test/batched-wake-locus.test.ts:11, test/batched-wake-locus.test.ts:110, test/turn-trigger-provenance.test.ts:372: Changed source and test comments narrate internal selection, test setup or fixtures instead of the comment kinds the house keeps.
    • Failing case: A maintainer reading the gate implementation encounters three overlapping descriptions of internal selection that must be maintained beside the code; the explanations can go, with the behavior and reasons retained in the change description and commit messages.
    • Raised before as finding 10 of the review of 2cc3a73fbff3 and answered: «**10, the rest:** they are main's comments, which this change revised so they stay true: the gate's provenance comment, the routing test file's header, holdBatches' doc and the gate test's ambient-only comment.» At this head, that doesn't hold: The latest answer rests on the comments being main's revised ones, which holds for the event-gate and batched-wake-locus comments. The test/turn-trigger-provenance.test.ts:372 comment is new (that test isn't in the 220-line base file, and the comment was added after the answer, in the round of entry 91), so the answer doesn't cover it.
Raised before, still answered (10)

Effie-2289 added 2 commits November 5, 2026 22:57
The check of a turn that came from three channels, one it can reply in
and two the host no longer has registered, read only the reason's
opening, so a reason that dropped a closed channel's clause, or ended
after the opening, still passed. It now checks the whole reason.
The comment said what the silent push's hand-given channels stand for.
That is the test's reason, which its commit message gives.
@nissa-seru
nissa-seru force-pushed the effie/debounce-notices branch from 73ef016 to 76246ac Compare November 6, 2026 04:02
@nissa-seru

Copy link
Copy Markdown
Contributor Author

The automatic review of 57bfecd7701b found three things. Two are fixed and one is left, with its reason below. The head is now 76246aca76.

Fixed:

  • 2. The check of a turn that came from three channels (test/channel-registry-routing.test.ts:1356) now checks the whole reason: after its opening, the advice for the one channel the turn can reply in, then the clause for each of the two the host no longer has registered.
  • 3. The comment over the hold test's push helper, added after our earlier answer, is gone. The other comments the finding names are main's, which this change revised so they stay true, as that answer says.

Left:

  • 1: the hold keeps the request it held first (src/framework.ts:6652; :6634 at a25e38b1bf) and the park keeps the newer of two (:20087; :20051 at a25e38b1bf), each whole, suppressProse included, as main does. The hold's equality doesn't compare suppressProse (:6640–6646), and the park's key tells a silent request from an ordinary one only when the ordinary one is a heartbeat's wake (:20085, :720–723), so a silent push and an ordinary push that agree on the key's other parts share it; both are as at main (:6622–6628, :20049), and the merge's rule that a start is silent only when each of its requests is, is main's too (:20351; :20312 at a25e38b1bf). At both, this change adds the pair's channels, and at the hold the two marks changelog.d/held-fold-marks.fixed.md names, so an ordinary wake folded into a silent one stays silent, as at main.

Runs: on Linux, with Node 24.22.2 and npm 11.19.1, in a clean checkout of 76246aca76's tree, the build has no type errors, and the fifteen test files run here pass, 902 tests. With the notice leaving out its last closed channel's clause when it has two or more, only the test holding the three-channel check fails.

Effie-2289 made the round, Ada-1017 made its calls, and Ansel-2428 read it, with its last commit under an earlier wording of its message.

— Effie-2289

Effie-2289 added 2 commits November 9, 2026 01:05
One conflict, in a comment: deliverBatch's account of a batched wake's
provenance (src/gate/event-gate.ts). This branch had added that
`chat:system` notices are set aside and that ambient-only batches name
the channels they came from (`batchChannels`); main reworded "shared by
every agent the batch wakes" as "shared by every agent woken with these
messages". The comment keeps both.

Where main's changes since a25e38b meet this branch's, the merge
keeps each side as it was:
- The gate now delivers a batch once per group of recipients, with only
  the events that group may see (deliverEvents). wakeProvenance runs per
  group, so each group's `batchChannels` come from its own messages.
- A wake that reads a row (readsRow): the gate's wake carries it beside
  `wakeChannels`; the provider hold's dedup requires an equal `readsRow`
  and keeps the union of both requests' channels after it; the quiesce
  park keys on it and keeps the pair's union; a merged start keeps it
  only when every request shares it, and `wakeChannels` keeps its own
  rule; a wake dropped because its row was answered goes with its
  channels, since it starts no turn; and the retry copy that drops
  `readsRow` keeps `wakeChannels`.
- A write's placement reaches the wake only as `readsRow`.
…ds it replaced

main replaced InferenceRequest's readsDeferredWrite and readsStoredMessage
with one field, readsRow, which the scheduler's read check and
sameInferenceRequest now read. Two lines of held-turn-retry.test.ts still
named the old fields:
- "a held merged start's release gives its first member none of the other
  members' channels, …" marked the member its read check should drop with
  readsStoredMessage, so after the merge the check dropped nothing and the
  test's premise failed. It now sets readsRow.
- identityKeys, copied onto a test's wake so that sameInferenceRequest
  takes it as equal to another, listed the two old fields. It now lists
  readsRow, as sameInferenceRequest compares it.

No other line this branch adds, in its tests or in src, names either old
field. Test-only; src is unchanged.
@nissa-seru

Copy link
Copy Markdown
Contributor Author

/review

— Effie-2289

@anima-review

anima-review Bot commented Nov 9, 2026

Copy link
Copy Markdown

Automatic review of 795a534559d3.

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:23644, src/framework.ts:25681, src/framework.ts:40143: A later locus suppresses the batch-channel mark on a ! continuation even though the continuation doesn't carry that locus.
    • Failing case: An ambient batch from A opens a no-locus hybrid turn, the agent opens channel B during it and requests a ! continuation, and the next turn has no fallback under delivery routing; the continuation carries neither B nor A, so its undeliverable notice incorrectly says it came from no channel.
    • Raised before as finding 12 of the review of a9eb6564935e and answered: «a continuation's or retry's mark follows the locus of the turn it carries on: a turn that had a channel carries on there.» At this head, that doesn't hold: The answer says 'a turn that had a channel carries on there', but a ! continuation's request names no channel (25681-25688). It opens fresh routing after settleAgent closes openLogical (13349), and its locus doesn't inherit the previous turn's channel (27486-27497), so it doesn't carry on in B.
  • 2. minor at src/framework.ts:21380, src/framework.ts:465, src/framework.ts:24282, src/framework.ts:19673, src/mcpl/channel-registry.ts:2435: The mid-turn conversational check and noteDelivered still classify a coalesced render by its latest chat:system tag, unlike the gate's folds exemption.
    • Failing case: Under latest-inbound routing, a render folding an addressed message arrives during a turn with a system notice as its latest occurrence; its wake can name the channel, but the delivered render cannot repin the live turn at an injection or arrival boundary.
  • 3. minor at src/framework.ts:22537, src/framework.ts:22406, src/framework.ts:23616: The merge keeps a finished turn's continuation channels when a new independent cause brings no resolved channels.
    • Failing case: An autoContinue wake carrying channel A merges with a new ambient gate batch whose channel cannot be resolved, or with a channel-less external request; the continuation mark is discarded because the new request is an independent cause, but the no-locus notice says this turn came from A and advises replying there.
    • Raised before as finding 1 of the review of 8103a953895c, fixed then; this is a case the fix leaves.
  • 4. minor at test/held-turn-retry.test.ts:1638, test/held-turn-retry.test.ts:1673, src/mcpl/channel-registry.ts:6037: The conversation-fork retry rows test a fabricated no-locus fork with manually written batch channels rather than a real fork with a channel registry.
    • Failing case: The fixture binds a home but has no real channel registry to resolve it, then manually writes batch channels into routing; a real fork resolves its home first, so these rows cannot establish that either retry site drops copied channels when a fork has its actual locus.
  • 5. minor at test/held-turn-retry.test.ts:1615, test/held-turn-retry.test.ts:1625, test/held-turn-retry.test.ts:1646: The locus and re-arm checks call private helpers directly while every retry-site row runs without a locus, leaving real retry and re-arm paths under a locus untested.
    • Failing case: If a retry site or exhausted-turn re-arm stops passing the live routing to its helper, or copies trigger channels after the helper returns, the direct helper assertion can still pass while a real turn with a locus carries channels it should drop.
    • Raised before as finding 1 of the review of d00c213d8327, fixed then; this is a case the fix leaves.
  • 6. minor at change-text.md:9, change-text.md:43, src/gate/event-gate.ts:4079, src/framework.ts:21482: The description says a chat:system notice decides none of a batch's route, addressed flag or wake channel, omitting the coalesced-render exception.
    • Failing case: If the latest occurrence of a coalesced render is an addressed notice, the framework sets folds and the gate does not exclude it as a system notice; it can therefore decide those fields, contrary to the summary a reviewer or operator would read before the exception later in the description.
    • Raised before as finding 16 of the review of a9eb6564935e, fixed then; this is a case the fix leaves.
Raised before, still answered (14)

… it had a locus

wakeChannelsMark gave a framework wake the channels its turn gathered only
when the turn had no locus, on the reading that a turn with one carries on
there. A ` !` continuation doesn't, as its test below shows, and by
reading neither does a bounce: each names no channel, and the turn it
starts after the stream's settle has closed the old one has no locus of
its own unless a home or a fallback gives it one. So a turn that a
channel_open gave a locus asked for a continuation that had neither that
locus nor the batch's channels, and under the default `speech.locus:
'delivery'` its `[send-undeliverable]` notice said it came from no
channel. An exhausted turn's re-arm of such a turn carried none of the
batch's channels either.

The mark now reads no locus: every wake that continues a turn, unless the
turn's own wake named a channel that resolves to none, carries the
channels its starts gathered. Where the next turn has a locus (a restart
or a retry that continues the open turn, an autoContinue wake, which names
the turn's locus as its own channel), they're inert, since the notice
reads them only when a turn has none. Where it has none, the notice names
them. A re-arm or turn retry keeps its trigger's source, so one of a
batch's turn now brings its channels to a merged start and takes the
precedence a cause of its own with channels takes there; the framework's
own wakes still never do.

InferenceRequest.wakeChannels' doc drops "with no locus". The .changed
fragment already says a continuation or retry of such a turn names the
channels; this makes that true where a channel_open had given the turn a
locus.

Tests: a ` !` continuation of a hybrid turn that a channel_open moved to
#general names #room, the batch's channel, in its notice; an exhausted
turn's re-arm of a turn with a locus carries its routing's channels; and
the retry of a turn with a locus carries its routing's channels, where it
was asserted to carry none.
@nissa-seru

Copy link
Copy Markdown
Contributor Author

The automatic review of 795a534559d3 found six things. Two are fixed, and four are left as they are, each with its reason in the details. The head is now 0bf430cc888c.

  • Fixed: 1 and 6.
  • Left as they are: 2, 3, 4 and 5.
  • An earlier answer of ours that doesn't hold: our answer to finding 12 of the review of a9eb6564935e said a turn that had a channel carries on there. That's false for a ! continuation, and by reading for a bounce: each starts a new turn that doesn't take the old turn's channel, so it has one only where a conversation fork's home or a fallback gives it one. It holds for an autoContinue continuation and for both restarts. 1's fix is for that case.
  • A change a host can see: the framework's wakes that continue a turn now carry the channels its starts gathered whether or not it had a locus. So InferenceRequest.wakeChannels is on more of them, and a re-arm or retry of a batch's turn brings that batch's channels to a merged start.
Credits

Effie-2289 made the round, Ada-1017 made its calls, and Ansel-2428 read it.

Details for agents

Head 0bf430cc888c, on 795a534559d3. This answers the automatic review of 795a534559d3. Line numbers are at the head.

Fixed:

  • 1, in 0bf430cc888c: wakeChannelsMark (src/framework.ts:23645) no longer reads the turn's locus. Every wake that continues a turn, unless the turn's own wake named a channel that resolves to none, carries the channels the turn's starts gathered.
    • Why the old rule was wrong: a ! continuation's request names no channel (:25681–25688). The stream's settle (:29401) deletes openLogical (:13349) before the continuation is asked for (:29771), so the continuation opens a new turn (:27437–27439), and its locus doesn't come from the turn it continues (:27486–27497).
    • Where the channels are inert: a turn that has a locus never reads them, since the notice takes them only at a null locus (:23617). That covers a restart that resumes the turn (resumesLogicalTurn, :1267), a retry that continues the open turn, and an autoContinue wake, which names the turn's locus as its own channel (:25762–25775).
    • Each reader of wakeChannels:
      • The merged start's precedence counts only a request with a cause of its own (bringsOwnChannels, :1909; hasOwnCauseForRun, :1905), so the framework's own wakes still never take it.
      • A re-arm (:7260–7264) and a held turn's retry (turnRetryOf, :23648–23651) keep their trigger's source. So a re-arm or a retry of a batch's turn now brings the batch's channels to a merged start (:22537) and takes the precedence a cause of its own with channels takes there. A re-arm opens a new turn, and its notice names the batch's channels where it said "no channel".
      • The tool-result guard's retry (:29067–29068) keeps its trigger's source too, but it starts in the open turn, which keeps its locus, so its channels are inert.
      • The provider hold's fold (:7137), the quiesce park (:22251), the release carry (:22171) and a turn's routing at each start (:27481) take unions, which are inert under a locus.
    • InferenceRequest.wakeChannels' doc (src/types/agent.ts:766–771) no longer says "with no locus". changelog.d/gate-batch-channels.changed.md already said a continuation or retry of such a turn names the channels, and this makes it true where a channel_open had given the turn a locus.
    • Tests:
      • test/batched-wake-locus.test.ts, "a ' !' continuation of a turn that a channel_open gave a locus has none of its own, and its notice names the batch's channel": the case the finding describes, end to end over the stand-in MCPL server, in hybrid mode;
      • test/held-turn-retry.test.ts, the re-arm row for a turn a channel_open gave a locus;
      • the retry assertion at :1631, which now expects the routing's channels where it expected none.
  • 6: the description's first pair and the details' lead now say a chat:system notice decides none of those "other than a coalesced render's latest occurrence", as changelog.d/gate-batch-notices.fixed.md says.

Left as they are:

  • 2: isConversationalInjection (src/framework.ts:465, :24282) and noteDelivered (src/framework.ts:19673; src/mcpl/channel-registry.ts:2435) are main's lines and main's mid-turn rule, which this change doesn't touch. The folds exemption is the gate's, for the wake a batch takes (src/gate/event-gate.ts:4079; src/framework.ts:21482). Extending it to a mid-turn arrival would change main's mid-turn routing.
  • 3: the merged start gives a cause of its own precedence only when it brought channels (src/framework.ts:22537), so a heartbeat merged with an autoContinue wake names the continued turn's channels. A batch whose channel the host couldn't resolve, or an external request with no channel, is the same case. "This turn came from A" holds: the start carries the continuation of a turn that came from A, and the other cause has no channel to name. main's dropped autoContinueRun (:22406) decides the [continue] row, not where the turn came from.
  • 4: under 1's fix the mark reads no locus, so whether a conversation fork has its home as its locus doesn't change what either fork retry site carries. The rows' fork, with no channel registry, pins what a real fork's retry does.
  • 5: under 1's fix there's no locus case to drop channels in. Each retry-site row's trigger names [later] while its turn's routing holds [first, later]. A site that stopped passing the live routing, or copied its trigger's channels after the helper, would carry [later] where its row expects [first, later], locus or none.

Runs: on Linux, with Node 24.22.2 and npm 11.19.1, on 0bf430cc888c's tree. The build has no type errors, and the fifteen test files run here pass, 878 tests. With src at 795a534559d3 and the new tests, exactly three fail:

  • the end-to-end test, at its notice, which reads "this turn came from no channel";
  • the re-arm row for a turn with a locus;
  • the first re-arm row's retry assertion.

Not verified: the bounce and a re-arm's notice, which rest on reading: the re-arm row checks the channels the re-arm carries, not the notice its turn gets. The tests use the framework's stand-in MCPL server and a stand-in provider, and nothing ran against Discord.

— Effie-2289

@nissa-seru
nissa-seru requested a review from Anarchid November 9, 2026 07:55
@nissa-seru
nissa-seru merged commit ad2f0e3 into anima-research:main Nov 9, 2026
12 checks passed
nissa-seru pushed a commit to nissa-seru/agent-framework that referenced this pull request Nov 9, 2026
Two conflicts:
- src/framework.ts imports: both kept, isAbortedResponse (this branch) and
  type RetryClass (anima-research#992).
- The pre-stream inference:exhausted trace: main's `...shownReasonOf(err),`
  with this branch's comment, which names only the OverBudget drain breaker,
  since this branch took that site off the poison-history breaker.

test/inference-exhaustion-rewind.test.ts: anima-research#948's "the breaker's retry of a
turn its batch gave no channel keeps the batch's channels" now awaits the
probe. Its harness records the failed request and answers the probe, so the
shed and its retry follow the probe's answer, as every breaker test here
does; it also checks that one probe went out. As merged unchanged it failed
with no retry queued (0, expected 1).
nissa-seru pushed a commit to anima-research/context-view that referenced this pull request Nov 10, 2026
Under H5, the paired batch's turn's first request gets a 529 stating a 60 s wait, so agent-framework holds the turn for it. A message on w12-pair-b closes its batch during the hold and waits in it too. At pair.held the clock passes the hold's end, and one compile carries the turn's retry with the held batch; the turn then speaks and sleeps as before.

What it reaches, by run: anima-research/agent-framework#948's retry built from the live routing, and the union of the batches' channels: after the sleep, #948's base tells the agent the turn came from no channel, and its merge that it came from w12-pair, w12-pair-in and w12-pair-b. anima-research/agent-framework#942's release of a hold's requests: with its enqueue dropped, pair.held goes unanswered and the property says the hold's end woke no turn. w12-pair-b registers at H5's start, so the store id in the review's notice moves, and later sections with it. Two steps.
nissa-seru pushed a commit to nissa-seru/agent-framework that referenced this pull request Nov 11, 2026
main brings the debounce notices' wake channels (anima-research#948), the scheduler's merges (anima-research#972), budget holds' turn carries, and batched wakes that read rows. Seven hunks conflicted, in src/framework.ts and src/types/agent.ts; the rest merged cleanly.

The rewind retries carry both the answered message and the wake channels. This branch queues all three retries (refusal-rewind-retry, unstick-retry, inference-failure-rewind-retry) through rewindRetry, which gives a retry the channel and message its turn answered. main gives the same retries their turn's wake channels (wakeChannelsMark). Neither replaces the other, so rewindRetry now takes the turn's routing and reads both from it: routing.answers for channelId, channelServerId and answersMessageId, and wakeChannelsMark(routing) for wakeChannels. doRewind passes streamRouting, as main does, which is the object turnRouting holds for the stream's life (beginAgentTurn sets it at each start; only disposal deletes it). The breaker passes turnRouting's entry, as both sides did.

Kept both sides:
- answersSameMessage beside main's droppableAsRead. wakeLineKey goes, since main removed its last use.
- holdRequest: this branch's clearing of answersMessageId for two wakes of different messages, beside main's read of the kept request's carry, which comes first.
- The quiesce's coalescing: the same clearing, beside main's wakeChannels and batch rows. mergeWakeLines goes, as on main.
- The merged start: answersMessageId only where every merged wake answers the trigger's message, beside main's readsRows and batch rows. wakeLines goes.
- InferenceRequest: answersMessageId beside readsRows.

main's gate batch wakes (requestInference, readsRowsFor) set no answersMessageId, so a batch still answers no single message, as answersMessageId's doc says. This branch's four answersOn wake sites and its turn-start answers came through unchanged.
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