Skip to content

fix(scheduler): drop a stored message's wake once a request has read and answered the message - #819

Merged
nissa-seru merged 7 commits into
anima-research:mainfrom
nissa-seru:cuthbert/r1-1-stored-wake
Oct 26, 2026
Merged

nissa-seru merged 7 commits into
anima-research:mainfrom
nissa-seru:cuthbert/r1-1-stored-wake

Conversation

@nissa-seru

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

Copy link
Copy Markdown
Contributor

A message stored while a turn's continuation waits in a provider hold no longer gets a second turn once the resumed turn has read and answered it. Its wake now names the stored message, and the scheduler drops it on the terms it already applies to the wake of a message heard mid-turn (#264). Anarchid found the second turn in review of #762 (R1-1, #808).

Closes #808.

Problem

With #762, a context-budget restart or a tool-result guard's retry starts without the wakes batched beside it, and those wakes go back to the queue. While such a continuation waits in a provider hold, the agent holds no turn token and has no stream, so a message that arrives is stored at once rather than deferred. The resumed request carries the message, and the turn answers it.

The message's wake carried nothing that named the message. Only a deferred write's wake was marked (readsDeferredWrite), on the premise that a stored write's wake starts the turn that reads it. So the wake then started a turn of its own. In silent-heartbeat-rows' cooldown case, 0.27.0 makes four model calls, and main since #762 makes five. The fifth has nothing new to read.

Changes

  • A stored message's wake is marked. A wake queued for a message stored for its agent as it arrived now carries readsStoredMessage, the stored row's id. This applies at the five places that build a wake for one write:

    • a channel message;
    • a push event;
    • a module event's one message;
    • a background script's wake;
    • the gate's self-wake notice.

    A write deferred for its agent's turn still carries readsDeferredWrite.

  • The scheduler judges it as it judges a deferred write's wake. It drops the wake when either of these holds:

    • a request that carried the message has been answered, and for a wake that carries a channel and isn't marked ambient, the agent has also since published into that channel having read it;
    • the message is no longer on its store's current branch.

    A wake still starts its turn if its message reached the agent only in a request that failed, or if its channel is one the agent hasn't spoken in since. If the store can't be read, the wake stays and the error is logged.

  • The new mark is carried where readsDeferredWrite is:

    • while the host is quiesced, wakes with different marks are parked apart;
    • a provider hold keeps them apart;
    • a merged trigger keeps the mark only when every merged request has it;
    • a turn drops it from its trigger once it passes the gate, so a held retry comes back as an ordinary wake.
  • WritePlacement gains messageId. For a message the gate writes, the addMessage hook fills it when the message was stored rather than held. The gate passes that placement to requestInference as after.

  • fix(turns): handle a stream cancel() that throws; start a continuation without the wakes batched with it #762's pending fragments continuation-starts-alone.changed.md and guard-retry-trigger.fixed.md now say that a wake a resuming continuation sent back to the queue waits there and is judged as any queued wake is, rather than that it starts a turn of its own.

Left unmarked on purpose, by our reading. A tool boundary's consumed mark is the store's head, so it can cover a row that no request carried. A mark on the following wakes could then drop a wake for a message the agent never read:

  • A conversation fork's channel message, which is stored past the fork's deferral, even while its turn runs.
  • Another resident's wake for a message stored in the shared slot. That resident's stream can be running when the message lands. As for a deferred write, only the agent the write was stored for gets the mark.

By our reading, a write through the five places above is stored only while its agent has no turn token or stream, or during that agent's own assembly before its compile. So no boundary of that agent's stream can cover it without carrying it.

Tests

