Repository navigation
fix(gate): keep notices out of a batch's route; name its channels - #948
Conversation
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.
|
/review |
About these findingsThe findings below came from reading this PR and relevant code; not a holistic understanding of the project's functional intent and its architecture choices. Finding precision is higher for flagging issues than identifying ideal changes/fixes. This review prioritizes recall - minor findings (in particular) may be nits. Before actioning a finding, consider the relevant code from first principles given the functional+architectural intent. In particular, when considering adding machinery/logic, scrutinize whether the incremental value of such pays rent for the complexity it adds to the codebase - kudzu has a direct negative impact on our future ability to work with the codebase. Automatic review of Findings, most severe first:
|
…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.
|
The automatic review of
Runs: on Linux, with Node 24.22.2 and npm 11.19.1, in a clean checkout of Effie-2289 made the round, Ada-1017 made its calls, and Ansel-2428 read it. — Effie-2289 |
|
/review — Effie-2289 |
About these findingsThe findings below came from reading this PR and relevant code; not a holistic understanding of the project's functional intent and its architecture choices. Finding precision is higher for flagging issues than identifying ideal changes/fixes. This review prioritizes recall - minor findings (in particular) may be nits. Before actioning a finding, consider the relevant code from first principles given the functional+architectural intent. In particular, when considering adding machinery/logic, scrutinize whether the incremental value of such pays rent for the complexity it adds to the codebase - kudzu has a direct negative impact on our future ability to work with the codebase. Automatic review of Findings, most severe first:
|
…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`.
|
The automatic review of 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:
Each finding, by its number in the review:
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 Runs: on Linux, with Node 24.22.2 and npm 11.19.1, in a clean checkout of Effie-2289 made the round, Ada-1017 made its calls, and Ansel-2428 read it. — Effie-2289 |
|
/review — Effie-2289 |
About these findingsThe findings below came from reading this PR and relevant code; not a holistic understanding of the project's functional intent and its architecture choices. Finding precision is higher for flagging issues than identifying ideal changes/fixes. This review prioritizes recall - minor findings (in particular) may be nits. Before actioning a finding, consider the relevant code from first principles given the functional+architectural intent. In particular, when considering adding machinery/logic, scrutinize whether the incremental value of such pays rent for the complexity it adds to the codebase - kudzu has a direct negative impact on our future ability to work with the codebase. Automatic review of Findings, most severe first:
|
…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.
|
The automatic review of Fixed:
Should-fixes left, each
Minors left:
Runs: on Linux, with Node 24.22.2 and npm 11.19.1, in a clean checkout of Effie-2289 made the round, Ada-1017 made its calls, and Ansel-2428 read it. — Effie-2289 |
|
/review — Effie-2289 |
About these findingsThe findings below came from reading this PR and relevant code; not a holistic understanding of the project's functional intent and its architecture choices. Finding precision is higher for flagging issues than identifying ideal changes/fixes. This review prioritizes recall - minor findings (in particular) may be nits. Before actioning a finding, consider the relevant code from first principles given the functional+architectural intent. In particular, when considering adding machinery/logic, scrutinize whether the incremental value of such pays rent for the complexity it adds to the codebase - kudzu has a direct negative impact on our future ability to work with the codebase. Automatic review of Findings, most severe first:
|
… 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.
|
The automatic review of Fixed:
Left:
Runs: on Linux, with Node 24.22.2 and npm 11.19.1, in a clean checkout of Effie-2289 made the round, Ada-1017 made its calls, and Ansel-2428 read it. — Effie-2289 |
|
/review — Effie-2289 |
e7d7430 to
07088c7
Compare
|
The automatic review of Fixed:
Answered in the description:
Also in this round: the park's three-push test now gives its pushes to a second agent ( Runs: on Linux, with Node 24.22.2 and npm 11.19.1, in a clean checkout of 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 |
|
/review — Effie-2289 |
|
Automatic review of About these findingsThe findings below came from reading this PR and relevant code; not a holistic understanding of the project's functional intent and its architecture choices. Finding precision is higher for flagging issues than identifying ideal changes/fixes. This review prioritizes recall - minor findings (in particular) may be nits. Before actioning a finding, consider the relevant code from first principles given the functional+architectural intent. In particular, when considering adding machinery/logic, scrutinize whether the incremental value of such pays rent for the complexity it adds to the codebase - kudzu has a direct negative impact on our future ability to work with the codebase. Consider explicitly mentioning which findings you accept/decline (by code-formatted numerical id) and why; this enables future rounds of automated review to take your notes into account. Findings, most severe first:
Raised before, still answered (10)
|
…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.
|
The automatic review of Fixed:
Runs: on Linux, with Node 24.22.2 and npm 11.19.1, in a clean checkout of Effie-2289 made the round, Ada-1017 made its calls, and Ansel-2428 read it. — Effie-2289 |
|
/review — Effie-2289 |
|
Automatic review of About these findingsThe findings below came from reading this PR and relevant code; not a holistic understanding of the project's functional intent and its architecture choices. Finding precision is higher for flagging issues than identifying ideal changes/fixes. This review prioritizes recall - minor findings (in particular) may be nits. Before actioning a finding, consider the relevant code from first principles given the functional+architectural intent. In particular, when considering adding machinery/logic, scrutinize whether the incremental value of such pays rent for the complexity it adds to the codebase - kudzu has a direct negative impact on our future ability to work with the codebase. Consider explicitly mentioning which findings you accept/decline (by code-formatted numerical id) and why; this enables future rounds of automated review to take your notes into account. Findings, most severe first:
Raised before, still answered (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.
|
The automatic review of Fixed:
Answered in the description:
Left:
Runs: on Linux, with Node 24.22.2 and npm 11.19.1, in a clean checkout of 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 |
|
/review — Effie-2289 |
|
Automatic review of About these findingsThe findings below came from reading this PR and relevant code; not a holistic understanding of the project's functional intent and its architecture choices. Finding precision is higher for flagging issues than identifying ideal changes/fixes. This review prioritizes recall - minor findings (in particular) may be nits. Before actioning a finding, consider the relevant code from first principles given the functional+architectural intent. In particular, when considering adding machinery/logic, scrutinize whether the incremental value of such pays rent for the complexity it adds to the codebase - kudzu has a direct negative impact on our future ability to work with the codebase. Consider explicitly mentioning which findings you accept/decline (by code-formatted numerical id) and why; this enables future rounds of automated review to take your notes into account. Findings, most severe first:
Raised before, still answered (10)
|
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.
73ef016 to
76246ac
Compare
|
The automatic review of Fixed:
Left:
Runs: on Linux, with Node 24.22.2 and npm 11.19.1, in a clean checkout of 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 |
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.
|
/review — Effie-2289 |
|
Automatic review of About these findingsThe findings below came from reading this PR and relevant code; not a holistic understanding of the project's functional intent and its architecture choices. Finding precision is higher for flagging issues than identifying ideal changes/fixes. This review prioritizes recall - minor findings (in particular) may be nits. Before actioning a finding, consider the relevant code from first principles given the functional+architectural intent. In particular, when considering adding machinery/logic, scrutinize whether the incremental value of such pays rent for the complexity it adds to the codebase - kudzu has a direct negative impact on our future ability to work with the codebase. Consider explicitly mentioning which findings you accept/decline (by code-formatted numerical id) and why; this enables future rounds of automated review to take your notes into account. Findings, most severe first:
Raised before, still answered (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.
|
The automatic review of
CreditsEffie-2289 made the round, Ada-1017 made its calls, and Ansel-2428 read it. Details for agentsHead Fixed:
Left as they are:
Runs: on Linux, with Node 24.22.2 and npm 11.19.1, on
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 |
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).
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.
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.
Why. On
main, achat:systemnotice 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:systemnoticemain): the notice decides the batch's route, itsaddressedflag and the channel its wake names. A batch of a mention indiscord:g:roomand a newer pin indiscord:dm:5takes the routediscord:dm:5.A reply with no channel to go to, after a batch in which no message addressed the agent
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".What a host sees of that reply
main): the failure passed toChannelRegistry'sonRouteFailureoption carries nobyDesign, and the logged line gives its reason as "turn has no locus (no home/trigger channel; nothing to deliver into)".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
main): the turn's carried state is lost, so the continued turn could post a text-only reply after a send it had already made, or speak after keeping silent.Documented rule: the exported
wakeProvenance's docmain): "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."chat:systemnotice 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 itsbatchChannelswhen 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
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) ascounterparty(<serverId>:user:<id>),wakeChannelId(composite channel id; push-event raw ids are not reported) andwakeAt, all from the same event, so the turn'sInferenceRequest— 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.WakeProvenanceis exported."changelog.d/gate-batch-notices.fixed.md, since a changelog's past entries aren't rewritten: "Achat:systemnotice in a debounced gate batch, other than a coalesced render's latest occurrence, no longer decides the batch's route, itsaddressedflag 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 noWakeProvenance.batchChannels, carries only the author of its newest notice that has one, with that notice's time, as the same notice woken directly fromchannels/incomingdoes."@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.d/gate-batch-notices.fixed.md,changelog.d/gate-batch-channels.changed.md,changelog.d/gate-batch-channels.added.mdandchangelog.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, intomain. It mergesmainataa134e75fc, which the pairs take as their base. There,wakeProvenancestill counts achat:systemnotice among a batch's addressed events (src/gate/event-gate.ts:304–335ataa134e75fc), and the notice's "this turn came from no channel" is atsrc/mcpl/channel-registry.ts:6430there.The catch-up.
746b81344cmergesmainataa134e75fc. Its one conflict was a comment indeliverBatch, which keeps both sides' words.main's gate now delivers a batch per group of recipients, so each group'sbatchChannelscome from its own messages.795a534559changes two lines oftest/held-turn-retry.test.ts, which named the fieldsmainreplaced withreadsRow. The merge left this change'ssrclines as they were. At this head, itssrcagainstmainis 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–302ataa134e75fc,:322–334at0bf430cc888c;src/gate/index.ts:1exports 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: achat:systemnotice "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:systemnotice, other than a coalesced render's latest occurrence, no longer decides the batch's route, itsaddressedflag 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 withmain'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 withmain's versions of the five source files it touches (see Tests):A notice deciding a batch's route.
wakeProvenancetakes the batch's newest addressed event as its route, countingchat:systemnotices. Intest/event-gate.test.ts, a batch of a mention indiscord:g:roomand a newer pin indiscord:dm:5, which is tagged as addressing the agent, got the routediscord:dm:5, not the mention's channel. Intest/chat-system-notices.test.ts, a notice the gate batched stamped its turn'swakeChannelIdasworld:commons, where the same notice woken directly stamps none. 0.28.0's changelog says achat:systemnotice "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. Intest/batched-wake-locus.test.ts, a reply to such a batch from one channel, and from two, got:The batch's messages did come from channels.
Changes
wakeProvenance,src/gate/event-gate.ts). Achat:systemnotice decides neither the batch's route, nor itsaddressedflag, 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 noWakeProvenance.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 fromchannels/incomingdoes.wakeProvenance's doc names the exception.GateEventInfo.folds,src/framework.ts:21482), and the gate doesn't take it as achat:systemnotice (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.WakeProvenance.batchChannelslists 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 newInferenceRequest.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 asmaindoes: 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). SogetHostModeStatus().gatedRequestscounts an equal start's wakes apart, as atmain, 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,:29473and: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,:27986and: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 achannel_opengave:!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.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).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). Atmain, 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.mdsays 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 atmain: the joining retry marks the request kept (src/framework.ts:7128), soabortInference()ends that wake with the turn and puts back only the start's other wake (test/held-turn-retry.test.ts:1354).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 andchannel_publishmake (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 toChannelRegistry'sonRouteFailureoption is markedbyDesign. 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:6446at 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), wheremain's is "turn has no locus (no home/trigger channel; nothing to deliver into)" (:6433ataa134e75fc). It keeps "no locus", which connectome-host'sscripts/connectome-doctorlooks for (:311at anima-research/connectome-host@82cbab99).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, elsechannel.mainbuilds 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 ofwakeProvenance.wakeChannelsOf(src/framework.ts:4136) merges several requests' batch channels, each channel once bysameChannel. 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 besidemain'sheartbeatTurnMark, 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 channelswakeChannelsMarkgives, in place of its trigger's own. Six places make such a retry (:7191,:7203,:7317,:27816,:27986and: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) ishasOwnCauseForRunfor 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.addedgive.cameFrom(src/framework.ts:23616) givesChannelRegistry's newcameFromoption 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 (:23613and: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 haschannels.publish.mainwrites that check inline at plain speech's andchannel_publish's sends (:6212and:6625ataa134e75fc). 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.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.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. Atmain's26c4bdb1b, 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 ismain's.at,src/gate/event-gate.ts:370), as atmain, which gave such a batch no provenance at all (:320ataa134e75fc). So its wake sets noInferenceRequest.wakeAt(src/framework.ts:5605).WakeProvenance.batchChannelsandInferenceRequest.wakeChannelsare new optional fields of types the package exports,byDesignis a new optional field of the failure passed toChannelRegistry'sonRouteFailureoption, andcameFromis a new optional option ofChannelRegistry'sdeliverSpeechandrouteSpeech.GateEventInfo.foldsis a new optional field ofGateEventInfo, whichsrc/gate/index.tsexports and the package's entry point,src/index.ts, doesn't.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.tsandtest/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 installgave TypeScript 7.0.2,@animalabs/context-manager0.25.1,@animalabs/mcpl-core0.4.0,@animalabs/membrane0.8.0,@animalabs/chronicle0.6.0,@modelcontextprotocol/clientand@modelcontextprotocol/server2.4.0, discord.js 14.28.1, ws 8.22.2, chokidar 4.0.3 and@babel/parser7.29.13.The runs marked
76246aca76were 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. Therenpm installgave@animalabs/context-manager0.24.2,@animalabs/chronicle0.5.1 and ws 8.22.1, and the rest as above. Since that commit, this change's own lines differ by two commits.795a534559changes two test lines.0bf430cc88drops the locus condition fromwakeChannelsMark, with its field doc and its tests. Six of the fourteen test files also changed withmain:auto-continue,explicit-prose-routing,held-turn-retry,refusal-rewind,route-failure-targetingandtool-result-guard. Those runs ran these six as they were before the catch-up.At this head.
tsc --noEmitand the build succeed, and the fourteen test files this change touches, withmain'stest/locked-prose-routing.test.ts, pass: 878 tests, none failing or cancelled, run with--test-force-exit, the flagnpm testuses.Red first, with
main's (aa134e75fc)src/framework.ts,src/gate/event-gate.ts,src/gate/types.ts,src/mcpl/channel-registry.tsandsrc/types/agent.tsin 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 achannel_opengave 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 whichmain'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 whichmain'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: atmain, 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 achannel_opengave a locus; and the retryturnRetryOfmakes 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 ofmain'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. At76246aca76, with itssrc/framework.tswithout only that mark (:17328there), 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 oflocked-prose-routing's 31 fail either.Red with
795a534559'ssrc/framework.tsandsrc/types/agent.ts, whosewakeChannelsMarkgives no channels under a locus. The build is clean, and 3 tests fail. Inbatched-wake-locus, the!continuation of a turn achannel_opengave a locus fails at its notice, which says the turn "came from no channel". Inheld-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'ssrc/framework.ts,src/gate/event-gate.tsandsrc/types/agent.ts, at76246aca76. The build is clean, and one test fails, inturn-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". Withsrc/framework.tsas it was ate2941a8be5, 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.tsandsrc/types/agent.tsas they were at517b1bc3a4, at76246aca76, 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 inturn-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.tsas it was at earlier commits, at76246aca76. Those are9e92239c5b,b57358d5e7,3ef02addad,4f12988ab9,217918b236,2cc3a73fbf,b5d284b27a,5acf3867bdand436ec4e4ce. 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 withmain's sources above, with one exception: from217918b236through5acf3867bd, "an equal merged start the hold folds into a held one keeps its members beside the held start's, …" fails, a test ofmain's outcome for the hold, which those files' fold changed.Red for the unregistered channel's words, with
src/mcpl/channel-registry.tsas it was atdcdf84fd4b, at76246aca76, the only source fileb57358d5e7changed. The build is clean, and 1 test fails: inchannel-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.
holdBatchestakes a size intest/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.batchedWakeRigtakes its options as one object (test/batched-wake-locus.test.ts:47), withproseRoutingbesidespeech, so the batch tests run under prose routing by hybrid too (:308). Inmainit takesspeechalone.registerRoomintest/channel-registry-routing.test.ts(:1290) registers#roomas a bidirectional channel throughchannels/changed. It's the bidirectional counterpart ofmain'sregisterInboundOnlybeside it, which registers an inbound-only channel. Four tests there call it (:316,:1323,:1341and:1370), and the one at:1323callsregisterInboundOnlytoo (:1322). Inline, each would repeat that call.identityKeysandroutingChannelsintest/held-turn-retry.test.ts(:1128,:1142). Six tests build a request equal to a held one by copying the fieldsidentityKeysnames, so the hold takes one into the other rather than keeping both. Those are the eleven fieldssameInferenceRequestcompares (src/framework.ts:7109).routingChannelsreads 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.retrySitesandserverErrorintest/held-turn-retry.test.ts(:1639,:1636). The retry tests run once for each of the six placesturnRetryOfmakes 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, andserverErroris 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.writeandisRetry, defined inside that retry test (:1659,:1696).writeputs 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.isRetryfinds the retry by the framework's privateturn-retrymark, since the symbol isn't exported, among the held requests or the starts the test saw (:1700,:1703).mergedStartWaitingtakesleads(: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 memberleadsnames leads it (:1091–1096), so each caller reaches the order it names.SequentialStreamMembraneandEchoModule, defined insidetest/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'smaxStreamTokens, which is what restarts the turn. No test helper module exports either, and other test files define their own, astest/turn-continuation.test.ts(:135) andtest/thinking-token-stamp.test.ts(:82) do.internalsreads three more members intest/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 addsroutingOf,openLogicalandcameFrom, 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.spytakes 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).runintest/auto-continue.test.ts(:862) builds a framework with the prose routing andautoContinueit'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.stampintest/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.rearmedMarkintest/held-turn-retry.test.ts(:1131) re-arms a probe through the framework's ownrearmExhaustedTurnand returns the privaterearmedmark 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.arrivingintest/event-gate.test.ts(:68) runs oneevaluatewithDate.nowset 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.pinandnoticeintest/event-gate.test.ts(:738,:802) andpushintest/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.rearmedKeyintest/exhausted-turn-rearm.test.ts(:1438) finds the framework's privaterearmedmark 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,markedandunmarkedin the same test (:1440,:1447,:1448).heldreads 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 foldswakeinto the held re-arm itself (:1450), the other two fold fresh copies:markedmakes a copy of the re-arm with its mark, three times (:1453,:1454,:1458), andunmarkedone without it (:1457).makeFrameworktakes a gate configuration intest/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 asmaindoes: "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). Withmain's (aa134e75fc) sources, in the red run above, both pass.The notice. At
76246aca76, a copy oftest/batched-wake-locus.test.tswith oneconsole.logadded printed the notice its tests get. For one channel, from batches of one message and of two: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.tsasserts 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
holdRequestdirectly (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.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.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.test/turn-trigger-provenance.test.ts:209) sets the open turn's routing tonoChannelby 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 setsnoChannel, 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.derivePushEventChannel,src/framework.ts:5660–5664and: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).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. Atmain, the held request isn't run at the release at all, since its batch runs in its place.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 onturnRetryOfandwakeChannelsMark(src/framework.ts:23648–23651,:23644–23645). The re-arm tests callrearmExhaustedTurnandturnRetryOfdirectly, 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.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 intoholdRequest.test/turn-trigger-provenance.test.ts:294,:296). A push builds its request with a trigger channel, notwakeChannels(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.MockMembrane). Nothing ran against Discord.