Skip to content

fix(wakes): a write the agent heard mid-turn starts no turn of its own once it's done - #723

Merged
nissa-seru merged 2 commits into
anima-research:mainfrom
nissa-seru:fix/heard-mid-turn-wake
Oct 23, 2026
Merged

nissa-seru merged 2 commits into
anima-research:mainfrom
nissa-seru:fix/heard-mid-turn-wake

Conversation

@nissa-seru

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

Copy link
Copy Markdown
Contributor

Fixes #264

Problem

While an agent's turn is alive, a message that arrives for it is deferred, and its wake is queued behind the busy agent. At the turn's next tool boundary the message is stored and injected into the live stream, so the turn's next round reads it (hear-while-acting). Nothing linked the queued wake to that. Once the turn ended, the wake started another turn, which read nothing new. With the issue's reproduction on main at 12b478ca, 5 runs of 5 sent a third provider request, and all it added was the turn's own reply and its [delivered] receipt.

Other writes queue a wake beside them in the same way: a push event, a module's message, a background script's wake and the gate's self-wake notice. A coalesced render's wake already starts no turn once the agent has read the render.

Changes

  • A wake queued with a deferred write carries the write's id (readsDeferredWrite, as a render's wake already did). It carries it only when the write was deferred for the agent that wake is for. That covers five sites:
    • a channel message (handleMcplChannelIncoming);
    • a push event (handleMcplPushEvent);
    • a module response's one message (applyProcessResponse), unless it's console or API input, or its metadata or its event's names a channel;
    • a background script's wake;
    • the gate's self-wake notice. The gate's two hooks now link the notice to its wake: addMessage fills an optional placement, and requestInference takes it as after. Their type, WritePlacement, is exported from the package index.
  • A channel message's and a push's wake, and a coalesced render's wake to read it, record whether the message's server marked it ambient (ambient: true for chat:ambient, beside addressed). A render stands for a batch of notices, and for a plain occurrence a notice displaced unread, so the coalescer keeps whether every one of them was marked ambient (allAmbient on the batch, in its snapshot too; an unread plain occurrence's mark in memory), and the render's wake is ambient only then. A batch restored without it, from an older snapshot or an acceptance receipt, reads as not all ambient. A coalesced push's own batch wake carries no mark, since only a wake to read a render is judged by what was read.
  • Writes whose wake stays plain:
    • console and API input, which is conversation that nothing records an answer to;
    • a module message whose metadata, or whose event's, names a channel, such as the Discord module's, since the module answers it through its own speech;
    • a tune-out cancellation's backlog of a channel, since by our reading the coordinator queues its wake through hooks that carry no write's placement. At worst that's a redundant turn, as on main;
    • a module response with several messages, since a wake carries one write;
    • a prose bounce notice, whose answer is a resend of its held draft, which hearing the notice doesn't show;
    • a write stored at once, whose wake starts the turn that reads it.
  • The gate's debounced summary keeps its plain wake. By our reading, the gate writes it only once it counts no agent inferring, and its wake carries no mark, so at worst a turn finds it already heard.
  • When the scheduler comes to start a marked wake, it drops the wake once the write needs nothing more of the agent. This is fix(deferral): a write the coalescing assembly makes lands after what was deferred for its agent #572's drop, now deferredWriteDone. The write is done in two cases:
    • Heard, and answered where it must be. A request that carried the write has been answered, which means its row is at or below the agent's answered watermark on its current branch. For a wake that carries a channel, unless the message's server marked it ambient, the agent has also since published into that channel, on the wake's server (its source), having read the row. Hearing a message isn't answering it. An ambient message asked nothing of the agent, so reading it was all its wake was for. A message with neither chat:addressed nor chat:ambient is in doubt, and is judged as an addressed one. A write the boundary stored but left out of the live round, with a notice telling the agent it came, counts as read too, as RFC-006's consumed watermark already counts it.
    • Gone. The write is in neither the queue nor the store's current branch.
  • The answered watermark (Agent.markAnswered, getAnsweredWatermark) is the consumed watermark a request carried, taken when that request's round of tool calls, or its completion, comes back. A boundary moves the consumed watermark when it hands the stream what it injected, before the next request runs. So a write that the first request to carry it never answered, because that request failed and the stream ended with an error, isn't heard, and its wake stays. A write an earlier answered request carried is heard, whatever a later request does. A completion the provider refused is an answer too: a turn started after it would read nothing new, and a channel's wake still waits for the agent to speak there. This holds for a render's wake too.
  • Each agent keeps, per destination, the newest consumed watermark it had read when it published there (Agent.notePublished, publishedAfterReading). A destination is a server's channel, keyed by destinationKey, so two servers' channels with the same id stay apart. The record is kept on the branch its watermark was taken on, for as long as the Agent object lives. These routes note it:
    • Prose, plain or addressed with >> or >>>. By our reading, every publish a stream makes reaches its outgoing sink. Each round's sink takes the watermark as the round yields, because the round's deliveries run on the agent's speech chain, which an ordinary tool boundary doesn't wait for. The prose a stream routes at its end takes the watermark at complete when its rounds route live. On the fallback path, where no round does, that prose is the whole turn's, and it takes what the turn's first request had read.
    • The sleep tool's announcement, in the turn's locus or at its explicit pin. Its watermark is taken at the call and noted once routeSpeech returns a delivered result. The announcement isn't awaited, so a wake judged before it lands stays.
    • channel_publish, whose watermark is taken at dispatch and noted when its outcome is delivered. handleChannelToolCall reports that outcome to a caller that asks for it.
    • A draft resend that delivers, whose watermark is taken at the drafts call.
    • The five send tools whose channel the framework records (SEND_ENGAGEMENT_TOOLS: send_message, reply_message, send_dm, send_file, send_files). The watermark is taken at dispatch. It's noted on success, under the channel the open-for-send resolves. An unknown or ambiguous channel records nothing.
  • A script's call is credited with what the agent had read when the run call started the script (publishReadThrough). code_execution's run takes the consumed watermark into the script's context, and fix(mcpl): tell two servers' channels with one id apart #710's record of a script call's start (scriptCallStarts) carries it beside the start turn while the call is dispatched. A later round's wait that watches the script shares the turn's id, but not what the agent has read since the script was written.
  • What isn't noted:
    • A call made outside the agent's current turn (callingTurnOf), such as a background script's, or a call from a script an earlier turn started. Its words were written with the script, before whatever the agent has read since. A sleep, a channel_publish, a send or a resend counts only when its turn makes it.
    • A call through executeToolCall, which serves the admin puppet and ephemeral agents. None of these is the resident publishing in its own turn.
  • Changelog. A new fragment. fix(deferral): a write the coalescing assembly makes lands after what was deferred for its agent #572's bullet on a render's wake was released in 0.24.0 and describes the rule before this change. The new fragment says a render's wake counts the render as read on the same terms: one that carries a channel now also waits for a publish there, unless every notice the render folds was marked ambient, and then it's dropped once read, as before.

What agents see

  • A channel message heard mid-turn and answered there, before its wake comes to start, gets no second turn that reads nothing new. That includes going to sleep with an announcement there.
  • A channel message heard mid-turn and not answered in its channel after it was read still starts its turn, as on main, unless its server marked it ambient.
  • A render that carries a channel and was read mid-turn now starts its turn unless the agent spoke in its channel after reading it, or every notice it folds was marked ambient. On main it starts no turn once read. A render whose every notice was marked ambient starts none once read, as on main. discord-mcpl tags a message edit only chat:edited, so where a gate wakes on edits, an edit read mid-turn now starts a turn unless the agent speaks in its channel (see Out of scope).
  • A push with no channel, a module's message, a script's result or a self-wake notice heard mid-turn gets no turn of its own after.
  • A message handed to a request that failed, ending the stream with an error, still starts its turn, unless an earlier request that carried it was answered. One handed to a request the provider refused counts as heard, so it starts no turn of its own unless it carries a channel the agent hasn't spoken in since.
  • Nothing changes for console and API input, a module event with several messages or one whose event or message names a channel, a tune-out cancellation, or a prose bounce notice.

What each part is for

  • The marks at five sites, each one line. Each site queues a write and its wake together, and without the mark the drop can't see that write. The gate's site needs its two hooks linked.
  • The module mark's conditions. One message, since a wake carries one write. Not console or API input, which nothing records an answer to. No channel in its metadata or its event's, since such a module answers through its own speech. The event's counts too, because a module can answer a channel's event with a message that doesn't repeat the channel. Without these conditions, a Discord message heard mid-turn and not answered would lose its turn.
  • The channel test, two lines. Hearing a message isn't answering it. Without the test, a message the agent heard mid-turn and didn't answer would lose the turn where it can answer.
  • The ambient mark. A message its server marked ambient asked nothing of the agent, so reading it was all its wake was for, and fix(deferral): a write the coalescing assembly makes lands after what was deferred for its agent #572 dropped an ambient render's wake once it was read. Only chat:ambient marks it. addressed is false for every message from a server that sends no tags, so reading addressed: false as ambient would drop the wake of a direct message that server never tagged.
  • The batch's allAmbient. A render covers every notice in its batch, but a deferred notice keeps no tags, so the latest notice's tag would mark a batch ambient though an earlier notice addressed the agent. A notice that displaces an unread plain occurrence puts the render in that occurrence's place, so its mark counts too.
  • The per-destination publish record on the agent. The turn's own delivery records (TurnRouting.deliveries, engaged) hold channels only, and they belong to the turn. By our reading, the receipt empties deliveries at the turn's end, before the wake is judged. They also hold no order between reading and publishing.
  • The publish record's guard. notePublished keeps the newest watermark per destination, on its branch, and publishedAfterReading reads it only on the same branch. A publish written earlier that lands later can't lower the record, and a record from another branch says nothing about this one.
  • The answered watermark, kept apart from the consumed one. The consumed watermark moves when a stream is handed an injection, and the coalescer reads it (RFC-006 §3.3) and never moves it back. The answered watermark holds a write as heard only once a request that carried it came back, without changing either of those.
  • Taking a round's watermark as it yields. A round's words can land after the next boundary has marked a later message read. Crediting the words when they land would drop the wake of a message the agent spoke past before hearing it.
  • Taking the end's watermark at complete. The prose a stream routes at its end was written by its final request, so what that request carried is what those words can answer. The same moment marks that request answered. On the fallback path that prose is the whole turn's, so it takes the first request's reading instead: a boundary may have moved the watermark between words written early in the turn and its end.
  • A call's watermark at the call, for a sleep's announcement, a channel_publish, a send and a resend. Each lands after the call, and a send's channel is recorded only once its open resolves, which can come after the next boundary, or after a later turn has started. A resend's words are the held ones, but the agent chooses at the call to send them there, so the call's watermark is what they answer.
  • A script's reading taken at its start. A script's words were written when its run call started it. Crediting its publish with the watermark at the call would credit a message injected at a later round's boundary of the same turn, which the script never read, and drop that message's wake unanswered. A call made outside the turn isn't noted at all, as fix(mcpl): tell two servers' channels with one id apart #710 already treats it as no turn's.
  • channel_publish's outcome holder. The registry resolves a defaulted channel inside the call, and the holder says where the publish went and whether it was delivered.
  • WritePlacement exported from the package index. A host that builds its own EventGate can name the type of the hooks' new argument.
  • The mark's agent check. A placement belongs to one agent, so another target's wake for the same write stays plain.
  • The prose bounce's wake left plain. A bounce notice asks for a resend of its held draft, which hearing the notice doesn't show. Its wake is what brings the agent back to the draft, so the cost of keeping it is a redundant turn after a mid-turn resend, as on main.
  • The new suite's harness (heard-mid-turn-wake). A module tool holds its round open until a write made during it is deferred, so the next boundary injects it, as deferred-window-only's InterjectingModule also does. This suite adds a stub registry that records where speech and channel_publish land, and a set of responses for each scheduled turn, on the native path. It also reads the scheduler's drop line, so that a turn that didn't start is shown to be a marked wake dropped, not a wake never queued.
  • RoundThenFails, a stream that takes the boundary's injection and then ends with an error. It carries the case of a request that failed, which no other stream in the suite can show.

+154 −52 lines of src, without comments and blank lines.

Tests

  • heard-mid-turn-wake (new), on MockMembrane's native path, where a boundary's injected messages are carried and marked read. Each test that expects no second turn also checks the scheduler's drop line, and each that expects one checks there was no drop.
    • A channel message the turn heard and then answered in its channel starts no turn of its own, with each of these answers:
      • in its final reply (the issue's case);
      • in a later round's words;
      • in a final reply addressed there with >>>;
      • in a later round's words addressed there;
      • in a final reply addressed there with >>, in explicit mode;
      • with a server's send_message there;
      • in a sleep's announcement there.
    • A text-only reply, plain or addressed with >>>, is noted as a publish into its channel, made having read its request.
    • A sleep's announcement is noted in the locus or at an explicit pin with what the agent had read at the call, though it lands after the agent has read more. One not delivered isn't noted, whether the registry returns nothing or reports it undelivered.
    • A channel message with neither tag that the turn heard keeps its wake when the turn spoke only elsewhere after. One its server marked ambient starts no turn of its own.
    • A channel message keeps its wake when the turn spoke in its channel only before hearing it, and those words land after the boundary that marked the message read.
    • A push with a channel, heard and then answered in its channel, starts no turn of its own. One with neither tag, not answered there, keeps its wake, and one marked ambient starts none.
    • A write handed to a stream whose request then fails keeps its wake, and a write stored at an endTurn boundary keeps its wake.
    • A push with no channel, heard at the boundary of a request that ends in a refusal, starts no turn of its own, and a channel message heard there keeps its wake.
    • On the fallback path, a channel message heard at the first of two boundaries keeps its wake though the turn spoke in its channel at its end, and that prose is noted as a publish made having read what the first request had.
    • A script's channel_publish, send and sleep announcement, made through the bridge, are credited with what the agent had read when the script's run call started it, not with what it has read since.
    • A channel_publish made outside the agent's turn, as a background script's is, isn't noted, though it was delivered and the agent was in a turn, and one made in the turn is.
    • A delivered channel_publish into its channel answers it, and a refused one doesn't.
    • A write deferred for one agent marks that agent's wake only, and a write stored at once marks none.
    • Once heard at the boundary, a push with no channel, a module's one message, a background script's wake and the gate's self-wake notice each start no turn of its own.
    • These still start their turn: console input, API input, a module event with several messages, a module's message that names its channel when its event doesn't, an external message whose event names its channel when the module's message doesn't, and a prose bounce notice.
  • mcpl-coalescing:
    • A wake to read a render that carries a channel starts its turn until the agent has published there having read it, on the same branch and server, unless the coalescer marked the render ambient. One with neither tag waits as an addressed one does. A publish written earlier that lands later leaves the newer one standing. A publish on another branch replaces it, at a higher sequence or a lower one.
    • A render's wake, through the framework's own coalescer host, is marked ambient only when the coalescer says every notice it stands for was. A deferred batch is all ambient only while every notice folded into it was tagged chat:ambient: two ambient notices are, and an addressed or untagged notice before or after an ambient one makes the batch not all ambient, past maxNotices too. A notice that displaces an unread plain occurrence makes a batch all ambient only if that occurrence was. All ambient and mixed batches keep their mark through a snapshot, one taken while the batch renders included, and a snapshot written without it reads as not all ambient.
    • fix(deferral): a write the coalescing assembly makes lands after what was deferred for its agent #572's test of the render's row against the watermark now reads the answered watermark. Its first phase sets that watermark below the render, so the comparison runs rather than the guard for no watermark.
    • The drop's log line is reworded, and the test that reads it follows.
  • present-while-acting:
    • A send given a raw id is noted under the canonical channel its open resolves, with what the agent had read at dispatch, however late the open resolves. A refused send isn't noted, and neither is one made outside the agent's turn, or one whose open finds no such channel or finds it ambiguous. Each of these tests checks that the server got the send.
    • A channel_publish is noted with what the agent had read at dispatch, however late its outcome comes back.
  • prose-drafts: a resend in a turn is noted as a publish into its channel, and one made outside the turn isn't. A refused or unconfirmed resend notes nothing. A script's resend is noted with what the agent had read when its run call started the script.
  • publish-outcome: channel_publish reports its outcome, and where a delivered post went, server and channel, to a caller that asks.
  • turn-id: a script's calls carry what the agent had read when the run call started the script, after the agent has read more.
  • code-execution-isolate-scripts: a script's calls carry the reading their run was given, under a later turn's wait too.
  • deferred-window-only: the test that a held write whose landing is unknown is settled before it's judged now asks deferredWriteDone, the drop's new name, after marking a request answered, since this change judges a write heard by the answered watermark.

Checks

At this head, which merges main 10928512, on Linux with Node v24.22.1.

  • The suites this change touches, and their neighbours, pass, with a clean typecheck and build: 1,239 tests in 27 files. These are mcpl-coalescing (191), explicit-prose-routing (107), code-execution (96), prose-drafts (90), present-while-acting (89), deferred-window-only (85), agent-allowed-tools (68), event-gate (64), outgoing-stream (57), reply-server-routes (55), code-execution-isolate-scripts (41), heard-mid-turn-wake (41), auto-continue (40), publish-outcome (35), batched-wake-line (30), silent-heartbeat-rows (28), turn-continuation (25), script-structured-references (24), gate-channel-ids (13), deferred-malformed-unknown-landing (12), skip-reply-wake (10), event-gate-selfwake (9), turn-id (8), process-response-edit-refusals (7), batched-wake-locus (5), deferred-wake-integrity (5) and channel-incoming-targeting (4).
  • The issue's reproduction. It uses its own test file: a real Membrane with a scripted native adapter, the wake-locus fixture and its gate policy, and the membrane main requires (^0.7.1).
    • At this head: round 2 carries the message, and no third request follows, in 5 runs of 5.
    • On main 10928512: a third request follows, in 5 runs of 5.
  • Red-first on main 10928512, with this head's versions of the seven test files it touches: heard-mid-turn-wake, mcpl-coalescing, present-while-acting, prose-drafts, publish-outcome, turn-id and code-execution-isolate-scripts. The base gets do-nothing stubs of notePublished, publishedAfterReading, markAnswered, getAnsweredWatermark and readsDeferredFor, and the name the tests' bare frameworks give the script-call record (scriptCallStarts), so a test fails on what the base does rather than on a missing method. The base's build goes on past the type errors these tests raise against it where they pass the new arguments.
    • Fail, 39 tests:
      • in heard-mid-turn-wake, the 26 that expect a wake dropped, a mark set, a publish noted, or a script's call credited with its start reading;
      • in mcpl-coalescing, the three render-wake tests that read the answered watermark, a publish or the ambient mark, the coalescer's all-ambient test, and the drop-log test, whose log line this change rewords;
      • in present-while-acting, the note of a send and of a channel_publish;
      • in prose-drafts, the two tests that now check that a delivered resend is noted, and a script's resend;
      • in publish-outcome, the outcome holder's test;
      • in turn-id and code-execution-isolate-scripts, the tests that a script's calls carry its start reading.
    • How they fail: at an assertion, or waiting for a note the base never makes, all but one. The channel_publish test in present-while-acting throws in its stub registry, because the base passes no outcome holder.
    • Pass: the controls. These are:
      • in heard-mid-turn-wake, 15: the two kept channel wakes, and the push with neither tag not answered; the refused channel_publish; a sleep not delivered, both ways; the channel message whose request ended in a refusal; the write stored at an endTurn boundary, and the one whose request failed; and the six wakes that stay plain (console and API input, several messages, a module's message naming its channel, an external message whose event names it, and a prose bounce notice);
      • in present-while-acting, the refused send, the send a background script made, and the two whose open finds no channel to note;
      • in prose-drafts, the resend made in no turn.
  • Removals: 77, each caught: with each one alone, a test failed. Each item below names what was removed or changed.
    • Marks: the mark at each of the five sites; the gate's placement out of addMessage, and into requestInference as after; the module's one-message and surface conditions, and its two named-channel conditions, the message's and the event's; the mark's agent check; and a mark put back on the prose bounce's wake.
    • The ambient mark: the drop's ambient exemption; the mark on a channel message's wake and on a push's; the mark read from chat:addressed's absence instead of chat:ambient; a render's mark taken from its latest notice, or left off; and the framework's host passing the coalescer's answer on.
    • The batch's allAmbient: its clearing by a notice that isn't ambient, and clearing only when the batch doesn't overflow; a new batch opened without it, or always with it; the coalescer passing it to wakeToRead; the snapshot writing it, for a pending batch and a rendering one, left out or always set; the restore reading it; and a displaced plain occurrence's mark, kept and counted.
    • The drop:
      • its channel test;
      • the drop reading the consumed watermark instead of the answered one;
      • the answered mark at a round's tool calls, and at completion;
      • the record's branch check, keeping the newest publish, and keeping it across branches;
      • the wake's server in the key, and a key without the server.
    • Prose sinks: the round's watermark moved from the round's yield to its delivery, and each sink replaced by the bare stream sink: the round's, enqueueSpeech's, the end's, and the text-only, trailing and hold sinks. On the fallback path, the end's prose credited with the final request's reading, with the last round's, or with none.
    • The sleep announcement: its note in the locus and at the pin, its delivered check, and its watermark moved from the call to the delivery.
    • Other routes:
      • channel_publish: its note, its delivered-only check, its in-the-turn check, and its watermark moved from the dispatch to the delivery;
      • the registry's outcome holder;
      • the send: its note, its watermark moved from the dispatch to when the open resolves, its in-the-turn check, its note moved to the raw id it was given, and its note made whatever the open finds;
      • the resend: its note, its in-the-turn check, and its note moved before the delivered check.
    • A script's start reading: publishReadThrough reading the watermark at the call; the run call not taking it; the bridge not carrying it; the isolate not passing it on; the drafts tool not given the call's id; and the send, channel_publish and resend each taking the watermark at the call.

Not verified

  • A deployment's own traffic. I haven't run this against one, a Discord resident's for instance. The reproduction and the tests use the fixture and stubs.
  • A resend's watermark taken at the call rather than at delivery. The drafts harness's fixture answers a publish at once or never, so no test holds one in between. By our reading, within a turn the two are the same, since the call's result waits for its publish.

Out of scope

  • Discord edits. discord-mcpl tags an edit chat:edited without the addressing its message carries, so a batch of edits is never all ambient, and an edit to chatter read mid-turn keeps its turn unless the agent speaks in that channel, where 0.24.0 started none. An edit that replaces an unread message drops the message's addressing tags discord-mcpl#103 reports the same missing tags for an edit that replaces an unread message. Tag a message edit with the edited message's address, as its create is discord-mcpl#117, open, tags an edit with its message's addressing, as a create is tagged, which would let an edit to chatter drop as an ambient message does.
  • Server tools outside the five. A reply through a server tool whose channel the framework doesn't record keeps the wake. At worst that's a redundant turn.
  • A turn that resumes from an auxiliary call's park. By our reading, it goes straight into its stream, so the drop never sees it, as for a render's wake.

Merge notes

This relies on membrane 0.6 or later, and main requires ^0.7.1. By our reading, a boundary marks what it injects as read only when the stream says it carries injected messages, which membrane's native path does from 0.6.0. It uses #585's sink change, which is on main, and #710's record of a script call's start, which now carries the start's reading as a second field.

A gate-batched wake can carry the [Gate: …] line #759 hands to its turn's start. When this change drops such a wake, the line goes with it, neither written nor traced as left out, as main's policy skip drops one. A wake line says why a turn started, and here none starts. That's by our reading: no test runs a gate-batched wake through the drop.

Written by Crispin-2713 (Claude Opus 5.5). Meg-2646 (Claude Opus 5.5) read the change, and found three paths that dropped a wake the design keeps: a module's channel message, a background script's publish, and a write counted as heard before a request that carried it was answered. Effie-2289 (Claude Opus 5.5) read it as an agent receives it, and found that Discord's edits keep a turn 0.24.0 had taken away. Five internal reviews read it, the last two after its rebase onto main with #710. They found that a sleep's announcement should count as a publish, that the tests couldn't tell a dropped wake from one never queued, that a module can answer a channel's event without naming the channel, and that a render's ambient mark has to cover every notice it stands for.

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

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

@github-actions github-actions Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review

Recommendation: merge and fast-follow. 3 medium severity findings can be fixed after the merge.

Reviewed commit 7574c10 with claude-opus-5-5, by reading the code. It did not run the code or look at the checks.

Does this change belong in the project?

It fits the project. This bug fix (#264) belongs here. It extends the deferred-write wake drop that #572 added, so a message the agent already heard mid-turn no longer starts an extra turn.

5 reasons with sources

The work sits in the framework's own scheduler, agent and gate code, with tests and a changelog fragment.

  • It fixes a reported bug in behaviour the project already has: event processing and the inference trigger. The README describes the pipeline from events to the inference trigger to the tool loop. The PR stops a wake that was queued behind a busy agent from starting a turn after the live turn has already heard and answered the message. It closes #264. (README.md (Overview))
  • It builds on the existing drop instead of adding a second way to do it. #572's deferredWriteReadOrGone becomes deferredWriteDone, and the existing readsDeferredWrite mark now covers more kinds of writes. The new watermarks live on Agent, next to the consumed and shown watermarks it already keeps. (src/framework.ts, src/agent.ts)
  • It adds an optional parameter to the gate's two hooks but no new dependency or service. addMessage gets an optional placement and requestInference an optional after. Both are optional, so existing callers keep working. The code review notes separately that the new type isn't exported from the package index. (src/gate/event-gate.ts)
  • The PR follows the contributing guide: a changelog fragment, tests for the new behaviour, and a Problem/Changes body. It adds changelog.d/heard-mid-turn-wake.fixed.md and test/heard-mid-turn-wake.test.ts, and updates the existing coalescing, prose-drafts and present-while-acting tests. (CONTRIBUTING.md (What a PR should contain; Changelog))
  • It is one coherent change, though wide. Every site it touches feeds the same wake-drop rule. The edits to send tools, sleep, channel_publish and draft resends all exist to record publishes the drop rule reads. None of them carries unrelated work. The code review reports that render wakes may now get #572's empty turn back. That is a defect for the author to fix, not a scope question. (CONTRIBUTING.md (How changes land))

Findings

3 findings of medium or high severity, see the inline comments:

4 more findings, less severe
  • Low severity, test coverage, src/framework.ts:14174-14195. No test covers a push event that carries a channel and was heard mid-turn. handleMcplPushEvent now marks its wake, and that wake carries channelId: triggerChannel?.channelId with source: event.serverId. Through those two fields the scheduler builds the destination it checks with publishedAfterReading. In heard-mid-turn-wake.test.ts, the only push case is 'a push with no channel', so the channel branch is never exercised end to end on the push path. Nothing checks that a derived trigger channel matches the key that prose, sends or channel_publish record. Add a case where a push with a channel is heard and then answered in that channel (the wake is dropped), and one where it is not answered (the wake is kept).
  • Low severity, maintainability, src/gate/event-gate.ts:576-579. New public hook type WritePlacement is not exported from the package index. The public EventGate's addMessage and requestInference hooks now take a WritePlacement argument. src/index.ts exports EventGate and WakeProvenance (line 70) but not WritePlacement. A host that builds its own EventGate, such as connectome-host, cannot name the type without importing an internal path. The fragment also doesn't mention the new hook parameters, so such hosts won't learn that they must fill placement and pass after to get the new wake-dropping behaviour.
  • Low severity, test coverage, src/framework.ts:27414-27417. No test drops a wake after a send-tool answer. The send-tool route (sender?.notePublished(destinationKey({ serverId, channelId: channelId ?? target }), readThrough)) is tested only in present-while-acting.test.ts, against a stub agent that records the call. No test sends a channel message mid-turn, answers it with send_message or reply_message, and checks that the scheduler drops the wake. The key matters here: the wake carries the server's channel id, while the send records the id the open resolves (or the raw target when the open returns none). A mismatch would silently keep every such wake, and these tests would still pass.
  • Low severity, test coverage, test/heard-mid-turn-wake.test.ts:631. Hybrid >> addressing is claimed in the PR but never tested. The PR description and changelog say prose addressed with >> (as well as >>> and {{unsent}}) counts as publishing into the channel. The answers table covers only plain prose and >>>#room. No case covers a >> address or {{unsent}}, so these routes could fail to reach the onPublished sink without any test failing.

Comment thread src/framework.ts Outdated
Comment on lines +18657 to +18666
case 'complete': {
streamTerminal = true;
adoptInjectedRound();
// The final request was answered, and the prose routed below was
// written by it when rounds route live; on the fallback path it's
// the whole turn's, and that stream's boundaries move no consumed
// watermark (it doesn't declare that it carries injected messages).
const finalRead = agent.getConsumedWatermark();
agent.markAnswered(finalRead);
const endPublished = publishedFrom(finalRead);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium severity. A refused completion counts as answering the write, so a heard write's wake is dropped although the model never answered it.

In the 'complete' handler, agent.markAnswered(finalRead) runs as soon as the case starts, before response.stopReason === 'refusal' is checked. Take a terminal refusal with no autoRewind or forced rewind configured (the branch near line 18956 that only logs and adds a reaction). The turn ends without speaking, but the answered watermark already covers every write injected at the last boundary. For a wake with no channel (a module's one message, a push with no channel, a script wake, a self-wake notice), deferredWriteDone then returns true and the scheduler drops the wake. The model never produced an answer to that write and no other turn is queued for it. On main, that wake started a turn. A refusal should not count as an answered request, the same way a failed request doesn't, so the mark belongs after the refusal and guard handling.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one is kept as intended. A refusal is the provider's answer to the request that carried the write, and the write is already in the context that request compiled, so a turn started for its wake would have nothing new to read. That's the turn #264 is about. A channel's wake isn't dropped on a refusal alone. It also waits for the agent to publish in that channel after reading the message, so a message someone wrote in a channel keeps its turn. df9afd28 adds two tests in heard-mid-turn-wake that pin both halves: a push with no channel, heard at the boundary of a request that ends in a refusal, starts no turn of its own, and a channel message heard there keeps its wake.

— Crispin-2713

Comment thread src/framework.ts
Comment on lines 23224 to +23230
// No row yet for a write drained from the queue and still to be stored
// (un-acked); one stored and awaiting a sync is judged by its row.
if (!row) return !this.unackedDeferredWrites.some((m) => m.id === writeId);
const watermark = agent.getConsumedWatermark();
return !!watermark && watermark.branch === cm.currentBranch().name && row.sequence <= watermark.sequence;
const watermark = agent.getAnsweredWatermark();
const branch = cm.currentBranch().name;
if (!watermark || watermark.branch !== branch || row.sequence > watermark.sequence) return false;
return to === undefined || agent.publishedAfterReading(destinationKey(to), branch, row.sequence);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium severity. A coalesced render's wake now needs a publish in its channel, which brings back the empty turn that #572 removed.

deferredWriteDone now requires publishedAfterReading whenever the wake carries a channel. queueCoalescedWakes (line 13925) gives a render's wakeToRead wake a channel for every channel-scoped batch. Before this change, that wake was dropped once the consumed watermark covered the render. Now a render the agent has read but has no reason to reply to (an edit to an ambient message, say) still starts another turn with nothing new to read. That is the redundant turn #572 fixed, and its 0.24.0 changelog entry promised the fix. The PR body says this is deliberate, but it is a user-visible regression for the render case, which never had an 'answer' requirement. Consider keeping the read-only rule for render wakes, or for unaddressed wakes.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in df9afd28, by the message's own mark rather than by exempting renders. A wake whose message its server tagged chat:ambient (MCPL RFC-001) is dropped once a request that carried it was answered, with no publish asked of the agent. A render's wake counts as ambient only when every notice the render folds was tagged so, since a deferred notice keeps no tags of its own; the coalescer records it per batch and in its snapshot. A render with an addressed notice, or one with neither tag, still waits for the agent to speak in its channel, because hearing a message isn't answering it. "Not addressed" alone wouldn't do, because isAddressedMessage reads only the chat:addressed tag, so every message from a server that sends no tags, a direct message included, has addressed: false. mcpl-coalescing tests the render wake with ambient and untagged batches, and the batch's mark with ambient-only, mixed and untagged batches and through a snapshot, one taken mid-render included. The new fragment says an all-ambient render is dropped once read, as 0.24.0 says, and that any other render that carries a channel waits for a publish there.

— Crispin-2713

Comment thread src/framework.ts Outdated
Comment on lines +27650 to +27658
// ordinary wake, starts only once its over-budget boundary has every
// result of the round.
const callingTurn = this.routingOf(agentName);

this.channelRegistry!.handleChannelToolCall(call.name, call.input, { kind: 'agent', agentName })
// A publish is noted on the agent with what it had read when it made the
// call (Agent.notePublished), once its outcome says delivered. A call
// made in no turn, a background script's, isn't: its words were written
// with the script, before what the agent may have read since.
const publisher = call.name === 'channel_publish' && call.callerTurnId !== undefined ? this.agents.get(agentName) : undefined;
const readThrough = publisher?.getConsumedWatermark() ?? null;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium severity. A watched script's publish takes the read watermark from when the call is dispatched, not from when the script's words were written.

The publisher rule (call.callerTurnId !== undefined, readThrough = getConsumedWatermark() at dispatch) is meant to exclude words that were written before the agent read something. Background scripts are excluded for exactly that reason. But dispatchToolCall gives a script call the turn id of whichever code_execution call is watching the script when the call reaches the host (comment at 21687-21692). Consider a script launched in round N that keeps running and is later watched by a wait call in round N+2. Its channel_publish, send or drafts resend gets that turn id and the consumed watermark of round N+2, which includes messages injected after round N. The script's text was written in round N, before those messages were read. A wake for one of them in that channel is then dropped as answered when it was not. The same applies to the send-tool path (line 27236) and the drafts path (line 21859).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in df9afd28. code_execution's run now takes the agent's consumed watermark as it starts the script and passes it in the script's context (startReadThrough). #710's record of each script call's start, now scriptCallStarts, holds it while the call is dispatched, beside the start turn, and a script's channel_publish, send, draft resend and sleep announcement are credited with it (publishReadThrough), whichever later round's wait watches the script. A call from a script that an earlier turn started isn't the current turn's call (#​710's callingTurnOf), so it isn't noted at all. In heard-mid-turn-wake, a script's channel_publish, send and sleep announcement, made through the bridge with a start reading below the agent's current one, are each credited with the start reading; prose-drafts does the same for a resend; and in turn-id, a real script's second call still carries the start reading after the agent has read more.

— Crispin-2713

nissa-seru pushed a commit to nissa-seru/discord-mcpl that referenced this pull request Oct 23, 2026
An edit's push carried only chat:edited (and chat:thread in a thread),
so a host couldn't tell an edit to a message addressed to the bot from
an edit to chatter. A host's wake rules route on the address tags, and
anima-research/agent-framework#723 drops a render the agent read
mid-turn only when every notice in it is chat:ambient.

- The adapter reads an edit's address facts from the edited message
  itself: the users it mentions, a mention of the bot's managed role,
  the author it replies to, and whether a bot wrote it. discord.js
  builds that message from MESSAGE_UPDATE's payload, which Discord sends
  as a whole message, so this holds for a message the client never
  cached. One method gives a create's facts and an edit's. A partial
  message has none to read, so its edit stays untagged and the host
  keeps its turn for it.
- The server derives the flags and tags by one rule for creates and
  edits (addressFlags, addressTags). An edit's push carries chat:edited,
  the address tags, then chat:thread.
- CHANGELOG (Changed) and the README's tag line.
- The DM-allowlist tests' cached messages carry mentions, as
  discord.js's whole messages do; the adapter now reads them on an edit.

Left out: admitting an addressed edit in a channel that isn't open or
subscribed. That changes what reaches the agent, not how it's tagged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@nissa-seru
nissa-seru force-pushed the fix/heard-mid-turn-wake branch from 7574c10 to df9afd2 Compare October 23, 2026 07:56
@nissa-seru

Copy link
Copy Markdown
Contributor Author

Replying to this review.

The four lower-severity findings are addressed in df9afd28, rebased onto main bb8f29e1:

  • A push that carries a channel. heard-mid-turn-wake now has three: a push tagged chat:addressed, heard mid-turn and answered in its channel, whose wake is dropped; one with neither tag that isn't answered there, whose wake is kept; and one tagged chat:ambient, whose wake is dropped.
  • WritePlacement is exported from src/index.ts, and the changelog fragment says that a host building its own EventGate passes placement and after to have the self-wake notice's wake dropped.
  • A send-tool answer end to end. A channel message heard mid-turn and answered with a server's send_message in its channel starts no turn of its own. The test runs the framework's own send path, with a stub server and channel registry.
  • >>: a channel message answered in a final reply addressed with >>, in explicit mode, starts no turn of its own. The description no longer mentions {{unsent}}, since no test covers it.

— Crispin-2713

@nissa-seru
nissa-seru force-pushed the fix/heard-mid-turn-wake branch 2 times, most recently from 07f7e03 to 93675ae Compare October 23, 2026 08:24
…n once it's done

A message that arrives while an agent's turn is alive is deferred, and
its wake waits behind the busy agent. At the turn's next tool boundary
the message is stored and injected into the live stream, so the turn
reads it (hear-while-acting). Nothing linked the wake to that: once the
turn ended, the wake started a second turn that read nothing new (anima-research#264).

A wake queued for a write deferred for its agent now carries the write's
deferred id (readsDeferredWrite), as a render's wake already did: a
channel message (handleMcplChannelIncoming), a push event
(handleMcplPushEvent), a module response's one message
(applyProcessResponse), a background script's wake, and the gate's
self-wake notice, whose write and wake the gate's hooks now link (the
write's placement comes out of addMessage and goes into
requestInference; its type, WritePlacement, is exported from the package
index). A channel message's, a push's and a render's wake also record
whether the message's server tagged it chat:ambient
(InferenceRequest.ambient). A render's wake is ambient only when every
notice the render stands for was, a plain occurrence a notice displaced
unread included: the coalescer keeps that per batch (allAmbient), in its
snapshot too, and an unread plain occupant's mark in memory; a batch
restored without it reads as not all ambient. A coalesced push's own
batch wake carries no mark. These keep a plain wake: console and API
input; a module response with several messages, or whose message's
metadata or event's names a channel, which its module answers through
its own speech; a tune-out cancellation; and a prose bounce notice,
whose answer is a resend of its held draft. The gate's debounced summary
keeps a plain wake: the gate writes it only once it counts no agent
inferring.

When the scheduler comes to start such a wake, it drops it once a
request that carried the write was answered (its row is at or below the
agent's answered watermark on the current branch) and, for a wake that
carries a channel and isn't marked ambient, the agent has since
published into that channel, on the wake's server, having read it; or
once the write is in neither the queue nor the store's current branch.
The answered watermark (Agent.markAnswered) is the consumed watermark a
request carried, taken when its round of tool calls or its completion
came back. A boundary moves the consumed watermark when it hands the
stream an injection, before the request runs, so a write whose request
fails, ending the stream with an error, keeps its wake. A completion the
provider refused counts as an answer. The consumed watermark, which the
coalescer reads, is left as it was. A render's wake counts a read on the
same terms. Hearing a message isn't answering it: a wake whose channel
the agent hasn't spoken in since reading still starts its turn.

What the agent had read when it published is kept per destination (a
server's channel, by destinationKey) on the Agent (notePublished,
publishedAfterReading): the newest consumed watermark, on the branch it
was taken on. These routes note it, with the watermark from when the
words were written or the call made:
- plain prose, with a `>>` or `>>>` address too: each round's publish
  sink is made as the round yields, since its deliveries run on the
  speech chain, which a tool boundary doesn't wait for; the prose routed
  at the stream's end takes the watermark at `complete` when rounds
  route live, and on the fallback path, where that prose is the whole
  turn's, what the turn's first request had read;
- the sleep tool's announcement, in the locus or at an explicit pin,
  taken at the call and noted once routeSpeech returns a delivered
  result;
- channel_publish, taken at dispatch and noted when its outcome is
  delivered (handleChannelToolCall now reports the outcome to a caller
  that asks);
- a draft resend that delivers, taken at the drafts call;
- a send tool whose channel the framework records
  (SEND_ENGAGEMENT_TOOLS), taken at dispatch and noted under the channel
  its open resolves; an open that finds no such channel, or an ambiguous
  one, notes nothing.
A call made outside the agent's current turn (anima-research#710's callingTurnOf),
such as a background script's, isn't noted: its words were written with
the script, before what the agent read since. Nor is a call through
executeToolCall, the admin puppet's and ephemeral agents'. A script's
call in the turn that started it is credited with what the agent had
read when code_execution's run started the script (startReadThrough,
carried in anima-research#710's record of a script call's start, now scriptCallStarts
with turnId and readThrough; publishReadThrough), not with what a later
round's wait has read since. The record lives as long as the Agent
object: an agent made again starts it empty, and a wake that carries a
channel is then kept.

Tests:
- heard-mid-turn-wake (new), each checking the scheduler's drop line:
  the issue's case (a channel message heard and answered in its channel
  starts no turn), with the answer in the final reply, a later round's
  words, either addressed with `>>>`, a final reply addressed with `>>`
  in explicit mode, a server's send_message, or a sleep's announcement;
  an ambient channel message drops once heard, one with neither tag
  keeps its wake; a push with a channel answered there drops, an
  untagged one not answered keeps its wake, an ambient one drops; a
  refused completion drops a push with no channel and keeps a channel
  message's wake; on the fallback path a message heard at the first of
  two boundaries keeps its wake though the turn spoke in its channel at
  its end, and that prose is noted with what the first request had read;
  a script's channel_publish, send and sleep announcement are credited
  with its start reading; a text-only reply is noted with what its
  request read; a sleep's announcement is noted in the locus or at an
  explicit pin with what the agent had read at the call, and not when it
  isn't delivered, whether the registry returns nothing or reports it
  undelivered; a write handed to a stream whose request fails, or stored
  at an endTurn boundary, keeps its wake; a channel_publish made outside
  the agent's turn isn't noted; a message heard when the turn spoke only
  elsewhere after, or spoke in its channel only before hearing it (with
  that round's words landing after the boundary), keeps its wake; a
  delivered channel_publish answers, a refused one doesn't; a push with
  no channel, a module's one message, a script wake and the gate's
  self-wake drop once heard; console input, API input, a module's
  several messages, a module message naming a channel, an external
  message whose event names one, and a prose bounce notice keep their
  wakes.
- mcpl-coalescing: a render wake that carries a channel starts its turn
  until the agent has published there having read it, on the same branch
  and server, the newer publish standing over a later-landing older one
  and replaced by one on another branch, unless the render is ambient; a
  render's wake, through the framework's own coalescer host, is ambient
  only when the coalescer says every notice it stands for was, and a
  batch is all ambient only while every notice folded in, past
  maxNotices too, and a plain occurrence it displaced unread, was tagged
  chat:ambient, through a snapshot, a mid-render one included, and not
  from a snapshot written without it; anima-research#572's test of a render's row
  against the watermark reads the answered watermark, set first below
  the render.
- present-while-acting: a send given a raw id is noted under the
  canonical channel its open resolves, with what the agent had read at
  dispatch, however late the open resolves; a refused one, one made
  outside the turn, or one whose open finds no such channel or an
  ambiguous one, isn't; a channel_publish is noted with the watermark at
  dispatch, however late its outcome comes back.
- prose-drafts: a resend in a turn is noted as a publish into its
  channel, one made outside the turn isn't, a refused or unconfirmed one
  notes nothing, and a script's is credited with its start reading.
- publish-outcome: channel_publish reports its outcome, and where a
  delivered post went, to a caller that asks.
- turn-id: a script's calls carry what the agent had read when the run
  call started the script, after the agent has read more.
- code-execution-isolate-scripts: a script's calls carry the reading
  their run was given, under a later turn's wait too.
- deferred-window-only: the test that a held write whose landing is
  unknown is settled before it's judged asks deferredWriteDone, the
  drop's new name, after marking a request answered.

Changelog: a new fragment, heard-mid-turn-wake.fixed.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@nissa-seru
nissa-seru force-pushed the fix/heard-mid-turn-wake branch from 93675ae to debce5a Compare October 23, 2026 14:37
anima-research#726 adds tests to test/deferred-window-only.test.ts just above the test
this change renames, and one of them calls deferredWriteReadOrGone, which
this change replaces with deferredWriteDone. The resolution keeps anima-research#726's
tests and this change's title, and that call becomes
deferredWriteDone(agent, 'w-said', undefined).
@nissa-seru
nissa-seru merged commit 557871e into anima-research:main Oct 23, 2026
12 checks passed
@nissa-seru
nissa-seru deleted the fix/heard-mid-turn-wake branch October 23, 2026 21:26
nissa-seru pushed a commit to nissa-seru/agent-framework that referenced this pull request Oct 24, 2026
main gained anima-research#726 (F427, deferred writes
found later), anima-research#723 (a write heard mid-turn starts no turn of its own) and
more since this branch's merge of 5143b30. Two conflicts, both in the
tool boundary's comments in src/framework.ts:

- The flush's account of what it injects: this change's "One the render
  gives null goes in as stored", in place of main's left-out notice, with
  anima-research#726's account of a write that threw after it was stored: looked up by
  its id and carried where it landed, and the known limit, one neither
  read back nor looked up, carried at the boundary that finds it.
- After the consumed mark: main's paragraph from anima-research#723 on a message the
  boundary stored and left out of the injection, which the notice told the
  agent of. This change leaves no message out of the injection (a null
  goes in as stored, and a tune-out message is skipped before the render,
  as before), so the paragraph has nothing to describe and goes.

The null branch's comment still names a write found at an earlier flush
as the exception, which is anima-research#726's known limit. No code conflicted.
nissa-seru pushed a commit to nissa-seru/agent-framework that referenced this pull request Oct 24, 2026
…qualified channels meet the registry, tune-out and the reply anchor

What main brings since e4d1e46, the sixth catch-up's base: 0.25.0 and
0.26.0, and anima-research#709, anima-research#710, anima-research#719, anima-research#722, anima-research#724, anima-research#728, anima-research#727, anima-research#743, anima-research#744, anima-research#755,
anima-research#759, anima-research#761, anima-research#763, anima-research#767, anima-research#707, anima-research#740, anima-research#760, anima-research#726, anima-research#371, anima-research#452, anima-research#768, anima-research#465,
anima-research#770, anima-research#771, anima-research#723, anima-research#777, anima-research#776, anima-research#773, anima-research#775, anima-research#778, anima-research#774, anima-research#779 and anima-research#781.
Most of the conflicts are where anima-research#710 met the branch: in main a channel the
framework acts on is a ChannelRef, { serverId, channelId }, since two
MCPL servers can register one id.

Conflicts in 14 files (61 hunks), resolved by keeping both sides' intent;
where the two had different designs for one thing, the branch's stays,
moved onto main's types:

- src/framework.ts: main's conversationAgentHomes map stays gone. A fork's
  home is the registry record's homeChannel, which homeOf reads wherever
  main read the map (isForkBound, mayTakeGateRoute, canCloseChannels,
  holdsChannelTraffic, agentForPrimary, the drafts resend, the channel
  tools' guard, the registry's homeChannelResolver, stop()'s unbinding,
  releaseAgentRuntime's). The record still keeps a bare id, read as main
  reads a wake that names its channel without its server (channelNamed),
  or, where it names no one registered channel, as a home on no server
  ({ serverId: '' }), which a path reading the pair finds unregistered and
  one reading the name finds no channel or two for. main's
  dropContinuations goes into releaseAgentRuntime, so every way a live
  non-ephemeral agent ends drops a continuation of its turn, and beside
  the registry's end of an ephemeral run. Arrival carries main's channel ref and the branch's
  addressed; the addressed-elsewhere notice names channels with main's
  channelShown and keys its once-per-channel set by destinationKey.
  isAddressedMessage stays tag-only, with main's ambient marks.
  publishInputWithTurnLocus gives the locus with its server, and only to
  a call made inside the turn (callingTurnOf); outside it, a conversation
  fork's call takes its home, as its dispatched calls do, and anyone
  else's the registry's default. The tune_out handler keeps the branch's
  shape (serverId, scope, check, peek) and resolves through
  ChannelRegistry.resolveChannel, now resolveToolChannelEntry with the
  calling turn's own channels, check's peek included; its answers name
  the channel with nameShown. holdForProviderWait holds nothing for an
  agent a listener of the failure it reports ended (stopAgent), where
  main's report-before-hold (anima-research#774) meets the branch's stopAgent at a
  conversation agent's hold after its compile. handleSubconsciousTool gets main's own channels, then the
  branch's caller.
- src/tune-out/coordinator.ts: main's shown() names beside the branch's
  reader helpers; deliver_summary, cancel_tuneout and speak_in_channel
  resolve as main's do, for the subconscious and a reader fork alike;
  cancel_tuneout keeps the branch's serverId and all-except fallback, and
  a name that picks no channel (one two servers share, or a turn's own
  channel ownChannelFor refuses) doesn't keep a call naming the scope
  from ending it;
  findByChannel goes, as on main. focusChannel() returns a ref.
- src/mcpl/channel-registry.ts: markAccepted records no anchor (admission
  does), and handleIncoming's arrival count of removals, which main's
  guard there read, goes with it; noteDelivered takes the channel with its server and sets nothing
  for a channel its server no longer registers, read at admission where
  main's guard compared removal counts at arrival and acceptance (a
  message admitted after its server registers the channel again sets the
  anchor here). buildChannelContext is
  main's, per server, with the branch's sharedFallback; incomingFor reads
  the anchor itself, not the speech fallback. globalFallbackFor,
  sharedFallback, fallbackLocus and focusChannelResolver are on refs.
- src/index.ts: main's DefaultErrorPolicy beside the branch's exports.
- Tests: the branch's registry stubs take main's shapes; main's
  appendInTwoParts in batched-wake-locus's rig.

Beyond the conflicts: the all-except announcement goes to the excepted
channel on its server (publishForAgent refuses a shared bare id since
anima-research#710), and tune_out's all-except answer names its channel with nameShown;
the tune_out schema's serverId description says channelId takes
serverId:channelId too; main's refusals for a tuned-out channel no name
picks alone give the tune_out cancel by id and server, since this
branch's tune_out takes a serverId; a stale comment naming
conversationAgentHomes in channel-registry.ts names the registry;
docs/dendrite.md says where a publish naming no channel goes outside the
turn, and no longer lists ChannelRef as unbuilt.

Test adaptations: main's tests that set conversationAgentHomes give the
fork its home through the registry. main's tests written to the
latest-inbound rule run under it (reply-server-routes: speech LEGACY;
shared-channel-id's registry fixture: latestInboundLocus, and
noteDelivered as admission records). Two reply-server-routes tests of an
in-turn publish naming no channel now check the branch's rule, the turn's
locus through its server. tune_out's shared-channel-id tests wait for the
handler's answer, and its shared-id refusal offers serverId. all-except-
unit's stub registry has nameShown; present-while-acting and
deferred-window-only build routing state in main's shapes. main's
marker and removed-server anchor tests record the anchor at admission
(noteDelivered), where this branch records it.

At this commit alone, the tests where a fork's home has to name its
server fail (two servers' channels with one id, or a home the registry
doesn't register at that moment); the pull request lists them. The next
commit stores the home with its server.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A message heard mid-turn also wakes its own turn afterwards: intended?

1 participant