New and changed tests:

  • silent-heartbeat-rows, the restart held in a cooldown:

    • the message's wake starts no turn: four calls, back from fix(turns): handle a stream cancel() that throws; start a continuation without the wakes batched with it #762's five;
    • a failed resumed request keeps the wake;
    • a message removed before the resumed request drops it;
    • a channel message keeps its wake unless the resumed turn's words were delivered in its channel;
    • an ambient channel message's wake is dropped, though the turn said nothing there;
    • after quiesce({ abandon: true }) ends the held turn, the wake waits in the hold while an operator's switchBranch lands. On a branch made before the message the wake starts no turn, and on one made after it the wake's turn reads the message.

    The tests release the hold by hand once the case is set up, as its timer would.

  • heard-mid-turn-wake:

    • each of the five wake kinds gives its wake the stored row's id, with the three kinds without a channel taken from the table the boundary tests use;
    • the unit test of the mark now names a stored row.
  • mcpl-coalescing:

    • a stored-message twin of each mark test: parked apart, merged only when shared (with a pass whose two wakes share one message, also added to the deferred test), held apart in a provider hold, dropped from a held retry, and kept when the store can't be read.

Evidence:

  • Red-first: with this head's tests on main's source (4976c67d5c), 15 of the 19 new or changed tests fail. The 4 that pass hold on both sides: the two "keeps its wake" cases, the case on a branch made after the message, and "a wake to read a render neither carries its mark into a merge with other causes nor absorbs another wake while parked".
  • Removals: 17, each taking out one piece: the mark a placement naming a stored message gives, the mark at each of the five places, the scheduler's judgment and each of its three terms, the exemption for an ambient message, the error path, and the mark's handling at the park, the hold, the merge (whether a merge keeps it, and that it keeps it whenever every merged request shares it) and the retry. Run at fa0dc53176, each makes at least one of the 19 targeted tests fail, and with nothing removed all 19 pass. Keeping a wake whose message is gone fails both the removed-message case and the case on a branch made before the message, and dropping the ambient exemption fails the ambient case.
  • Full check at eb5349b86a: the typecheck is clean; npm test runs 7,682 tests, 7,638 pass, 0 fail and 44 are skipped; CI's second step passes 136 of 136; and run one file at a time, the 319 test files give the same totals.
  • The two commits after eb5349b86a change tests and one changelog fragment, and no source. At fa0dc53176 the three changed test files pass whole: heard-mid-turn-wake 46 of 46, mcpl-coalescing 194 of 194 and silent-heartbeat-rows 38 of 38.

Not verified


Ada-1017 (Claude Opus 5.5) decided the scope and the mark's form, and which wakes stay unmarked. Crispin-2406 (Claude Opus 5.5) read the change and checked the reason those wakes stay unmarked against addMessage. Cuthbert-2453 (Claude Opus 5.5) wrote the PR.

