Repository navigation
docs(watermark): a tool boundary moves the consumed watermark only when its stream declares it carries injections - #604
Merged
nissa-seru merged 3 commits intoOct 20, 2026
Conversation
…en its stream declares it carries injections The consumed watermark's doc said it moves at "the last tool boundary that injected messages into a live one", and markContextConsumed's doc listed "a tool boundary that injected messages into the live stream", each with no condition. Since c55eee9 the boundary marks only where the stream declares supportsInjectedMessages true, which membrane's native path does from membrane 0.6.0. Membrane 0.5.x declares nothing, so with it no tool boundary moves the watermark, as that commit chose. The doc also said everything above the watermark "has never been in a request". A live stream stores each round's own content and results at its tool boundaries and carries them into its next request, and on a stream that doesn't declare the flag, the messages injected at a boundary may have been carried too, so both can sit above it. The doc now names the three moves (the agent's creation, a compile, and a tool boundary that injected into a stream declaring it carries them) and those two exceptions. The README's coalescing section defined "unread" as above a watermark "advanced at every compile of a model request"; it adds the declared tool boundary. Three comments in framework.ts that name membrane's native and XML prefill paths as declaring the flag now say "from membrane 0.6.0". Comments and README only: no code changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…eam doesn't declare The first commit rewrote the consumed watermark's doc. The texts that depend on what "unread" means said the same unconditional things, so they change with it. Comments, test comments and README only: no code changes. - src/agent.ts: the consumed watermark's doc says a compile counts from when it takes its value (so one that began before a boundary's mark doesn't roll it back), that a dry run such as previewActivation's doesn't move it, and that each move takes the store's head, so it also covers messages the agent wasn't given. It no longer claims to list every way a message above it can have been in a request: a compile that retries after taking its value, and an XML resumption that sends a turn's output as prefill, are two more. watermarkMarks' doc no longer states the write-back rule, which applyCompileWatermark's gives; that doc names a committing compile, not a preview (a dry run), as the case its rule guards. The shown watermark's "nothing above it can have been shown" allows for the round content and results a boundary that injects nothing stores. - README.md: a later occurrence appends once the earlier one "has been read", not once "a model has seen it". The definition of unread adds the watermark set at the agent's creation, says a preview doesn't move it, and says a message injected into a stream that doesn't say whether it carries injections stays unread though the model may have seen it. - src/mcpl/push-coalescer.ts: CoalescerHost.isUnread is true when, as far as the host can tell, no request has included the placement, and says where the host can't tell. refreshOccupant's doc says the same. - src/framework.ts: the comments at the boundary say which membrane path carries injected messages and which drops them, by version. The presentsInjections default's comment says an undeclared round counts as presenting them, which 0.5.x's XML path doesn't. releaseWindow's and unreadFailedReplies' docs say a notice injected into a stream that doesn't declare is passed on (it may have been dropped), that the watermark is set at creation, and that "every one" applies to an agent with no watermark rather than one that never compiled. The deferral queue's doc names the declaring stream. - test/deferred-window-only.test.ts: the header and two comments give the membrane version that declares the flag. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…on; leave the failed-reply docs to anima-research#598 - src/agent.ts: the shown watermark's doc no longer says nothing above it can have been shown "apart from" one case. As with the consumed watermark, a compile that retries after taking its value and an XML resumption reach it too, so it names the round's own content and results as one way, not the only one. The consumed watermark's doc says "a committing compile that returns", since one that throws doesn't move it. - src/framework.ts: releaseWindow's and unreadFailedReplies' docs are back to main's text. anima-research#598 rewrites both, and its text already says what the previous commit changed them to say. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Closed
1 task done
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The consumed watermark's docs say a tool boundary moves it whenever the boundary injects messages. The code moves it there only when the stream declares it carries them, which membrane does from 0.6.0. This brings the watermark's docs, and the texts that rely on what "unread" means, in line with the code. Comments, test comments and README only.
Problem
The watermark's doc (
src/agent.ts) said it is the newest stored message "at the last compile of a model request, or at the last tool boundary that injected messages into a live one", and that "Everything above it has never been in a request".markContextConsumed's doc listed "a tool boundary that injected messages into the live stream". Neither names a condition.The code: since c55eee9, the boundary moves the watermark to the store's head, over what it injected, only when the stream declared
supportsInjectedMessages: true(the=== truecheck afterprovideToolResultsinsrc/framework.ts). With no declaration it injects and marks nothing, as that commit chose: membrane 0.5.87 declares nothing on either path, and its XML path drops what's injected.Membrane declares the flag from 0.6.0: true on its native yielding path and false on its XML prefill path. The published 0.6.0 package sets it at
dist/membrane.js:3794and:3340, and membrane'stests/unit/round-report.test.tsasserts both. No file in the published 0.5.87 package contains the name.So with membrane 0.5.x, which this package's
^0.5.87range installs, no tool boundary moves the watermark. A message injected there on the native path sits above it though the stream carried it, so "has never been in a request" is false for that message. It's also false for each round's own content and tool results, which a boundary stores and the stream carries into its next request, whatever the stream declares.The texts that rely on "unread" said the same unconditional things:
CoalescerHost.isUnread: "no model request has included it";test/deferred-window-only.test.ts's header: membrane's native and XML paths declare the flag, with no version.The failed-reply release's docs (
releaseWindowandunreadFailedReplies) say the same. fix(channels): a channel notice reaches whoever can close the channel, and a released agent's unread ones reach the primary #598, which is open, rewrites both, so this change leaves them as they are.Changes
src/agent.ts:markContextConsumed's doc gains the same condition.watermarkMarks' doc leaves the write-back rule toapplyCompileWatermark's doc. That doc's example is now a committing compile rather than a preview, which is a dry run.README.md: a later occurrence appends once the earlier one has been read. The definition of "unread" adds creation and the declared tool boundary, says a preview doesn't move the watermark, and says a message injected into a stream that doesn't declare stays unread though the model may have seen it. Two paragraphs are rewrapped.src/mcpl/push-coalescer.ts:CoalescerHost.isUnreadandrefreshOccupantsay "as far as the host can tell", and where it can't.src/framework.ts:presentsInjectionsdefault's comment says an undeclared round counts as presenting them, which 0.5.x's XML path doesn't.test/deferred-window-only.test.ts: the header and two comments give the version that declares the flag.No code changes, and no changelog fragment, since the change is docs only (CONTRIBUTING, "What needs an entry"). It touches
src/, so thechangelogcheck needs theno-changeloglabel.What each part is for
agent.tsare where a reader ofAgentlearns when the watermark moves and what sits above it. Both were wrong for any agent on membrane 0.5.x whose turns ran tool rounds.CoalescerHost.isUnread's doc is the contract the coalescer is written against.Tests
33e05c05, Linux, Node 24.22.0):npm install,npx tsc --noEmitandnpm run buildexit 0.npm testran 5,611 tests: 5,569 passed, 42 were skipped and none failed.main's CI atd8cc6909(run 39327669985, ubuntu) ran 5,611: 5,568 passed, 43 were skipped and none failed.main's and the head'ssrc/agent.ts,src/framework.ts,src/mcpl/push-coalescer.tsandtest/deferred-window-only.test.ts, the four TypeScript files the change touches. As a control, changing the boundary's=== trueto!== falsechanges that output.test/deferred-window-only.test.ts's harness (a probe, not part of this change). The harness runs a turn of two rounds through the mock membrane, with one message queued for the tool boundary. After the turn, the probe read each stored message as the coalescer's unread test reads it:Not verified
src/membrane.ts:4177-4201at v0.5.87). The probe shows they sit above the watermark, not what reached a provider.OverBudgetErrorretry incompileAtLiveBudgetNoting);src/membrane.ts:3870-3876at v0.5.87).🤖 Written by Meg-2646 (Claude Opus 5.5), with our internal review, using the Rookery's tooling. Petra-1012 (Claude Opus 5.5) found the mismatch while reading another change, and Ada-1017 (Claude Opus 5.5) set its scope. Duncan-2410 (Claude Opus 5.5), who wrote the boundary's check, reviewed it.
Description checked and posted by Fiona-2757 (Claude Opus 5.5).