Description checked and posted by Lottie-2661 (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.

Cuthbert-2453 and others added 6 commits October 26, 2026 00:09
… read and answered it

A message stored as it arrived, rather than held for a turn that was
running, had a wake with no mark naming it: readsDeferredFor gave one
only to a deferred write, on the premise that a stored write's wake
starts the turn that reads it. That premise fails when the wake waits
while another request reads the message. Since anima-research#762 a wake batched
beside a continuation that resumes its logical turn (a budget restart, a
tool-result guard's retry) goes back to the queue and waits for that
turn. A message that arrives while such a continuation waits in a
provider hold is stored at once, since the agent then holds no turn
token and has no stream, and the resumed request carries and answers it.
Its wake then started a second turn with nothing new to read.

Every wake that names its write now names a stored one too:
readsWriteFor (renamed from readsDeferredFor, at its five call sites)
gives InferenceRequest.readsStoredMessage, the row's id, when the write
was stored for the wake's agent. The scheduler judges it as it judges a
deferred write's wake: storedMessageDone finds the row on the store's
current branch with getMessage, and writeAnswered, split out of
deferredWriteDone so both use one judgment, drops the wake once the row
is at or below the answered watermark on the current branch and, for a
channel wake not marked ambient, the agent has published there having
read it. A row that isn't on the current branch drops the wake, as a
deferred write that's gone does. A store that throws keeps the wake and
logs it.

The new mark is carried wherever readsDeferredWrite is: the quiesce
park's coalescing key, sameInferenceRequest for a provider hold, the
merged trigger (kept only when every merged request shares it), and
beginAgentTurn's strip, so a held turn's retry comes back as an ordinary
wake. WritePlacement gains messageId, which addMessage already filled at
runtime for the gate's self-wake notice.

The red-first case is silent-heartbeat-rows' cooldown test, turned back
to four calls: the resumed call ends on the message, and its wake starts
no turn. Beside it: a resumed request that fails keeps the wake (five
calls), a message removed before the resumed request drops it, and a
channel message keeps its wake unless the resumed turn's words were
delivered in its channel. heard-mid-turn-wake's unit test of the mark
turns round to name the stored row. mcpl-coalescing gains a stored twin
of each mark test: parked apart, merged only when shared, held apart in
a provider hold, stripped from a held retry, and kept when the store
can't be read. continuation-starts-alone.changed.md's "one more turn"
sentence now says the wake is dropped where the restarted turn read and
answered its message.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… kind's mark, a shared merge, a hold released by hand

From the review of afcd501:
- Each of the five wake kinds the fixed fragment names (a push, a
  module's one message, a background script's wake, a channel message,
  the gate's self-wake notice) is now pinned to give its wake the stored
  row's id, in heard-mid-turn-wake: the turn its wake would start names
  that row. Each fails at anima-research#762's head, where no stored mark exists.
- The merge tests of both marks gain a pass whose two wakes name one
  write: the merged trigger keeps the mark, as the every-shares rule says.
- The held-restart helper in silent-heartbeat-rows holds the restart
  with a 60 s cooldown and releases it by hand once the case is set up,
  where a 1 s hold ended on its own timer and a loaded host could end
  it before the message arrived or the row was removed.
- continuation-starts-alone.changed.md now gives the drop's terms (a
  channel wake not marked ambient waits for the agent to speak there),
  and stored-message-wake.added.md says how messageId reaches a wake:
  the gate's addMessage hook fills the placement it then passes to
  requestInference as after. No host call takes messageId itself.
- The readsStoredMessage doc names the ambient exemption; the scheduler
  comment no longer says a wake sent back from a resuming continuation
  then starts a turn of its own; markContextConsumed's comment names
  storedMessageDone beside deferredWriteDone as a reader of the answered
  watermark.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…gments, the deferred mark's ambient doc, a hold past the test's timeout

From the review of 6b023d5fd8:
- continuation-starts-alone.changed.md promised a turn to any wake that
  doesn't name a message the restarted turn answered, but a wake whose
  message is gone from the queue and the store's current branch is
  dropped too. It now gives the drop's terms whole. The guard-retry
  fragment said the wake starts a turn after the retry; it now says the
  wake goes back to the queue behind it, where the same terms apply.
- readsDeferredWrite's doc gains the ambient exemption its sibling
  readsStoredMessage's doc states, as the scheduler applies to both.
- The held-restart helper's hold is now the provider's longest cooldown,
  10 min, longer than the test's own 120 s timeout, so no timer ends the
  hold while the test runs; the test releases it by hand.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… condition alone, and names the wakes it leaves out

"…has since published into that channel, and once the message is no
longer on its store's current branch" read as if both were needed;
either drops the wake. The fragment now also says what an operator
reading it would otherwise miss: a conversation fork's channel message
and another resident's wake for a message in the shared slot are not
judged this way, and start their turns as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
main at 773a145, with anima-research#762 merged (a697939) and the train's other
merges after the branch's base. One conflict, resolved:
- test/silent-heartbeat-rows.test.ts: main removed transientFailure's
  doc comment along with its retryAfterMs; the branch added
  unretriedFailure just above it. Both are kept: unretriedFailure with
  its own doc, transientFailure as main has it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…it covers by naming the rule it follows

"It covers the same wakes" had no referent. The fragment judges the
wake by the rule for a message heard mid-turn (anima-research#264), and the list is
that rule's list as 0.27.0's CHANGELOG gives it, so the fragment now
names that rule as the one whose kinds of wake it covers. A render's
wake, which a deferred write's mark also covers, isn't among them: a
render stored as it arrives is read by the turn being assembled and
queues no wake.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@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. 2 medium severity findings can be fixed after the merge.

Reviewed commit ffe5186 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 (#808) removes an extra turn that #762 introduced. It does so by giving stored-message wakes the same mark and drop rule the scheduler already applies to deferred-write wakes, so the change fits.

5 reasons with sources
  • It fixes a regression in behavior the project already has, so it nearly always belongs. Since #762, the cooldown case in silent-heartbeat-rows makes five model calls instead of four, and the fifth reads nothing new. The PR brings it back to four. The project reviewer found the problem and tracked it as #808. (changelog.d/continuation-starts-alone.changed.md (pending fragment from #762))
  • It extends the scheduler's existing wake-drop rule rather than adding a second way to do the same thing. readsStoredMessage is handled at every point where readsDeferredWrite already is: the quiesce park key, the provider-hold equality check, the merge and the gate-pass strip. The answered-watermark check is factored into a shared writeAnswered, so both marks use the same rule. (src/framework.ts (processInferenceRequests, deferredWriteDone))
  • The new public fields copy existing ones, so they add little upkeep. InferenceRequest.readsStoredMessage and WritePlacement.messageId mirror readsDeferredWrite and deferredId. The PR adds an .added.md fragment for them, as the changelog rules require. (src/types/agent.ts (InferenceRequest), src/gate/event-gate.ts (WritePlacement))
  • The PR is put together the way CONTRIBUTING.md asks. The body has Problem / Changes / Tests / Not verified sections. It gives red-first and removal evidence with numeric test counts, adds fragments in changelog.d/, and updates #762's pending fragments rather than CHANGELOG.md. (CONTRIBUTING.md (What a PR should contain; Changelog))
  • It stays one change: the wake mark, plus the tests and changelog wording that go with it. (CONTRIBUTING.md (How changes land: keep commits coherent))

Findings

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

3 more findings, less severe
  • Low severity, maintainability, src/framework.ts:26034-26045. The new helpers have no doc comments, and the reason the mark is safe was deleted with readsDeferredFor. readsDeferredFor's JSDoc explained when the mark is set and why. readsWriteFor replaces it with no comment, so the one invariant that makes readsStoredMessage safe is no longer stated at the code: the message is stored only while its agent has no turn token or stream, or during that agent's own assembly. storedMessageDone and writeAnswered also have no docs. The long JSDoc on deferredWriteDone (around 25985) still describes only deferred writes, even though writeAnswered now carries its logic for both kinds. A later change to addMessage's deferral conditions could break this invariant without any hint in the code.
  • Low severity, maintainability, test/heard-mid-turn-wake.test.ts:711-717. The new test table borrows writers from another table by position. storedAsItArrives takes its writers from unchanneled[0]![1], unchanneled[1]![1] and unchanneled[2]![1], and labels them by hand ('a push with no channel', and so on). If the unchanneled table is reordered or extended, these cases quietly test a different writer than their names say. The match would still pass as long as the shown text happens to be found. Naming the writers, or deriving these cases from that table, would avoid the hidden coupling.
  • Low severity, test coverage, src/framework.ts:15643-15647. The ambient path for a stored-message channel wake is untested. For a wake carrying a channel, the scheduler drops it without requiring a publish when r.ambient === true (to = undefined). The new silent-heartbeat-rows tests cover only addressed channel messages, with deliver true and false. No test checks that a chat:ambient channel message stored during the hold has its wake dropped once the resumed request is answered, even though the changelog and the type docs describe that behavior.

For a deeper review by several readers and a judge, which takes longer, a maintainer can comment /review deep.

Comment thread src/framework.ts
Comment on lines +26022 to +26032
private storedMessageDone(agent: Agent, messageId: string, to: PublishDestination | undefined): boolean {
try {
const cm = agent.getContextManager();
const row = cm.getMessage(messageId);
if (!row) return true;
return this.writeAnswered(agent, cm, row, to);
} catch (error) {
console.error(`[wake] ${agent.name}: could not read whether stored message ${messageId} was read, so its wake stays:`, error);
return false;
}
}

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, test coverage. No test covers dropping a stored-message wake because its message left the current branch.

The changelog (stored-message-wake.fixed.md) and the InferenceRequest.readsStoredMessage doc both say the wake is dropped once the message is no longer on its store's current branch. storedMessageDone relies on cm.getMessage(messageId) returning null for that case. Nothing in this repo shows whether getMessage is scoped to the current branch. tool-result-guard.ts:303-304 checks the branch separately before it calls getMessage, which suggests getMessage is not branch-scoped. The only test of the 'gone' case removes the row (silent-heartbeat-rows 'removed before the resumed request'). If getMessage still finds a row from an abandoned branch, the wake is kept, or it is judged against a sequence from another branch. A test should switch branch while the wake waits.

Comment on lines +11 to +14
spoken in since it read the message, still starts its turn. It covers the
kinds of wake the rule for a message heard mid-turn covers: channel
messages other than a conversation fork's, push events, a module event's
one message, background-script wakes and the gate's self-wake notices.

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, documentation. The changelog claims channel messages are covered, but gate-debounced wakes stay unmarked.

The fragment says the fix covers 'channel messages other than a conversation fork's'. When a gate debounces channel traffic, the wake comes from EventGate's requestInferenceFn(agentName, 'gate:debounce', 'gate', provenance) (event-gate.ts:3489). That call passes no placement, so readsWriteFor gives that wake no readsStoredMessage. On a host with a gate, a debounced channel message held behind a provider hold therefore still gets the extra turn this PR sets out to remove. The PR description lists this under Not verified, but the user-facing changelog overstates what is covered and should name the exception.

…nd a branch switch, and name the gate's debounced wake as unmarked

Answers the automated review of ffe5186:
- An ambient channel message stored while a restart was held has its
  wake dropped once the resumed request is answered, though the turn
  said nothing there. No test covered the ambient term for a stored
  message.
- A branch switch can land while a stored message's wake waits: a
  quiesce with abandon ends the held turn, the wake waits in the
  provider hold, and once every agent is idle switchBranch goes ahead.
  Two tests drive that through the framework's own quiesce,
  switchBranch and resume. On a branch made before the message the
  wake starts no turn; on one made after it, the wake starts its turn
  and the turn reads the message.
- storedAsItArrives now takes its unchanneled writers from the
  unchanneled table, which carries each writer's shown text, instead of
  borrowing them by position.
- The fragment says a wake the event gate starts when a debounce fires
  carries no mark (anima-research#814), since its "channel messages" read as covering
  a gated host's debounced ones.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@nissa-seru
nissa-seru merged commit f2e0cf6 into anima-research:main Oct 26, 2026
12 checks passed
@nissa-seru
nissa-seru deleted the cuthbert/r1-1-stored-wake branch October 26, 2026 16:50
nissa-seru pushed a commit to nissa-seru/agent-framework that referenced this pull request Oct 26, 2026
main brought anima-research#826, anima-research#819, anima-research#824, anima-research#821, anima-research#825 (ignore and include read
gitignore's forms, and refuse any other), anima-research#815, anima-research#820 and anima-research#818 (answers that
write say where the change stands on disk). Where they met this change, in
src/modules/workspace/index.ts:

- the imports from observe.ts: main's list, which drops
  includePatternReadable and adds directoryAt and patternRefusal, with this
  change's DIRECTORY, LEAVES_MOUNT, NOT_FOLLOWED, NOT_REGULAR and
  rootAbsence;
- after StoredThenFailed: this change's ONCE_REOPENED beside anima-research#818's
  DISK_ORDER and furthestFromDisk.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
nissa-seru pushed a commit to rufalupa666-netizen/agent-framework that referenced this pull request Oct 27, 2026
main moved 20 merges past 4976c67. Four of them meet this branch's lines:
anima-research#824 (an explicit send holds plain speech only where it may have posted),
anima-research#819 (a stored message's wake is dropped once a request has read and
answered it), anima-research#830 (a chat:system notice decides no channel) and anima-research#817 (a
cut-off round's calls are refused). Twelve blocks conflicted, eleven in
src/framework.ts and one in src/types/events.ts.

Where both sides only add, each keeps both:
- The start's request from a process event keeps nonChannelOrigin and the
  API's retake mark, with anima-research#819's readsWriteFor (its new name for
  readsDeferredFor).
- The quiesce key keeps a private wake apart, and anima-research#819's stored message.
- The start's routing keeps a private start's addressed and
  nonChannelOrigin, with anima-research#830's systemNotice cleared.
- The XML fallback's filter refuses anima-research#817's cut-off calls and a private
  turn's channel_publish with no channel.
- A script's wake carries readsWriteFor and the turn it continues.
- ToolResult.refused's doc lists anima-research#817's cut-off case and a private turn's
  channel_publish.

anima-research#824 moved deliverHybridProse's loop body into deliverHybridEnvelope, one
envelope per call, and added routeSilencedHybrid for a round with a send.
This branch's three hybrid rules go back into both:
- deliverHybridEnvelope keeps a private turn's speech with no destination
  private before main's hold of a failed envelope's words, sets a target
  through pinTurn, which sets none in a private turn, and logs a private
  turn's empty-bodied target as setting none.
- routeSilencedHybrid passes "not private" as allowLocus where it delivers
  a segment no send held, as every other caller does, and sets a target
  through pinTurn.
- A locus-mode round with a send that holds nothing, and a tool-call turn's
  trailing speech, go to the private branch after main's held branch; at
  turn end a private turn's speech that anima-research#817's private think kept stays
  main's.

No rule gives way to another: in a private turn, a segment beside a send
whose place is unknown is held as a draft, as a send-silenced one was, and
one beside sends whose places are known is kept private.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
nissa-seru pushed a commit to rufalupa666-netizen/agent-framework that referenced this pull request Oct 27, 2026
…'s drop in play

main's anima-research#819 drops a stored message's wake once a request that carried the
message has been answered and, for a wake with a channel, the agent has
since published into that channel. This branch's tests of a room's wake
that waits on a private turn's continuation use a bare wake, which names no
stored message, so they never reached that rule.

The new cases store the room's message during the turn's tool call and
queue its wake naming it. Behind a private turn's budget restart, which
reads the message and publishes into no channel, the wake still starts a
turn of its own, and that turn speaks in no channel and names the room,
once. Behind a channel turn's restart, which answers in the room, the wake
is dropped, as on main.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
nissa-seru pushed a commit to rufalupa666-netizen/agent-framework that referenced this pull request Oct 27, 2026
…ate turn

main's anima-research#824 holds plain speech beside a send only where the send may have
posted, and anima-research#819 drops a stored message's wake once a request that read it
has been answered. The changed fragment said that words a send silenced are
held as drafts, as in any turn; it now says that in locus and hybrid mode a
private turn's unprefixed speech, which has no target, is held only beside a
send whose place the framework can't read, and kept private beside sends
whose places it reads.

The breaking fragment said the [routing] notice names the channels whose
messages the private turn holds. A message whose wake anima-research#819 drops once the
turn has read and answered it, as an ambient or system message's is, starts
no turn, so the notice may not name its channel: a run of an ambient and a
system message stored during a private turn's tool call, whose wakes waited
behind its budget restart, showed both wakes dropped and no notice. On main
such a message gets no reply of its own either.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
nissa-seru pushed a commit to rufalupa666-netizen/agent-framework that referenced this pull request Oct 27, 2026
…nouncement

Two claims the description makes rested on probes only.

E1: a stored room message tagged chat:ambient or chat:system, whose wake
waits behind a private turn's budget restart, starts no turn once the
restart has answered (main's anima-research#819 and anima-research#830 drop such a wake with no publish
in its channel), reaches no channel, and no notice names its room. The
untagged case, whose wake starts a private turn that names the room once, is
the E1 test before these.

B5: in explicit mode a turn that wrote a >> target and then calls sleep
announces nowhere when it is private, from its start or from a tool
boundary, since a private turn's envelope sets no target and a turn that
turns private loses the one it had. A channel turn announces at its target,
the control. Each private case goes through the code that keeps the target
away: letting pinTurn set a target in a private turn fails the first, and
keeping the target when the turn turns private fails the second.

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 stored while a held continuation waits gets a second turn after it is answered (#762)

1 participant