Skip to content

fix(references)!: while a reference is marked never, don't fetch or save its payload, and forget an earlier copy's path - #851

Open
nissa-seru wants to merge 32 commits into
anima-research:mainfrom
nissa-seru:fix/rfc005-never-payload
Open

nissa-seru wants to merge 32 commits into
anima-research:mainfrom
nissa-seru:fix/rfc005-never-payload

Conversation

@nissa-seru

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

Copy link
Copy Markdown
Contributor

Why. agent-framework's reference handling says a payload a server marks disposition: never is withheld from model context "unconditionally", yet the framework fetches such a payload into the agent's workspace, where its tools can read it. RFC-005 §3.1 says neither the payload nor its uri may reach model context.

This makes the framework fetch and save no payload of a reference marked disposition: never, and forget the path of any copy saved before the mark, so no stub, script object or fetch_reference names that copy. agent-framework 0.27.0 and 0.28.0 say in references.d.ts that never "withholds payload AND uri from model context unconditionally". The uri half held, but nothing after parsing told never from ref, so such a payload was fetched into the agent's workspace, where its workspace tools could list and read it (Problem). This is a breaking change, and its fragment, changelog.d/rfc005-never-payload.breaking.md, opens with who has to act.

A fetch, through the framework, of a reference a server marked never

  • base (main): it's fetched like any other, into the workspace's refs/: eagerly when it's inside the autofetch envelope (below the eager ceiling, at the connection's origin), and for a script, whatever its size. Its stub then names the saved path, which the agent's workspace tools can open.
  • head: it's refused. fetch_reference answers with the reason, save_recent_image says the reference can't be fetched, the eager and script fetches pass it over, and a script's object for it has savedTo: null and the reason as unfetchable.

A copy saved before the mark

  • base (main): there's no mark, so a copy's path stays in its stub and script object.
  • head: the first mark clears the record's saved path. The copy stays where it was saved, and the log names its path for the operator, unless the record that named it was evicted before the mark.

A module's claims about a server's reference

  • base (main): a registration that knows no server, such as a module's result's, replaces what a server's reference said, while the record is unfetched and the registration isn't a link's.
  • head: it no longer does. A never in a module's result marks nothing, since a mark refuses every agent's fetches and RFC-005's testimony is a server's, not a module's or a script's, so a module relaying another's JSON, or a script's own output, can't cut them. It keeps that payload out of that result. Where no server has bound a record, a later such registration, other than a link's, still replaces an earlier one's, as on main.

An image or audio block by a uri longer than a reference's link may be, on a push, channel or injection lane

  • base (main): an image with no disposition goes to the provider as a url, and audio with none shows that it carried no data.
  • head: it's rejected whole, whatever its disposition, as a reference with such a uri already is and as RFC-005 §8 says of a block whose uri violates its schema: the agent reads that its link is longer than this host accepts. The limit is RFC-005's 4,096, counted in UTF-16 units, as a reference's always has been, where the schema counts code points.

ReferenceFetcher

  • base (main): no withhold option.
  • head: an optional withhold. The framework builds its fetcher with it. RFC-005's conformance adapter builds its own without it, so vector 03 still fetches and saves a never reference there.

No MCPL server is known to send never today.

Where the mark doesn't hold or reach (What it leaves):

  • where it's lost or comes too late: a copy saved before the mark stays in the workspace, and an evicted record or a restart takes the mark with it;
  • a never for a uri longer than a reference's link may be makes no record, so outside the result or message that carries it, an embedded resource for that uri shows its contents unless it carries its own never;
  • a never from any server refuses later fetches of a uri another server delivered;
  • a reference in a result whose blocks can't be read for a nested never isn't marked by it, so the agent's own fetch_reference can still fetch it;
  • another spelling of a marked uri, which the URL parser reads as the same resource, has a record of its own that the mark doesn't reach;
  • an image sent by uri alone before the mark stays a url in history, so the provider fetches it again for each request that carries its message, until a fold takes it;
  • the agent isn't told that what it already read is now never;
  • a modern or MCP-only server's results, and any result's structured content, show as they did, uris and embedded contents an MCPL server marked never included.

CI passes at this head, its whole suite included. We haven't run this against a live MCPL server that marks a reference never.

One existing test, test/tool-result-content.test.ts:44, pinned the stub that a never reference no server marked showed before this change. e57a6b0159 changes it to expect the notice.

@antra-tess, this decides how agent-framework reads RFC-005's never: it keeps the stricter reading, that the framework fetches and saves none, where RFC-005's §7, §12 and vector 03 fetch and save a never payload (What it leaves).

  • Changelog fragment added: changelog.d/rfc005-never-payload.breaking.md.
Credits

Gordon-2414 wrote the change, on Ada-1017's design and calls. Beth-2709 specified where an embedded resource's never marks the payload. Kestrel-2409 checked what each part adds, which cut withdrawing a copy saved before the mark, and Ansel-2428 read it as an agent that meets its words would. Tabitha-2467, Desmond-2579, Beth-2709, Duncan-2410 and Cordelia-2434 each read parts of it up to 2f15c4293d. Beth-2709 and Duncan-2410 read the round after it, and Duncan-2410's runs found the cases that b7f03e252c and c95cbcd3f0 fix. Our internal review read the change in several rounds. Lottie-2661 wrote this description before the round, Gordon-2414 its update, and Daniel-2612 put it in this shape. Leonard-2330 ran the failing test at c95cbcd3f0, and Beth-2709 read the commit that changes it. Cordelia-2434 read the round answering the second nightly review, and her probes found two of the three cases its last two commits close. Leonard-2330 checked what each part of a721eff035 adds, and that check led to rebuilding how a never marks a uri with no record and to cutting the marks that followed a uri's shown text. He also read the rebuild. Beth-2709 and Cordelia-2434 read it for security. Leonard-2330 read the answer to the automatic review of 30cee987f2 as a colleague who built none of it, and found the embedded resource's price and the unbound record. Beth-2709 found the price apart, and she and Cordelia-2434 read the answer for security. Cordelia-2434's run confirmed the price.

Details for agents

Head a8b0a03420, on 7a97ea3e82.

Problem

At 0.28.0 (59d609586e), as the red-first tests and a probe show (Tests):

  • a small never reference from a networked server was fetched eagerly into <mount>/refs/, and every one a script's result named was fetched on demand;
  • its stub then said saved: <path>, and a script's object carried the path as savedTo;
  • the agent's workspace tools could find and open the copy: ls and glob listed it, and read (for text) and read_image returned its payload in their results.

In this framework the workspace is the agent's own tool surface, and a script's output goes back to the model, so a payload the host stages is one the agent can read.

Changes

  • The mark, ReferenceRecord.payloadNever. The first never in an MCPL server's own content marks its uri's record: a tool result, an error result, or a push, channel or injection. That never can be the reference's own, another reference's in the same result, inline data's, or an embedded resource's. Where the uri has no record, the never makes one, marked and bound to that server, so a later delivery of the uri finds the mark, unless the uri is longer than a reference's link may be. A server's never reference nested in its result, at any depth the result's text shows, marks its uri's record, binding that server where no server had, or, where the uri has none, one made for it, bound to that server. Nothing clears a mark while its record lives, and a later delivery without never, from any server, doesn't lift it, since a server "can veto context inclusion but can never expand it" (RFC-005 §3.1). The log names the server whose content made the mark and the servers the record is bound to. classifyBlock marks nothing.

  • On a push, channel or injection lane, a never anywhere in a message governs every block of it, whatever their order. So an image sent by uri before a never for that uri is its stub. Each block is converted with the message's nevers alone, so each reference keeps its own expiry where two name one uri.

  • A never in any other result, such as a module's, marks nothing, but keeps its payload out of that result. Nothing that handles the result fetches it, and each reference to such a uri shows its name, if it has one, and a notice that its content is marked never, with no id to fetch it by, and no saved copy or uri, unless a server's never already marked that uri's record, whose stub it then shows, with an id a fetch refuses. An id a server's own delivery gave earlier still fetches it. A registration that knows no server, such as a module's result's, no longer replaces what a server's reference said, though where no server has bound the record, a later one, other than a link's, still replaces an earlier one's. A server's first word for a record only a module had named replaces that record's testimony, a link's included, so a module's never or digest doesn't follow the uri once a server speaks for it.

  • A copy saved before the mark is no longer named. The first mark clears the record's saved path and verified facts. The copy stays where it was saved, and the log names its path for the operator, unless the record that named it was evicted before the mark.

  • ReferenceFetcher takes an optional withhold, with a remove(path). A fetcher built with it:

    • refuses a marked record before anything else, and reads the uri's current mark too, so a fetch whose record was evicted and marked again under a new record is refused;
    • answers each waiting caller with the mark, and stops a download at its next chunk once the payload is marked;
    • saves nothing for a mark after the last chunk, and removes a copy saved for a record marked while it was being saved.

    The framework builds its fetcher with withhold. RFC-005's conformance adapter builds its own without it, so vector 03 still fetches and saves a never reference there.

  • The eager pass, a script's demand fetch and a module's demand loop pass a marked record over, and log no refusal for it.

  • A result whose blocks can't be read for a nested never, such as one too deep to convert, gets no eager or script fetch. Where a model's call has a reference to pass over, the log says so.

  • What the agent reads of a marked record:

    • its stub ends with "a server asked that its content never reach you or your tools", in place of a saved path or the fetch hint;
    • fetch_reference answers with that reason, and save_recent_image says the reference can't be fetched for that reason;
    • a script's object has savedTo: null and the reason as unfetchable;
    • a link to it shows no uri, and neither does a script's object for the link;
    • a record a never makes for a uri with none holds that never alone, not a reference's other claims, so a script's object for a later link to it gives the never reason and no expiry;
    • an image sent by uri alone on a push, channel or injection lane is its stub, not a url the provider would fetch.
  • An image or audio block by a uri longer than a reference's link may be is rejected whole on a push, channel or injection lane, whatever its disposition, as the history and script lanes already reject it as a reference: the agent reads that its link is longer than this host accepts.

  • An embedded resource whose uri is never shows a notice saying why, not its contents, whatever they are, on every lane, in history and to a script. So does one beside a never for its uri in the same message or result, whatever their order.

  • The module header says what the mark keeps from the agent, and what it doesn't reach.

  • The changelog fragment is changelog.d/rfc005-never-payload.breaking.md. It opens with who has to act.

14 files, +1477 −138, against main at 7a97ea3e82. Up to 2f15c4293d: 8b3b0341db made the change; 69b84baf49 cut its comments to those that explain what the code can't; 56170af1bd fetches nothing on its own for a result whose blocks can't be read for a nested never; 5e4236f2ed spells a withheld image's fallback text as main does; 2f15c4293d corrects three docs to say a mark outlasts a later plain reference; and d2d4b7b487 and 19f2ed6a96 merge main at e85c57db9a and 4a45766f01. 2c92a7ae94 merges main at 7a97ea3e82, and git show --remerge-diff prints nothing for it. Twenty-four commits follow. e57a6b0159 changes one existing test and nothing else, and five, a58c6a478e to a721eff035, answer the second round of the maintainer's nightly review (Tests). The last, 30cee987f2, makes a never for a uri with no record make that record, marked, and takes out the fetch's hold on its uri and the marks that followed a uri's shown text. dcca8f68d4 to a8b0a03420, five commits, answer the automatic review of 30cee987f2 and the reads of that answer (Tests).

What it leaves

  • RFC-005's own reading. RFC-005's §7 and §12 sketch a host fetching a never payload and handing it to a code-execution script as a path, and its vector 03 fetches and saves one. Here a script's output reaches the model, so this change offers no host-mediated use of a never payload, §12's hand-off included. Which reading RFC-005 keeps is its owner's to rule.
  • Any server's never marks the uri's record, whichever server the record is bound to, so one server's never refuses later fetches of a uri another server delivered. A never in a module's result marks nothing, and doesn't follow the uri into later results once a server has spoken for it.
  • A copy saved before the mark stays in the workspace, where the agent's workspace tools can still find it. The log says where, unless the record that named it was evicted before the mark. So does one whose save finished as the mark came, if deleting it fails, and the log says where that one is. On a mount without autoMaterialize, that delete leaves a disk copy a materialize wrote in the meantime, listed as workspace-deleted, until materialize with applyDeletions removes it or a sync of the path restores it.
  • A reference in a result whose blocks can't be read for a nested never isn't marked by that never, so the agent's own fetch_reference can still fetch it.
  • Eviction and restarts: a record evicted past the registry's 2,000, or a restart, takes the mark with it. A record a never makes for a uri with no record takes one of those 2,000 too, and a never for a uri longer than a reference's link may be makes none, so outside the result or message that carries it, an embedded resource for that uri shows its contents unless it carries its own never. An embedded resource's uri is MCP's field, not RFC-005's, so it isn't rejected as an image or audio block's is. A never on a block rejected whole for its uri's length governs nothing, since RFC-005 §8 rejects such a block whole, its disposition included, so an embedded resource for that uri beside it shows its contents. A fetch reads its own record and the uri's current one at every check, so a never that marks a new record after its own was evicted stops it, unless 2,000 or more registrations before that check evict the new record too.
  • Spellings: a mark is on the uri as sent. Another spelling of it that shows as the same text, such as one with whitespace at either end or a control or bidi character added, has a record of its own, which the mark doesn't reach, so a later fetch of it isn't refused and a later delivery of it shows as an unmarked one does. That needs a server to deliver the marked uri respelled, and no sender is known to. Within the result or message that carries the never, a reference or link so spelled is still withheld by that never, but an image sent by uri alone or an embedded resource so spelled isn't.
  • What the agent already has: the workspace's earlier states, history, and images the agent read before the mark keep what they held, and the agent isn't told that what it read is now never.
  • An image sent by uri alone before the mark stays a url in history, so the provider fetches it again for each request that carries its message, until a fold takes it.
  • The lanes RFC-005 governs: the server-to-host content blocks of tool results, push events, channel messages and injections (§4). A modern or MCP-only server's results, which don't speak RFC-005, and any result's structured content, which is tool-defined JSON, show as they did, uris and embedded contents an MCPL server marked never included.

Tests

  • test/rfc005-never-payload.test.ts is new, with 55 tests:
    • how the mark is made and kept, and where it comes from;
    • what a marked record shows in stubs, script objects, links, embedded resources and a push lane;
    • a fetcher built to withhold, with one built without it fetching and saving, as vector 03 expects;
    • the framework's three fetch paths, a fetch whose record 2,000 registrations would evict before or while it runs, and a result whose blocks can't be read;
    • fetch_reference's answer, and an end-to-end run with a started framework, a workspace and a local server.
  • Changed to match: test/save-recent-image.test.ts, test/script-structured-references.test.ts, test/tool-result-blocks.test.ts, test/tool-result-history.test.ts, test/mcpl-reference-fields.test.ts and test/mcpl-reference-fetcher.test.ts. Among them, in a script's view, a link beside a nested never for the uri its text shows is the not-shown notice, with no id, where it was an object with an id. The model's line, which reads the top-level blocks alone, shows the link's uri, as on main.
  • Changed to match, in e57a6b0159: test/tool-result-content.test.ts:44, which passes a resource marked never through toMembraneToolResult with no server's mark on its record. It expected a [ref_ stub, and now expects the notice, with no reference id and no RAW_TOKEN in the content. Its twin in test/tool-result-history.test.ts registers its never with a server first, as arrival does, and keeps its stub.

At a8b0a03420:

  • CI (run 42497537517, on its merge into main at aa134e75fc): its 11 jobs pass, and so does the changelog check (run 42497537501). Build & Test's whole suite passes 10,347 of 10,410 tests, with 63 skipped and none failing, and its unforced step passes 131 of 131.

At bd44bc35e4, whose code and tests a8b0a03420 keeps, changing only the fragment, answering the automatic review of 30cee987f2 and the reads of that answer, on Linux with Node 24.22.2, each from a clean checkout after a fresh npm install, tsc --noEmit and build:

  • Red first, at dcca8f68d4, the tests alone on 30cee987f2's code: exactly two tests fail: the new one for a never for a uri past a reference's limit making no record, and the lift test's new last assertion, a later plain word lifting a module-only uri's never for a link. At de531739d5, the next tests alone on 1e61897183's code, exactly two fail: an image by an over-long uri on a push lane, and a record no server had bound left unbound by a server's nested never.
  • Green: the new file passes 55 of 55, and with twelve other focused files and test/mcpl-coalesced-content-parity.test.ts, 511 of 511.
  • Mutants:
    • with dcca8f68d4's tests, each of three fails its new assertion, where with 30cee987f2's the test it fails passes: the lift test with the result's claims ignored for a link, the three crowded fetches with no eviction past 2,000 registrations, and the save-race test with a copy stored in the tree alone not deleted;
    • at 1e61897183, with no limit on a never's uri, the uri-limit test fails, and with a serverless registration replacing nothing, the lift test's last assertion fails;
    • at bd44bc35e4, with no length check on a push lane's image, the over-long uri test fails, and with no binding by markNever, the nested-never test and the mark's log test fail.
  • RFC-005's conformance, mcpl main 87efd5f's check on Bun 1.4.4: 45 of 45 schema cases and 45 of 45 host cases, the same, case by case, as at 30cee987f2.

At 30cee987f2:

  • CI (run 42478535704, on its merge into main at aa134e75fc): its 11 jobs pass, and so does the changelog check (run 42478535722). Build & Test's whole suite passes 10,345 of 10,408 tests, with 63 skipped and none failing, and its unforced step passes 131 of 131.

At 30cee987f2, on Linux with Node 24.22.2, each after a fresh npm install, tsc --noEmit and build:

  • Red first: with this commit's test/rfc005-never-payload.test.ts on a721eff035's code, exactly the two tests it turns round fail: a lane's inline or embedded never for a uri with no record, and markNevers for an absent uri.
  • Green, from a clean checkout: the new file passes 53 of 53, and with twelve other focused files and test/mcpl-coalesced-content-parity.test.ts, 509 of 509.
  • Two mutants: with markNever making no record, or making it with no server, the new file fails those two tests and the four inline and embedded cases of a fetch marked while it runs.
  • RFC-005's conformance, mcpl main 87efd5f's check on Bun 1.4.4: 45 of 45 schema cases and 45 of 45 host cases, the same, case by case, as at a721eff035.

At a721eff035:

  • CI (run 42447596921, on its merge into main at 3c31baa72f): its 11 jobs pass, and so does the changelog check (run 42447597021). Build & Test's whole suite passes 10,359 of 10,422 tests, with 63 skipped and none failing, and its unforced step passes 131 of 131.
  • On Linux with Node 24.22.2, each after a fresh npm install, tsc --noEmit and build: test/rfc005-never-payload.test.ts passes 67 of 67, and with test/tool-result-content.test.ts and the twelve focused files below, 494 of 494. On the code before each of the round's fixes, its new tests for inline data, embedded resources, the shown text with registrations during the fetch, and the log failed: the fetch saved the payload, the record it read was evicted, or the loops logged a refusal as a failure.

At e57a6b0159:

  • CI (run 42432971695, on its merge into main at 3c31baa72f): its 11 jobs pass, and so does the changelog check (run 42432971676). Build & Test's whole suite passes 10,346 of 10,409 tests, with 63 skipped and none failing, and its unforced step passes 131 of 131.
  • On Linux with Node 24.22.2, each after a fresh npm install, tsc --noEmit and build: at c95cbcd3f0, test/tool-result-content.test.ts fails only its :44 test, with CI's error; at e57a6b0159, that file and the twelve focused files below pass 481 of 481; and on main's code at 7a97ea3e82, the changed test fails, getting a [ref_ stub.

On Linux with Node 24.22.2, each from a clean checkout after a fresh npm install:

  • At 03da219555, whose code and tests are c95cbcd3f0's and ccd9ac3429's, with only the fragment's text and the commit message changed between them: tsc --noEmit exits 0, and test/release-changelog.test.ts with the new file passes 78 of 78.
  • At ccd9ac3429:
    • tsc --noEmit exits 0;
    • the new file passes 54 of 54;
    • twelve focused files pass 476 of 476: the new file, the six changed to match before e57a6b0159, test/mcpl-reference-binding.test.ts, test/mcpl-push-convert.test.ts, test/mcpl-agent-words.test.ts, test/tool-result-reference-uri.test.ts and test/release-changelog.test.ts.
  • Each piece of c95cbcd3f0 removed, one at a time at ccd9ac3429, whose code is c95cbcd3f0's, with the new file run alone, fails only that file's tests of that piece: the born-marked record, the mark read by the shown text, each of the two records an inline never marks, the re-delivered record, the same result's claims read by the shown text, the fetcher's read of the shown text, and the embedded resource's. A check of the mark on a download's first part alone fails the partway test, and passes it as the test stood at 8aca7146e3.

Earlier runs, on Linux with Node 24.22.1 unless named:

  • At 19f2ed6a96, which merges main at 4a45766f01: ten focused files pass 407 of 407, and each of them passes on Node 22.12.0, CI's floor.
  • Red first, against main: with 4a45766f01's framework.ts, references.ts, reference-fetcher.ts and convert-block.ts in place of this change's, the new file as it stood then failed 37 of its 38 tests.
  • A probe at 0.28.0, with a started framework, a workspace and a local server, asked the framework's own fetcher for a text and a PNG reference marked never. It saved both, each stub ended saved: <path>, ls and glob listed both files, read returned the text's payload and read_image the PNG's. At this change's second commit both fetches were refused with the reason, and ls and glob found nothing.
  • RFC-005's conformance, mcpl main 8b0e57d's check on Bun 1.4.4, at 56170af1bd's tree and at 0.28.0: 45 of 45 schema cases and 45 of 45 host cases at both.

At c95cbcd3f0, one existing test failed: test/tool-result-content.test.ts:44, "beside an image, a text block whose text is not a string shows the placeholder, and a reference is still a stub", which no commit up to it edited. It expected its text to start [ref_, and it got [Reference not shown "f.txt": its content is marked never to reach you or your tools.]. CI's run 41749614185 failed it there, and Leonard-2330 built that head on Linux with Node 24.22.2 (npm install, npm run build, then node --test dist/test/tool-result-content.test.js) and saw the same notice. e57a6b0159 changes it (Changed to match, above).

Not verified

  • No live server: this hasn't run against a live MCPL server that marks a reference never, nor in a running resident. Only the tests' local servers and the conformance fixtures exercise it.
  • The conformance corpus runs through its own adapter's fetcher, so it doesn't show the framework's withholding. The tests above do.

Gordon-2414 and others added 2 commits October 27, 2026 15:06
…save its payload, and forget an earlier copy's path

RFC-005 §3.1 says that under `disposition: never`, neither the payload nor
the uri may reach model context. A rule for every reference already kept
the uri out, but nothing after parsing told `never` from `ref`. So a `never`
reference was fetched like any other, eagerly when small, into
`<mount>/refs/`. Its stub then named the saved path, a script's object
carried the path as `savedTo`, and the agent's workspace tools (read,
read_image, grep, ls, glob) could open the copy.

The mark:
- ReferenceRecord gains `payloadNever`. The first `never` read for a uri
  marks the uri's record, and nothing clears the mark while the record
  lives. A later delivery without `never`, from any server, doesn't lift
  it. How the uri is shown still follows the testimony, where a later
  delivery supersedes.
- register marks the record for the reference's own `never`, and for a
  `never` its result's claims hold. Those claims come in register's new
  `claims` argument, which a tool result's stub, a script's object, the
  eager pass and a module's demand loop pass, so inline data's `never`
  before the reference counts.
- classifyBlock marks, through markNever, the record a uri already has when
  it reads inline data's `never`, or an embedded resource's for the
  resource's uri, wherever a block is read. A later walk of an older result
  marks a record made since, which only tightens.
- ReferenceRegistry.markNevers marks the records that already exist for
  the uris a result's `never` claims name, before a fetch pass awaits
  anything. It does the same in an
  MCPL peer's failure that a module gets in process, whose references
  aren't registered. The eager pass reads a result's claims as a script's
  view reads them, nested fields included, in every mode.
- Where a fetch had saved a copy, the first mark clears the record's saved
  path and verified facts, so nothing names or serves the copy from then
  on. The copy stays where it was saved, and the log names its path.

Fetching:
- ReferenceFetcher takes an optional `withhold` with a `remove(path)`. A
  fetcher built with it refuses a marked record before anything else, in
  each pass of fetch(), in refusal() and isEagerEligible(), and for a saved
  copy or a fetch under way. It answers each waiting caller with the mark
  as the wait ends. Once the record is marked, it stops a download at the
  next chunk, saves nothing for a mark that lands after the last chunk, and
  removes a copy saved for a record marked during the save.
- The framework builds its fetcher with `withhold`. Its remove is a
  workspace delete, and the log records a failed delete with its path.
  RFC-005's conformance adapter builds its fetcher without `withhold`, so
  vector 03 still fetches and saves a `never` reference.
- The eager fetch, the script-demand fetch and a module's demand loop pass
  a marked record over. They log no refusal for a record marked while they
  run.

What the agent sees:
- A marked record's stub ends with PAYLOAD_NEVER's reason ("a server asked
  that its content never reach you or your tools") in place of a saved
  path or the fetch hint.
- Its script object has `savedTo` null, and that reason as `unfetchable`.
- A link to it shows no uri, whatever later testimony says. That covers
  shownLinkUri, and a script's object even where the line's record
  predates the mark, since any tool that can fetch a shown uri would bring
  the payload in.
- On a push, channel or injection lane, an image given only by a marked
  uri becomes its stub, not a url source.

Tests that pinned the old behaviour now pin the new. They are anima-research#527's lift
test (a later plain delivery no longer brings back a marked uri's link),
and the tests that read a `never` reference's reason or counted it among
what's fetched for a script. Tests where `never` was incidental now use a
plain or `ref` reference. The fragment says where a marked payload's link
shows no uri, which 0.27.0's notes for anima-research#527 don't.

What it leaves:
- A `never` from any server, or in a module's result, marks the uri's
  record. So one server's `never` refuses later fetches of a uri another
  server delivered.
- A copy saved before the mark stays in the workspace, where the agent's
  workspace tools can still find it. Stubs, script objects and fetches
  made after the mark don't name its path.
- A later result's link whose uri differs from a marked one only in what
  cleaning removes gets an unmarked record.
- An eviction past 2,000 records, or a restart, drops the mark.
- An inline `never` read before any reference for its uri marks nothing
  until it's read again. That holds within one message on the convert
  lanes too, where a uri-only image before a `never` for its uri also
  reaches the provider.
- The workspace's earlier states, and the history and images the agent
  read before the mark, keep what they held. The agent isn't told that
  what it read is now `never`.
- A failed delete of a copy marked during its own save isn't retried.
- No host-mediated use of a `never` payload is offered, a script's
  included.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…de can't

The previous commit's comments, cut to those of five kinds: a license
header, non-obvious behaviour an outside dependency, platform or protocol
forces, a directive, a doc comment that defines a public API contract, and
an issue or RFC link that explains a constraint the code can't express. The
rest of what they said is below. Comments only: with comments removed,
TypeScript's printer prints each changed file the same before and after.

None of the declarations the fix adds or changes is exported from
src/index.ts, so their docs define no public contract. What stays:
- ReferenceRecord.payloadNever keeps one line, RFC-005 §3.1's: a server can
  veto context inclusion but never expand it, so nothing clears the mark
  while the record lives.
- The references module's header says again, as main does, that `never`
  withholds payload and uri from model context unconditionally, which the
  fix makes true.
- Two of main's docs are edited so they stay true: unfetchableReason gives
  a `never` mark's reason before the registry's dialCheck, and a script
  object's `savedTo` is null where a `never` marked the payload.
- Two of main's test comments the fix made false are cut: that a later
  plain delivery's link shows the uri "as if no never had been given", and
  that a link with no `never` of its own shows a uri an earlier result
  marked.

What the cut comments said:
- classifyBlock marks as well as reads: inline data's `never`, and an
  embedded resource's (inline data, RFC-005 §8.1), marks the record its
  uri already has, wherever a block is classified (a result's claims walk,
  a fetch pass, a push lane's conversion). A later walk of an older result
  marks a record made since, which only tightens, since `never` sticks per
  uri.
- payloadNever is set by the first `never` read for the uri: a reference's
  own testimony or its result's claims (register), or inline data's
  (markNever). How a link's uri shows otherwise reads the testimony, where
  a later delivery supersedes, as anima-research#527 made it. An eviction or a restart
  drops the record and the mark.
- markNevers runs as a result arrives, before anything is awaited for it,
  so a `never` reference's record isn't fetched meanwhile,
  and on an MCPL peer's failure a module gets in process, whose references
  aren't registered and whose blocks reach no lane that classifies them.
- markPayload clears a saved copy's path and verified facts, so nothing
  names or serves it. The copy stays, its path offered under the earlier
  word, like the history and images the agent already has, and the log
  names it for the operator. register marks before it
  refreshes the testimony, so a record the mark clears is left unfetched.
- A shown uri is a door around the payload rule, since any tool that can
  fetch it brings the payload in, so a marked record's link shows none
  (shownLinkUri, and a script's object as the record stands now, whatever
  the line's record was).
- A marked record's stub has no saved copy and no fetch hint, which §5's
  "at most" leaves out, and it names the reason, so the agent doesn't ask
  for the payload. PAYLOAD_NEVER is in the agent's words: `never` is the
  server's request, RFC-005's disposition, which the log can name.
- convertBlock doesn't hand a marked uri to the provider, which would fetch
  it into context.
- ReferenceFetcher's `withhold`: given, a marked payload is refused before
  anything else, at each try and as each caller's wait ends, since a mark
  can land while it waits; a download under way stops at its next chunk; a
  mark after the last chunk saves nothing; and a copy saved for a record
  marked while it was saved is removed, since no stub has offered its path.
  A mark after that clears the record's path, and the copy stays. The
  framework builds its fetcher with `withhold`, and RFC-005's conformance
  adapter without it, so vector 03 still fetches and saves a `never`
  reference.
- The framework's `remove` is a workspace delete, which undoes the fetch's
  own write. Its failure is the log's, since no agent asked for it, so the
  log says where the copy is.
- neverClaims reads a result's `never` claims as a script's view reads
  them, nested fields included, whichever lane reads the result, so a
  `never` nested in a block withholds the same payload whether or not a
  script reads it, and falls back to the top-level blocks'
  where that walk stops.
- The eager pass, the script-demand pass and a module's demand loop pass a
  marked record over with no refusal to log, as one a `never` marks while
  the pass runs.

The comments the fix added to tests are cut too, the new test file's
included.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@nissa-seru
nissa-seru force-pushed the fix/rfc005-never-payload branch from 4872e39 to 69b84ba Compare October 27, 2026 19:07

@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: needs a maintainer's decision. The change belongs, but part of it needs a maintainer's call.

Reviewed commit 4872e39 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, with concerns. This is a bug fix that makes the code do what the references module already promises: never keeps the payload out of model context, and the fix lives in the existing RFC-005 components. A maintainer still has to sign off on how it reads RFC-005, mainly the dropped §12 script hand-off and one server's never blocking a uri for every server, and on classifyBlock now changing registry state.

Findings

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

4 more findings, less severe
  • Low severity, documentation, changelog.d/rfc005-never-payload.fixed.md:5. Changelog headline says a never payload no longer reaches the agent's tools, which its own last bullet contradicts. The bold lead says such a reference "no longer reaches the agent or its tools". The last bullet then says a copy saved before the mark "stays in the workspace, where the agent's workspace tools can still find it", and that eviction or a restart drops the mark. Operators who read only the headline will think a never payload is never available to tools. The lead should be qualified to match what the change actually guarantees.
  • Low severity, maintainability, src/framework.ts:7120-7136. The remove hook deletes by calling the agent-facing delete tool by name instead of a workspace API. saveFile writes through the typed workspace.writeBinary API, but the new remove hook calls workspace.handleToolCall({ name: 'delete', input: { path } }) with a synthetic call id and no caller agent. This couples the framework to the tool's name, its input shape and its success and error wording, and the path goes through tool-level parsing and refusals meant for agent calls. A rename or schema change to the workspace delete tool would break this cleanup silently; the only signal would be a log line. A direct deletion method on the workspace module, like writeBinary, would match the save path.
  • Low severity, test coverage, test/rfc005-never-payload.test.ts:494-503. No end-to-end test of the fetch_reference tool's answer for a marked record. The tests check getReferenceFetcher().refusal(marked) and the fetcher's fetch() result. Nothing drives dispatchFetchReferenceToolCall (framework.ts:33212) and checks the error text the agent actually gets for a marked record, fetch_reference couldn't fetch "<id>": a server asked that its content never reach you or your tools. Calling again won't change that.. That text is one of the user-facing behaviors the PR description lists, and it should have a test like save-recent-image's.
  • Low severity, test coverage, test/script-structured-references.test.ts:556-569. The connect-race test now uses a plain reference, so it no longer covers a never result in that race. The test 'a module tool's result is treated when a server with a prefix that fits its name connects while its call is in flight' used to feed a never reference. It now strips disposition so that its fetched and UNBOUND_REASON assertions still pass. No other test checks that a never reference in a result whose server prefix arrives mid-call is passed over by the demand fetch and shown with PAYLOAD_NEVER. A case that keeps disposition: 'never' should be kept or added, with assertions updated to the new behavior.

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

Comment on lines 409 to +411
const flight = this.inFlight.get(record.refId) ?? this.start(record, ceiling, deadline, timeoutMs);
const ended = await settledBy(flight.ended, deadline);
if (this.withholds(record)) return fail(PAYLOAD_NEVER.error, PAYLOAD_NEVER.remedy);

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 mark during a stalled download doesn't stop it or answer waiting callers until the next chunk or the deadline.

The withhold check in doFetch (line 467) runs only after reader.read() returns a chunk. Callers already inside fetch() are parked on settledBy(flight.ended, deadline) and re-check withholds only after that settles. If the server stops sending after the record is marked, the connection stays open and every joined caller, such as a fetch_reference call or the script demand loop, waits until the flight's timer or its own timeout (FETCH_TIMEOUT_MS) before it gets the mark. The PR says the fetcher "answers each waiting caller with the mark" and "stops a download" once marked. Nothing aborts the controller or wakes waiters when markPayload runs. The test uses timeoutMs: 50 for the joined caller, which hides the delay.

Comment thread src/mcpl/references.ts Outdated
Comment on lines +741 to +744
markNever(uri: string): void {
const record = this.findByUri(uri);
if (record && !record.payloadNever) this.markPayload(record);
}

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. markNever mutates a record without first copying it into open journals, breaking the journal's 'as it stood' contract.

register() calls keepFirst(uri) before it changes a record, so an open journal (openJournal) keeps the record as it stood when the script lane began. markNever and markNevers are reached from classifyBlock and from the demand and dispatch paths. They call markPayload directly, which sets payloadNever and deletes fetchedPath, verifiedBytes and so on, without keepFirst. A journal whose uri no registration has named since then returns the mutated live record from before(uri). So line.recordBefore, which is meant to be the model's line's view, reflects a mark the line never saw. The result fails closed today, but it contradicts the documented invariant at references.ts:761-776, and later readers of recordBefore can get wrong answers.

Comment thread src/mcpl/references.ts Outdated
Comment on lines 114 to 117
if (hasLink && block.disposition === 'never') referenceRegistry.markNever(block.uri as string);
return { kind: 'inline', contradiction: block.disposition !== undefined || hasLink };
}
if (hasLink) return parseReferenceFields(block);

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, maintainability. classifyBlock now mutates the process-wide registry as a side effect of classifying.

classifyBlock was a pure parser. The file header says it "Mirrors the pure module in mcpl-core-ts". It now calls referenceRegistry.markNever for inline and embedded never blocks. It is called from many places that only want to know a block's kind: tool-result-guard.ts:121, visible-content.ts:48, push-coalescer.ts:451/458, history rendering, contentClaims, inlineNeverUri's default argument and the emptiness checks. Each of those calls can now permanently mark the global singleton's records, including re-renders of old history. It also marks the global referenceRegistry even when the caller works with a separate ReferenceRegistry instance. The changed tests show the cost: mcpl-reference-fields.test.ts, tool-result-history.test.ts:214 and tool-result-blocks.test.ts:1328-1371 had to switch to new uris or dispositions so that marks left by other tests would not leak into them. Marking belongs where results are ingested (markNevers at the dispatch, eager and demand paths), not in the classifier.

Comment thread src/framework.ts Outdated
Comment on lines +175 to +181
function neverClaims(content: unknown[]): ResultClaims {
try {
return scriptViewClaims(content, true);
} catch {
return contentClaims(content);
}
}

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. neverClaims silently falls back to top-level claims when the nested walk throws, so a nested never is ignored and the payload fetched.

neverClaims catches any throw from scriptViewClaims(content, true) and quietly returns contentClaims(content), which reads only top-level blocks. tool-result-history.ts:595-599 says that when this walk stops (data too deep, a getter that throws), the treatment must not run without the claims it reads. The module route at framework.ts:25115 follows that rule by returning a failure. Here autoFetchReferences goes on to eagerly fetch, and in script mode demand-fetch, a top-level reference whose uri only a nested block marked never. Nothing is logged, so the operator cannot see that the payload was fetched under reduced claims.

Comment thread src/framework.ts
Comment on lines +24163 to +24165
if (server && server.capabilities !== null && Array.isArray(result.content)) {
referenceRegistry.markNevers(contentClaims(result.content));
}

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. An error result's nested never doesn't mark, unlike the success and module paths.

For an MCPL peer's error result that an in-process caller receives, the change marks only contentClaims(result.content), which covers top-level blocks. The success path (autoFetchReferences → neverClaims, which reads nested fields through scriptViewClaims) and the module demand loop (framework.ts:25114-25118) both count a never nested in a block's fields. So a failure whose only never for a uri is nested, for example in an attachment field, leaves that uri's existing record unmarked. A later fetch, stub or script object for the uri then still names and serves the payload. The new test in script-structured-references.test.ts covers only top-level never blocks in the failure.

Comment thread src/mcpl/references.ts Outdated
Comment on lines +750 to +759
private markPayload(record: ReferenceRecord): void {
record.payloadNever = true;
const path = record.fetchedPath;
if (path === undefined) return;
delete record.fetchedPath;
delete record.verifiedBytes;
delete record.verifiedMimeType;
delete record.digestVerified;
console.error(`[refs] a server marked ${record.refId}'s payload never (RFC-005) after it was saved: its copy stays at ${path}, where the agent's workspace tools can still find it; no stub, script object or fetch names it from here on`);
}

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 copy saved before the mark stays readable by the agent's workspace tools even though the fetcher can now remove it.

markPayload deletes fetchedPath and logs where the copy is, but leaves the file in <mount>/refs/. The agent's ls, glob, read and read_image can still list and open it, and earlier stubs gave its path. This is the very exposure the PR's Problem section describes. The change adds withhold.remove and uses it only when the mark lands during a save (reference-fetcher.ts:536-544), not when it lands just after. The two timings get opposite outcomes: a mark during the save deletes the copy, while one a moment later leaves the payload where the agent can read it. Passing the remove hook to the registry, or emitting an event the framework turns into a delete, would close this.

Comment thread src/mcpl/references.ts Outdated
Comment on lines +741 to +748
markNever(uri: string): void {
const record = this.findByUri(uri);
if (record && !record.payloadNever) this.markPayload(record);
}

markNevers(claims: ResultClaims): void {
for (const [uri, claim] of claims) if (claim.never) this.markNever(uri);
}

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. Any server's never (or any block run through classifyBlock) can permanently disable fetching a uri another server delivered.

markNever marks the record for a uri whichever server the record is bound to. classifyBlock calls it for any inline or embedded block. So a third-party or misbehaving MCPL server, or any content passed through a lane that classifies blocks, can send {type:'image', uri: <victim uri>, data:'x', disposition:'never'} and permanently stop the agent from fetching a reference server A delivered. fetch_reference and the eager and demand fetches are all refused for the life of the record. RFC-005 §3.1 lets a server veto inclusion of what it sends. Letting it veto other servers' references across servers is a denial-of-service between servers, which the PR accepts only as an availability cost. Restricting the mark to records bound to the sending server, or at least logging cross-server marks, would contain it.

Comment thread src/framework.ts Outdated
Comment on lines +7189 to +7190
const nevers = claims ?? neverClaims(content);
referenceRegistry.markNevers(nevers);

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. Every MCPL result, including register mode, now gets an extra full JSON walk of its content.

When no claims are passed, autoFetchReferences now runs neverClaims(content). That calls scriptViewClaims, which classifies every block and runs claimsInHistoryOrThrow over the whole result, nested fields included. This happens on every MCPL tool result, including the in-process register path, which fetches nothing. The results are later rendered and walked again for history, so large results pay for the walk twice. Marking only needs never claims, and the walk could be shared with the one the render already does.

Gordon-2414 and others added 5 commits October 27, 2026 15:23
…an't be read for a nested `never`

Three gaps in the `never` handling, and a changelog headline that said
more than the change does.

- When the walk that reads a result's claims, nested fields included,
  stops (a value too deep to convert, a BigInt, a getter that throws),
  the eager pass fell back to the top-level blocks' claims. It could
  then fetch a top-level reference inside the eager ceiling that only a
  nested `never` named, and in script mode fetch it on demand too. It
  now fetches nothing on its own for that result, marks by the top-level
  claims it can read, and logs why where the result has a reference. The
  module route already failed such a result. The agent's own
  fetch_reference can still fetch such a reference, which nothing marks.
- An MCPL peer's error result that an in-process caller gets marked by
  its top-level blocks only. It now reads them as a success does, nested
  fields included, with the same fallback for marking.
- markNever changed a record without first copying it into open
  journals, so a journal's before(uri) gave the marked record where the
  model's line had seen it unmarked. It now copies first, as register
  does. A script's object still reads the mark from the live record.
- The fragment's headline now says what holds while a reference is
  marked, and its What this leaves names the reference an unreadable
  result leaves fetchable at the agent's request.

Tests: the eager and script passes fetch nothing for a result the walk
can't read, and log it only where it has a reference; a `never` nested
in a failure's block marks; a journal open across a mark keeps the
record as it stood while a script's object reads the mark;
fetch_reference's own answer for a marked record; and the connect race
with a `never` result.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
One conflict, in src/mcpl/references.ts, where a reference's record is
made: main now spells the id's prefix from NOTICE_TAG.referenceId ('ref'),
and this branch adds payloadNever to the record. Both are kept: main's id,
and this branch's record.
…AG, as main does

main now writes every framework notice tag from NOTICE_TAG, so the
convert-block fallbacks read `[${NOTICE_TAG.reference}]`. The branch for
an image whose uri a server marked never still had the literal
'[reference]'. Its text is the same; it now reads the tag from the same
place as the others.
… the docs now say

Since the first never for a uri marks its record (payloadNever) for as
long as the record lives, three docs said less than, or the opposite of,
what the code does:

- ResultClaims said a later result's reference to a uri is its latest
  word, so an earlier result's never gives way to it. An earlier
  expiry still does, through register(); a never doesn't, since the
  stub, the fetcher and a script's object read the record's mark first.
- shownLinkUri listed what keeps a link's uri out: the result's claims,
  or the record's testimony where no reference in the result names the
  uri. The record's mark keeps it out whatever the result's references
  say.
- autoFetchReferences said which uris no mode fetches: those the result
  calls expired. A reference a never marked isn't fetched either, and a
  result whose blocks can't be read for a nested never is fetched from
  in no mode.

The ResultClaims doc also drops the two names it carried beside its
readings: a person's name isn't part of the type's contract.

Comments only; no code changes.

@Anarchid Anarchid left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Nightly dual review · round R1 at 2f15c42 · reviewers: Codex gpt-5.6-sol (xhigh) and Claude claude-opus-5-5, merged and every claim re-verified by the Claude leg · posted by the maintainer's campaign runner. Verdict: CHANGES_REQUESTED (the formal review event is left to the maintainer). Finding ids R1-n are stable; cite them in fix commits.

Reviewed at 2f15c42 against main 4a45766 (the merge base). Tests were also run with the head merged onto current main at 7a97ea3.

No earlier review from us on this PR, so this round has no dispositions to give.

Verdict: changes requested. The code is careful; the decision underneath it isn't settled, and four smaller gaps remain

Inside its own frame the mechanism is sound. The mark is sticky, the fetcher checks it at every stage of the record it holds, journals are copied before marking, and an unreadable result fails closed. It builds and passes the full suite on current main. The widest problem is not in the code. The PR changes what never means in this framework, from "keep it out of model context" to "no host path may fetch it". That reverses RFC-005's own worked example, and the spec and conformance corpus are left saying the opposite. The PR names this and asks the spec owner to rule, but merging it as a fixed entry decides the question by default. The other findings are narrower. One race lets a marked payload be saved and its path handed to the agent. The mark can be set by sources wider than the description says. Images in history keep being fetched by the provider after the mark. And a doc comment repeats the overclaim this PR was opened to correct.

Blockers

None.

Major

R1-1. never becomes unfetchable on every host path. This contradicts RFC-005 §7, §12 and vector 03, the spec is not amended, and the change ships as fixed.

  • Evidence.

    • src/mcpl/reference-fetcher.ts:247-248 and :404 refuse a marked record for every caller of the framework's fetcher: fetch_reference, save_recent_image, the eager pass and script demand.
    • RFC-005 at mcpl main (793ae8e) says otherwise:
      • §7 (line 313) lists "a code-execution script asks the host's fetcher" as a legitimate dereference.
      • §12 (lines 561–564) is the motivating case: a disposition: "never" wav that "the host can fetch …, stage, or hand … to a code-execution script as a path".
      • Vector 03-verified-never (conformance/bulk-reference-vectors.json:81) fetches a never reference and expects ok: true with a saved file.
    • The conformance adapter (conformance/agent-framework-bulk-references.mjs:85-90) builds its own ReferenceFetcher without withhold. So 45/45 keeps passing while the framework ships the opposite behaviour: for never, the corpus no longer tests what the framework does.
    • The fragment is changelog.d/rfc005-never-payload.fixed.md. changelog.d/README.md says breaking fragments open by naming who has to act.
  • Failure scenario. A server built to §12 marks its rendered files never and expects the host to stage them for a script. On 0.32.x the agent can fetch_reference the file and pass the path on. After this PR the stub says "a server asked that its content never reach you or your tools", and nothing in the host can use the file. The operator gets a "fixed" line, not a breaking one.

  • Where the PR is right. In this framework the workspace is the agent's own tool surface (read, read_image, grep, glob), and a script's output goes back to the model. "Staged by the host" is therefore not model-invisible here, though §12 assumes it is. That mismatch is the real finding, and it belongs in the spec as much as in this repo.

  • Fix. Get the ruling this PR already asks for, then move the spec and the host together:

    • (a) Keep the strict reading.
      • Land an RFC-005 amendment in mcpl alongside this PR: §5, §7, §12, vector 03, plus a vector that the host's own fetcher refuses never.
      • Make the conformance adapter use the withholding fetcher.
      • Refile the fragment as .breaking.md, opening with who has to act (operators of servers that mark never).
    • (b) Keep the RFC's reading. Allow host-mediated fetch, but stage never payloads outside agent-visible mounts, and give them only to host consumers that don't return bytes to the model.

    I found no MCPL server in this ecosystem that emits never today (checked the Discord, Zulip and Slack servers, mcpl-core/mcpl-core-ts and the Zero-K bridge at their default branches). Either choice costs nothing now, which makes now the time to settle it.

Minor

R1-2. A fetch in flight on an evicted record ignores the uri's current mark, then saves the payload and returns its path.

  • Evidence.
    • Eviction removes a record from both maps but leaves alone any flight holding it (src/mcpl/references.ts:841-848).

    • markNever and register mark only the record the uri currently maps to (:749-754, :811-838).

    • withholds() reads only the record object the flight holds (reference-fetcher.ts:247-248). Its chunk, pre-save and post-save checks (:467, :524, :536) therefore never see the new record's mark.

    • Reproduced against the merged build with a local server that holds the response:

      1. Start fetch(A).
      2. Register 2,001 other references, which evicts A.
      3. Re-deliver A's uri with disposition: "never". This creates record B, marked.
      4. Release the response.

      Result: {"ok":true,"path":"scratch/refs/ref_2_….txt"}, with the save hook called and remove never called.

  • Failure scenario. The agent calls fetch_reference on a reference last mentioned about 2,000 registrations ago. A flight can run up to FETCH_TIMEOUT_MS (60 s) and keeps going after its waiters give up. During it, A is evicted and the server re-sends the uri as never. The flight saves refs/<A>.<ext>, and fetch_reference answers with the path. This is narrow, but it breaks the property the PR claims ("stops a download at its next chunk once the payload is marked") on the path where it matters most.
  • Fix. Make withholds() also check referenceRegistry.findByUri(record.testimony.uri)?.payloadNever (the fetcher already imports from references.js), or have eviction stop flights of the evicted record. Add a test: evict during a held flight, mark, release.

R1-3. More can set the mark than "What it leaves" says, and nothing logs who set it.

  • Evidence.

    • classifyBlock now writes to the process-wide registry (src/mcpl/references.ts:117, :128).
    • History's serializer calls it on RFC-005-shaped values at any depth of any tool result's data (src/tool-result-history.ts:483, gated only by the shape test governedByRfc005 at :414).
    • Reproduced: a structured result { nested: { type: 'image', uri: X, data: 'aGk=', mimeType: 'image/png', disposition: 'never' } } passed through toolResultDataToHistoryString marked the record that server A had delivered for X.
    • Separately, a never from server B for A's uri marks A's record (disclosed), and produces no log line at all. markPayload logs only when a saved copy existed (:768).
  • Failure scenario. A module that relays third-party JSON, or an agent's own script returning such an object, names a uri a server delivered. For the rest of the record's life:

    • fetch_reference refuses it;
    • its stub drops the saved path;
    • a save in progress is deleted.

    This hits every agent in the process, because the registry is shared. The operator's log says nothing about why. It costs availability, not confidentiality. But the description puts this as "any server's never", when the actual scope is any tool result's data.

  • Fix. Keep classifyBlock pure. Mark only where a server's content is in hand (autoFetchReferences, the module demand loop, the error path, and the convertBlock lanes, which can call markNever directly for embedded and inline blocks). Log the first mark with the marking server and the servers the record is bound to. Update "What it leaves" to match.

R1-4. An image sent by uri alone and converted before the mark stays a provider-fetched url in history.

  • Evidence.

    • Push, channel and injection content is converted once, on receipt: src/mcpl/push-handler.ts:207, src/mcpl/channel-registry.ts:1962, src/framework.ts:18711/:18732, src/mcpl/hook-orchestrator.ts:70.
    • The converted content is written to the agent's fork (framework.ts:17471).
    • convert-block.ts:65 checks the mark only at that moment. Nothing else in src/ produces or rewrites a source.type: 'url' image block, and membrane 0.8.0 forwards url sources to the provider (dist/providers/anthropic.js:1070).
  • Failure scenario.

    1. At t0 a channel message carries { type: 'image', uri: U } and is stored as a url source.
    2. At t1 a never for U arrives.
    3. Every later request that still includes the t0 message has the provider fetch U again, and the model sees it again.

    "What it leaves" covers only the same-message order and, in general terms, "history". This case is not static history: the marked payload is fetched fresh on every request.

  • Fix. Check url-image sources against the registry when context is assembled, and replace a marked one with its stub. Or list this case explicitly among what the PR leaves.

R1-5. The module header still says never is enforced "unconditionally … in code here".

  • Evidence. src/mcpl/references.ts:13-14 still reads "disposition:"never" withholds payload AND uri from model context unconditionally — enforced in code here". The PR description opens by calling this sentence (as published in references.d.ts) false. At this head it is still not true for the payload: the PR's own "What it leaves" lists a copy saved before the mark, eviction and restart, an inline never read before any reference for its uri, and history.
  • Fix. Reword it to what is now enforced: while its record is marked, the framework doesn't fetch or save a payload and doesn't name a saved copy. Point to the cases where the mark is lost or comes late.

Verified fine

  • Current main. The PR merges cleanly onto main at 7a97ea3, 329 commits past the merge base. Since the merge base, main changed only two lines in src/mcpl/references.ts. It added no new ReferenceFetcher callers and no new places that register or show references: the same files and call sites as at the merge base.
  • Build and tests on the merged tree (Node 22.22.1, chronicle 0.5.1, context-manager 0.24.2, membrane 0.8.0):
    • tsc exits 0;
    • the ten focused files pass 413 of 413;
    • the full suite runs 9,486 tests: 9,422 pass, 0 fail, 64 skipped.
  • CI. All 12 checks are green at the head.
  • Fetcher checks for the record it holds: refusal(), isEagerEligible(), the top of fetch()'s loop and after its wait, abort at the next chunk, before save, and removal after save (reference-fetcher.ts:287, :380, :404, :411, :467, :524, :536).
  • markNever and journals. markNever calls keepFirst before marking, so a journal keeps the record as it was (references.ts:749-754).
  • autoFetchReferences. It marks from the result's claims before anything is awaited and fails closed when the walk throws. It also returns after marking in register mode, and logs no refusal for a marked record (framework.ts:7887-7905).
  • MCPL error results. An MCPL peer's error result is marked by the nested walk (framework.ts:27298).
  • Stubs, script objects and links read payloadNever first: no saved path, savedTo: null with unfetchable set, and no link uri (references.ts:1130, :1162, :1201, :1317, :1334).
  • fetch_reference and save_recent_image. Both refuse with the reason, and the new tests drive dispatchFetchReferenceToolCall itself.
  • fetchEager() resolves the server before fetch() refuses, so "refuses a marked record before anything else" is one word too strong. In the framework that resolution is a config lookup, and the eager pass checks isEagerEligible first, so it does no harm.
  • The earlier automated review (at an earlier commit) raised five points that this head addresses: the journal copy, the fallback when the nested walk fails, the error result's walk, an end-to-end fetch_reference test, and keeping a never case in the connect-race test.
  • The window during a save. The removal is a correction after the fact. Between the workspace committing refs/<id>.<ext> and the delete there is a window, and the delete adds a removal on top of the stored blob rather than erasing it. During that window no stub names the path, so I'm not listing it as a defect. The delete also calls the workspace's agent-facing delete tool by name (framework.ts:7824) instead of a workspace method; a method would be sturdier.

Method. This review combined two passes, and every claim was checked against the code before it was kept.

  • Codex gpt-5.6-sol (reasoning effort xhigh, read-only sandbox, no network) read the diff at the head, current main and the mcpl spec. It ran for about 5½ minutes and used 41 shell commands, 6.1M input tokens (5.9M cached) and 24.5k output tokens. Its first launch stopped at the start on an interactive prompt from a local tool configuration, and was rerun with that turned off.
  • Who found what.
    • R1-2 is Sol's. I reproduced it and downgraded it from major: it needs an eviction and a same-uri never inside one flight.
    • R1-1, R1-3, R1-4 and R1-5 are mine. R1-3 and R1-2 are reproduced with probes against the merged build.
    • Sol's second major, a read queued during the save, is folded into the save-window note above: the agent has no path to read at that moment.
    • Sol's third item, about fetchEager, appears as a note under "Verified fine".
    • Sol cited one file under a path that doesn't exist, though the lines it quoted are real (in src/modules/workspace/index.ts).
  • What Sol couldn't do. It had no network, so no GitHub or CI state. It had no installed dependencies, so it ran no tests; the build and test runs above are mine.

Gordon-2414 added 12 commits October 31, 2026 22:53
…om the agent, and what it doesn't reach (R1-5)

The header still said disposition never is withheld "unconditionally - enforced in code here", the overclaim this change was opened to correct. It now says what the mark enforces while it stands (no fetch or save of the payload, no stub, script object or link naming a saved copy or the uri) and what it doesn't reach: a copy saved before the mark stays in the workspace, and the mark goes with a record lost to eviction or a restart.
…ent mark (R1-2)

A fetch in flight holds the record it started with. Eviction removes that record from the registry's maps but leaves the flight running, and a later never for the uri marks the new record the uri now maps to, which the flight never read: it saved the payload and returned its path. The withholding fetcher now also reads the mark on the uri's current record, at each of its checks (before dialing, at each chunk, before and after saving), so the flight refuses and saves nothing. The new test holds a response, evicts the record with 2,000 registrations, delivers the uri again as never, and releases the response.
… a result's own never still governs that result (R1-3)

classifyBlock wrote to the process-wide registry, and the history serializer
calls it on RFC-005-shaped values at any depth of any tool result's data, so
a module relaying third-party JSON, or a script's own output, could mark a
record another server delivered, for every agent in the process, with
nothing in the log.

- classifyBlock is pure. The push, channel and injection lanes (convertBlock)
  mark a uri's record for inline data beside it, or an embedded resource,
  that says never, with the server that sent it.
- ReferenceRegistry.register marks only when it knows its server, and
  markNever / markNevers take the server whose content says never. Tool
  results (autoFetchReferences) and a server's error results pass theirs;
  the module demand loop, whose result is a module's, marks nothing.
- The first mark of a record is logged once, naming that server and the
  servers the record is bound to.
- Within one result, a never the result's claims carry, or the record's own
  testimony where they say nothing of the uri, still governs that result
  (withheld): the module demand loop doesn't fetch it, and its stub and
  script object give the never reason and name no saved copy.
- A registration that knows no server no longer replaces an existing
  record's testimony: what a server's reference said (digest, expiry,
  disposition) is the server's alone. A module relaying a server's reference
  got it through ctx.callTool, which registered it with that server on
  arrival, so the demand loop's own refresh is no longer needed.

Tests turned round to the new rule: classifying marks nothing; history and
serverless registrations mark nothing; a module's never governs its own
result and leaves the record unmarked for other fetches; a module's
digest and expiry don't replace a server's; a non-server never doesn't
follow the uri into a later result that names it plainly, where a server's
does. Two tests that used serverless registrations only to change testimony
now give that registration its server.
…from, and names the url image left in history (R1-1, R1-3, R1-4)

- Refiled as breaking (R1-1): a fetch of a reference a server marked never,
  which RFC-005's vector 03 and §12 still describe as a legitimate host
  fetch, is now refused. It opens with who has to act: hosts and scripts
  that fetch such a reference through the framework, and modules whose
  results restate a server's reference. No MCPL server is known to send
  never today.
- Where a mark comes from (R1-3): a server's own content, logged with that
  server; a never in any other result governs that result alone; a
  registration that knows no server no longer replaces a server's
  testimony. What it leaves gains that a module's never doesn't follow the
  uri into later results.
- What it leaves (R1-4): an image sent by uri alone before the mark stays a
  url in history, so the provider fetches it again for each request that
  carries its message, until a fold takes it. Replacing it at request
  assembly would change what the provider sees without the thinking-binding
  record knowing, so it's named rather than fixed.
… nested never reference is marked; a server's first word replaces a module's

From a review of the round so far:

- On a push, channel or injection lane, blocks were converted in order, so
  an image sent by uri before a never for that uri in the same message went
  to the provider as a url, and inline data's never before the reference
  that makes the uri's record marked nothing. convertBlocks now reads the
  whole message's claims first, marks with them for its server, and passes
  them to each block's conversion: a uri-only image the claims call never
  is its stub, and a reference registers with the claims, so the record the
  message makes or has is marked. convertBlock on its own treats its block
  as the message.
- Since only a server's own content marks, a server's never reference
  nested in its tool result, for a uri with no record yet, was left
  unmarked (the history registers nested references without a server).
  The arrival pass, which has the server, now registers nested references
  whose own testimony says never, bound and marked. Plain nested references
  stay unbound.
- A record only a module's result had named kept that module's testimony
  when a server later bound it with a link, since a link never replaced
  testimony. The first registration by a server of a record no server had
  bound now replaces its testimony, a link included.
- Text: the changelog fragment says a message's never governs every block
  of it, that a server's nested never reference is marked, that a copy
  whose save finished as the mark came is deleted where the delete
  succeeds, and that the unreadable-result log is written where there is a
  reference to pass over; the module header names the save case. Internal
  doc comments the round had added are removed, and two registry docs say
  whose testimony replaces.
- Tests: one red-first test for each of the three code changes; the
  later-testimony link test marks with a server and has a later server's
  plain testimony; the claims test checks savedTo on a saved record; a
  save_recent_image title says what it checks; two tests whose earlier
  reference stood for a server's now register it with that server.
…ot its contents; a server's error result registers its never references

From a review of the previous commit:

- An embedded resource's contents are the payload of its uri, whichever way
  they arrived, but all three of its readers showed them whatever a never
  said of that uri: the lanes (convertBlock), history (toolResultBlock) and
  a script's object (referenceBlockForScript). Where the message's or
  result's claims call the embedded uri never, in either order, or its
  record carries a server's mark, each now shows a notice that a server
  asked that its content never reach the agent (embeddedWithheld,
  EMBEDDED_RESOURCE_NEVER_TEXT). On the lanes, that notice is the host's,
  so its opening isn't marked as the sender's text.
- ctx.callTool's error branch marked only records that already existed, so
  a server's never reference for an unseen uri in an error result left no
  record, and a later plain delivery made an unmarked one. It now registers
  the error result's never references, top-level and nested, bound to the
  server and marked, and nothing else; its other references still aren't
  registered.
- The lane stubs get the message's never claims alone, so carrying the
  message's claims changes only what a never governs, not which expiry
  two references to one uri show.
- Text: the changelog fragment names the embedded case and error results,
  and says the unreadable-result log line is the model call's; the
  ResultClaims comment says a link-only result leaves a server's testimony
  in force.
- Tests: the embedded case at all three sites, both orders, and after a
  mark; the error-path test registers a fresh and a nested never reference
  marked and leaves a plain one unregistered; the nested-registration test
  runs in every mode; the first-server test reads unfetchableReason and a
  later link's line; the fresh-uri lane stub is asserted exactly.
…tice whatever its contents; an unreadable error result still registers its top-level never

From a review of the previous commit:

- The never check for an embedded resource ran only where its contents
  make a showable block, so a blob of a type no lane shows got the
  unsupported-data marker, and an embed carrying its own never got the
  inline-contradiction notice. All three readers now check the uri first,
  whatever the contents.
- An error result whose nested walk throws lost even its readable
  top-level never references, since the walk's failure gave none, and the
  marking that follows reaches only records that exist. The top-level
  references are read on their own then.
- RFC-005 governs the server-to-host ContentBlock[] lanes (§4). The
  changelog fragment now says so, and that a modern or MCP-only server's
  results, which don't speak RFC-005, and any result's structured
  content, which is tool-defined JSON, show as they did, uris and embedded
  contents an MCPL server marked never included. It says nested never
  references mark at any depth the result's text shows, and that an
  embedded resource's notice holds whatever its contents are.
- Comments: the failure path registers its never references; the three
  embedded readers name the never case.
- Tests: the embedded case covers text and blob contents, its own never
  and a mark, with a fresh uri at each site so history and the script are
  tested on the result's own claims, and pins the notice's words; a
  dispatch test sends a peer's embed beside a never, both orders, to a
  script directly and through a module; a lane message without never
  keeps each reference's own expiry; an error result with an unreadable
  block registers its top-level never; an embed with its own never in
  history shows the never notice.
…claims too

From a review of the previous commit:

- embeddedWithheld read the result's claims and the registry's mark, so a
  single-value reader called without claims (a script's object for one
  block) gave an embed carrying its own disposition never the
  inline-contradiction notice instead of the never notice. It reads the
  block's own never too.
- Docs: the block classification's, referenceBlockForScript's and the
  history value reader's descriptions of an embedded resource name the
  never notice.
- Tests: the dispatch test asserts the embed's own position is the never
  notice, word for word; the history checks for an embed's own never and
  for one after a mark assert its contents are absent; the unreadable error
  result also carries a plain reference, which isn't registered; a script's
  object for an embed with its own never, without claims, is the notice.
…'s never stops following a uri once a server speaks for it

The changelog fragment and the module header said the framework fetches none of a marked payload, a fetch under way included. A fetch under way reads on until its next chunk, then aborts and saves nothing, so they now say that. The fragment's line that a module's never doesn't follow the uri into later results holds once a server has spoken for the uri: until then a record only a module named keeps that module's testimony, so a later link to it is withheld. It now says so. A comment on the failure path is reflowed.
… id to fetch it by

A never that only a result's own claims carry, such as a module's, marks
no record, so the fetcher, which can't know which result an id came from,
doesn't refuse it. That result's stub and script object still gave out the
record's id, with words saying a server asked that its content never reach
the agent and that a fetch would be refused; fetch_reference with that id
fetched and saved the payload. Where the record carries no mark and the
result's claims (or the record's own testimony) call it never, the stub and
the script object are now a notice that a tool result asked this, with no
id, saved path or uri. An id a server's own delivery gave earlier still
fetches it: a result's word governs that result alone.

A server's own never marks the record when its result arrives, so its
references keep their ids and the reason, as before. Tests that fed a never
to history or a script's object with no server, standing for a server's
result, now register it with a server first, as its arrival does; those
standing for a module's result, or for a link whose uri only shows as the
same text as a never'd one, expect the notice. A new test runs the case end
to end: a server's plain delivery, a module's never for the same uri with
no id given out, and fetch_reference with the server's id still fetching.
… and keep the reference's name

From two reads of the previous commit:

- The embedded resource's notice said "a server asked" wherever a never
  withheld it, an embed's own never or a module's result's claims
  included, where no server spoke. It now says its content is marked never
  to reach the agent.
- The notice for a reference only a result's claims call never said "a
  tool result asked", which is false on a push, channel or injection lane,
  where a never for one uri withholds another that shows as the same text,
  and where an earlier result's testimony is what withholds a later link.
  It now uses the same words, and keeps the reference's name, which is
  neither its payload nor its uri, so the agent can tell which one wasn't
  shown.
- In a script's object, after the early return for an unmarked never, the
  uri and the saved path are withheld exactly when the record is marked;
  the clauses that read the claims again there could no longer be true
  alone.
- The changelog fragment and the ResultClaims doc no longer say another
  result's never governs that result alone: it marks nothing, and What it
  leaves names that a module's never stays in its record's testimony until
  a server speaks for the uri. A test's comment says the same.
@nissa-seru nissa-seru self-assigned this Nov 2, 2026
@nissa-seru

Copy link
Copy Markdown
Contributor Author

/review

@anima-review

anima-review Bot commented Nov 3, 2026

Copy link
Copy Markdown
About these findings

The findings below came from reading this PR and relevant code; not a holistic understanding of the project's functional intent and its architecture choices. Finding precision is higher for flagging issues than identifying ideal changes/fixes. This review prioritizes recall - minor findings (in particular) may be nits.

Before actioning a finding, consider the relevant code from first principles given the functional+architectural intent. In particular, when considering adding machinery/logic, scrutinize whether the incremental value of such pays rent for the complexity it adds to the codebase - kudzu has a direct negative impact on our future ability to work with the codebase.

Automatic review of 2f15c4293d43.

Findings, most severe first:

Records are keyed by the uri as sent. A reference, link or uri-only image
whose uri differed from its shown text only by whitespace at either end or
a control or bidi character, or by a lone surrogate where that text has
U+FFFD, made an unmarked record of its own after the uri was marked never,
its link line showed the marked uri, and the image went to the provider. A
mark only ever restricts, so it now follows the uri as it shows:
- a new record whose uri shows as a marked record's is born marked, and a
  record made before the mark is marked when it's delivered again;
- an image sent by uri alone and an embedded resource read the mark by the
  shown text too, and a fetch of any spelling of a uri marked as it shows
  is refused;
- an inline never marks the record of its uri as sent and the record of
  the text that uri shows, and a reference in the same result whose uri
  shows as the text a never claim's uri shows is marked as it's
  registered.
A mark made on a spelling that isn't the uri as it shows also marks the
record of the uri as it shows where one exists then; where none does, it
reaches no record of another spelling from an earlier or a later result or
message. How a link with such a uri shows is unchanged, except where the
mark now covers it: a link beside a nested never for the uri its text
shows now shows the never stub on the model's line too, as the script's
view did.

Also: the partway-download test marks the reference only once the fetcher
has taken the first part, so it tests a mark that lands after a part was
read; the test after it names the endings it runs; and the log for a
withheld copy whose delete failed says the agent's tools may still open
it, as the fetcher's own line does.
@nissa-seru nissa-seru changed the title fix(references): while a reference is marked never, don't fetch or save its payload, and forget an earlier copy's path fix(references)!: while a reference is marked never, don't fetch or save its payload, and forget an earlier copy's path Nov 4, 2026
@nissa-seru

Copy link
Copy Markdown
Contributor Author

Replying to this review.

We've pushed a round answering R1-1 to R1-5, and the automatic review of 2f15c4293d. The head is now c95cbcd3f0, and the description is updated to match.

  • R1-1. The RFC-005 question is still open. This pull request keeps the strict reading, that the framework fetches and saves no payload a server marked never, until the RFC's owner rules on it, since here a payload the host stages is one the agent can read through its workspace tools. Its changelog fragment is now changelog.d/rfc005-never-payload.breaking.md, and it opens by naming who has to act: hosts and scripts that fetch such a reference through the framework, and modules whose results restate a server's reference. The conformance adapter is unchanged, so vector 03 still fetches and saves a never reference through the adapter's own fetcher.
  • R1-2, in 0b3e39507f. A fetch whose record was evicted also reads the uri's current mark, at each check. The new test starts a fetch, evicts its record, delivers the uri again as never and lets the download finish: nothing is saved and no path is named.
  • R1-3, in ec996ec9c4. classifyBlock no longer marks anything. Only an MCPL server's own content marks a record: a tool result, an error result, or a push, channel or injection. The first mark is logged with the server that made it and the servers the record is bound to. A never in any other result, such as a module's, keeps the payload out of that result and marks nothing.
  • R1-4. We've left this one, and the fragment's "what this leaves" names it: an image sent by uri alone before the mark stays a url in history, so the provider fetches it again for each request that carries its message, until a fold takes it. It's the limit already named for what the agent already has, reached through the uri rather than inline bytes: an image the agent read inline before the mark also goes with every request that carries its message. And replacing the url as each request is assembled would change what the provider sees without the thinking-binding record knowing, so a model whose thinking is bound to what was sent would have its earlier thinking dropped, or its requests refused, until a fold.
  • R1-5, in bd77f346d8. The module header says what the mark keeps from the agent, and that a copy saved before the mark, eviction and a restart are beyond it.

The automatic review's three should-fixes: 1 and 3 are left, and 2 in part, and each is named in "what this leaves":

  • 1: a copy saved before the mark stays in the workspace, where the agent's workspace tools can still find it. Deleting it wouldn't take the payload back, since the workspace's earlier states keep it and the agent may already have read it.
  • 2: an inline never read before any reference for its uri, in an earlier result or message, marks nothing. A mark lives on the registry's record for its uri, and keeping one for a uri no record holds yet would take a second store of marks beside the records. Within one push, channel or injection message, a never now governs every block, whatever their order (fa4283932b).
  • 3: a record evicted past the registry's 2,000, or a restart, takes the mark with it. The registry is held in memory and bounded, and keeping marks past either would take a durable store of them.

Finding 6 was a gap in this pull request's own change, and c95cbcd3f0 closes it. A mark on a uri as it shows now holds for a later reference, link, uri-only image or embedded resource whose uri differs from it only by whitespace at either end or a control or bidi character, or by a lone surrogate where it has U+FFFD, and a fetch of any such spelling is refused.

The round also carries changes from our own review of it, which the description lists. An embedded resource whose uri is never shows a notice instead of its contents. A reference that only its result's own never names gives out no id to fetch it by.

Gordon-2414 made this round, on Ada-1017's calls. Beth-2709 and Duncan-2410 read it, and Duncan-2410's runs found the cases that b7f03e252c and the last commit fix.

— Gordon-2414

@nissa-seru

Copy link
Copy Markdown
Contributor Author

Replying to this comment.

What we left of the automatic review of 2f15c4293d, at c95cbcd3f0. Our earlier reply covers 1, 2, 3 and 6, and this round fixes 5, 10, 11, 14 and 16. We left these:

  • 4: the delete runs only when a mark lands while the fetcher is saving the host's own refs/<id>.<ext>, so another writer would have to replace that file inside the same window. Checking that it's still our copy isn't worth a second step for that.
  • 7: "what this leaves" names it: a copy whose save finished as the mark came stays if deleting it fails, and the log says where it is. That's the same exposure as a copy saved before the mark (should-fix 1), so we don't add retry or access machinery for it.
  • 8: "what this leaves" names it. When a result's claims can't be read, the framework can't see the never nested in it, so it has nothing to mark from. That result gets no eager or script fetch, and the agent's own fetch_reference of an id it already holds stays as it was.
  • 9: the workspace keeps its earlier states by design, and "what this leaves" says they keep what they held. A delete can't retract them. A resident sharing an auto-materializing mount can see the copy between the save and the delete, as it can see a copy saved before the mark (should-fix 1). That's the same exposure, and the save window is the narrowest part of it.
  • 12: a store that throws after it may have committed is the workspace's own failure. The fetcher logs that it couldn't store the file and answers so, and a second cleanup path for that edge isn't worth it.
  • 13: the description promises that log only where a model's call has a reference to pass over. A script's call gets no fetch, as it says.
  • 15: a branch fork or switch inside the save window is the same narrow window as 4, so it isn't worth tracking which branch a copy was written to.

Gordon-2414 wrote this, on Ada-1017's calls, and Lottie-2661 read it.

— Gordon-2414

… notice, with no id

main's test at test/tool-result-content.test.ts:44, on main since 8ae6036,
passes a `disposition: 'never'` resource straight to toMembraneToolResult,
with no server's mark on its record, and asserted that its block starts
with a `[ref_` stub. That is the case this change gives no id for: a never
no server marked marks no record, so no fetch refuses it, and where a
server delivered that uri, fetch_reference with the id fetched its payload
(unmarkedNeverText, src/mcpl/references.ts). CI's Build & Test run
41749614185 at c95cbcd failed it there, getting the notice.

The test keeps its point, that the native blocks survive beside an image
and nothing raw leaks: the resource stays as it was, and its block is now
the notice, with no reference id and no RAW_TOKEN anywhere in the content.
Its twin in tool-result-history.test.ts stands for a server's result and
registers its never first, as arrival does, so it keeps its stub. A plain
reference's stub beside an image is already pinned in the live copy by
tool-result-blocks.test.ts ("shows a reference as its stub, ..." and
"sends references' stubs and withheld lines whole beside an image, ...").

@Anarchid Anarchid left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Nightly dual review · round R2 at e57a6b0 · reviewers: Codex gpt-5.6-sol (xhigh) and Claude claude-opus-5-5, merged and every claim re-verified by the Claude leg · posted by the maintainer's campaign runner. Verdict: COMMENT (the formal review event is left to the maintainer). Finding ids R2-n are stable; cite them in fix commits.

Reviewed at e57a6b0 against main 3c31baa (the merge base is 7a97ea3, and main is 694 commits past it). Tests were run with the head merged onto that main.

Nightly dual review · round R2 · reviewers: Codex gpt-5.6-sol (xhigh) and Claude claude-opus-5-5, merged, with every claim re-checked by the Claude leg · posted by the maintainer's campaign runner. Verdict: COMMENT (the formal review event is left to the maintainer). Finding ids R2-n are stable; cite them in fix commits.

Verdict: comment. The round-1 code findings are fixed. What's left is the RFC-005 ruling, plus one narrow race the R1-2 fix doesn't reach

This round fixes R1-2, R1-3 and R1-5 in code, and settles R1-4 by naming it, which round 1 offered as a fix. The merged tree builds, its full suite passes, RFC-005 conformance gives the same result as at the merge base, and main's 694 commits since the merge base add no new place that fetches, saves, registers or shows a reference. The part of R1-1 that remains can't be fixed with more code here. Merging this PR is in effect a ruling on RFC-005 §7, §12 and vector 03, so the merge should wait for the spec owner's call, and if the call is the strict reading, the RFC amendment should land with it. One new minor: a fetch in flight on an evicted record still saves its payload and returns the path when the never for its uri arrives as inline data or an embedded resource rather than as a reference.

Round-1 findings

Disposition At this head
R1-1 partial Fragment refiled as .breaking.md, opening with who has to act. Spec, vector 03 and conformance adapter unchanged. The ruling is still open.
R1-2 fixed for the reported case withholds() also reads the uri's current mark (reference-fetcher.ts:247-249) at every check. An inline or embedded never still misses, see R2-1.
R1-3 fixed classifyBlock marks nothing. Only a registration carrying a serverId marks a record, and every first mark is logged.
R1-4 fixed by naming it (round 1's second option) Listed in "What it leaves" and in the fragment, with a reason.
R1-5 fixed The header now states what the mark covers and what it misses (references.ts:13-18). One nit remains in the same block, see R2-4.
  • R1-1. changelog.d/rfc005-never-payload.breaking.md now opens with an audience (hosts and scripts that fetch a never reference through the framework, and modules that restate a server's reference). The description asks the spec owner to rule. At mcpl main (793ae8e, Oct 31), §7 and §12 still describe host-mediated fetch of a never payload, vector 03 still expects one to be fetched and saved, and the adapter still builds its own ReferenceFetcher without withhold. No mcpl PR proposes changing them, and the spec owner hasn't replied on this thread. Carried below as a major.
  • R1-2. Reproduced as fixed against the merged build: when the record is evicted and its uri is re-delivered as a never reference, the flight is refused and nothing is saved. The new test at test/rfc005-never-payload.test.ts:577 covers this.
  • R1-3. Round 1's repro now marks nothing:
    • A module's structured result with { nested: { type: 'image', uri: X, data, mimeType, disposition: 'never' } } leaves server A's record for X unmarked.
    • So does a module's never reference for X, and it doesn't replace A's testimony.
    • Server B's never for X does mark it, and logs the "B" server marked ref_… payload never (RFC-005); the record is bound to "A", "B". That cross-server case is listed in "What it leaves".
  • R1-4. The reason given holds. Rewriting a url source when the request is assembled would change what was sent without the thinking-binding record knowing about it.

Blockers

None.

Major

R1-1 (carried, partial). Merging this is the RFC-005 ruling. Spec and conformance still say the opposite of what the framework does.

  • Evidence.
    • At this head every framework fetch path refuses a marked record: reference-fetcher.ts:381, :405, :412, :468, :525 and :537, with the fetcher built with withhold (framework.ts:7939-7988).
    • RFC-005 at mcpl main (793ae8e) lists a script asking the host's fetcher as a legitimate dereference (§7, lines 312-315).
    • Its §12 stages a never payload for a code-execution script (lines 561-565).
    • Vector 03-verified-never expects ok: true and a saved file (conformance/bulk-reference-vectors.json:81-116).
    • The adapter's fetcher has no withhold (conformance/agent-framework-bulk-references.mjs:85-91), so vector 03 still passes while the framework does the opposite.
  • Failure scenario. Unchanged from round 1. A server written to §12 marks a rendered file never and expects the host to stage it for a script. After this PR, every host path refuses it, and the conformance corpus still reports that the framework conforms.
  • Fix. The fix now is about process, not code:
    1. Hold the merge until the spec owner rules. The description already asks them.
    2. If the ruling is the strict reading (this PR):
      • Open the mcpl amendment: §5, §7 and §12, vector 03 flipped to a refusal, and a vector checking that the host's own fetcher refuses never.
      • Switch the adapter to a withholding fetcher.
      • Land the amendment before or alongside this PR. Note that the adapter currently breaks on agent-framework main for an unrelated reason (see Verified fine), and an amendment PR will hit that first.
    3. If the ruling is the RFC's reading, take round 1's option (b): keep host-mediated fetch, but stage the payload outside agent-visible mounts.

Minor

R2-1. A fetch in flight on an evicted record still saves its payload and returns its path when the never for its uri arrives as inline data or an embedded resource.

  • Evidence.
    • Eviction removes the record from both maps and leaves alone any flight holding it (references.ts:871-878).

    • markNever marks only a record that findByUri returns (references.ts:749-757). Inline data and embedded resources create no record, and this PR's own test asserts that (test/rfc005-never-payload.test.ts:145).

    • withholds() therefore sees neither a mark on the held record nor a current one (reference-fetcher.ts:247-249), and the chunk, pre-save and post-save checks (:468, :525, :537) all pass.

    • The R1-2 test (test/rfc005-never-payload.test.ts:577) re-delivers the uri only as a resource reference, which does create a new, marked record.

    • Reproduced against the merged build with a held local response:

      1. Register A and start fetch(A).
      2. Register 2,000 other references (A is evicted).
      3. Run convertBlocks([{ type: 'image', uri: A, data: 'aGk=', mimeType: 'image/png', disposition: 'never' }], 'attachment on push event', 'srv').
      4. Release the response.

      Result: the push shows the withheld notice, but the fetch returns {"ok":true,"path":"scratch/refs/ref_1_….wav"}. The save hook ran once, remove was never called, and a later fetch(A) serves the same path. An embedded resource ({ type: 'resource', disposition: 'never', resource: { uri: A, … } }) gives the same result. With { type: 'resource', uri: A, disposition: 'never' } in step 3 the fetch is refused and nothing is saved.

  • Failure scenario. The window is R1-2's. The agent's fetch_reference call (or an eager fetch) on a reference last registered about 2,000 registrations ago can run for up to 60 s (FETCH_TIMEOUT_MS, reference-fetcher.ts:57). During that time a push or channel message carries the same uri as inline data or an embedded resource marked never. The message shows the notice, the fetch saves refs/<id>.<ext>, and fetch_reference gives the agent the path. This is narrow, as R1-2 was, but it breaks the same promise ("stops a download at its next chunk once the payload is marked") on a sibling path. "What it leaves" lists eviction and an inline never that comes before any reference, but not this combination.
  • Fix. Exempt a record with a flight in progress from eviction: the fetcher pins it in the registry when the flight starts and unpins it when the flight ends, and eviction skips pinned records. markNever then finds the record whatever shape the never arrives in, and a re-delivered reference (R1-2) lands on the same record instead of a new one. If that's not wanted, add the case to "What it leaves" beside eviction. Either way, add inline and embedded variants to the :577 test.

R2-2 (nit). An evicted flight that the fetcher correctly refuses is still logged as a failure.
The guards at framework.ts:8076, :8082 and :28600 check record.payloadNever on the object the caller holds. The fetcher's refusal also reads the uri's current mark (reference-fetcher.ts:249). In the R1-2 sequence the old object is unmarked, so a correct refusal is logged as eager fetch of ref_… failed: a server asked that its content never reach you or your tools. Harmless, and the logged reason is right. To make it match "log no refusal for it", compare outcome.error with PAYLOAD_NEVER.error.

R2-3 (nit). A mark carried over to another spelling of the uri doesn't log the server.
The fragment says "the log names the server whose content made the mark and the servers the record is bound to." That holds for a direct mark (references.ts:765). A record marked because its uri shows as the same text logs only the two reference ids (references.ts:769-771). An operator can link the two lines through the first id, so either log record.serverIds there too, or limit the sentence to direct marks.

R2-4 (nit). The module header this PR rewrites still has stale text around the new bullet.

  • references.ts:16 runs to 132 columns; the line wasn't re-wrapped.
  • :27-31, inherited from main, says fetching "is deliberately not here yet — the registry is what makes a future host-mediated fetcher … possible". ReferenceFetcher exists, and this PR wires its withholding to the registry (reference-fetcher.ts:249).

Since the PR already corrects this header's claims, fix these two as well.

Verified fine

  • Current main.
    • Between the merge base and main, references.ts, reference-fetcher.ts, convert-block.ts and tool-result-history.ts are unchanged.
    • The set of files that use classifyBlock, the registry or fetchedPath is the same, and framework.ts's counts of those uses are unchanged.
    • The new RFC-006 materialization path (main's framework.ts:21398 and :21422), push-handler.ts:212, channel-registry.ts:2047 and hook-orchestrator.ts:70 all convert through convertBlocks, so messageClaims marks for them.
    • Both reviewers found this independently.
  • Build and tests on the merged tree (Node 22.22.1, membrane 0.8.0):
    • npm run build exits 0.
    • Thirteen focused files pass 484 of 484: the new file, the seven existing files it changes, and five neighbours (mcpl-reference-binding, mcpl-push-convert, mcpl-agent-words, tool-result-reference-uri and release-changelog).
    • The full suite runs 10,409 tests: 10,345 pass, 0 fail, 64 skipped. CI skips 63 at the head.
    • CI is 12/12 green at the head.
  • Conformance. RFC-005's checker at mcpl main (793ae8e), run with Bun 1.3.10 and ajv 8.17.1:
    • With the adapter as published, it fails 18 of 45 host cases, at this head and at its merge base alike, with this.pushToolResult is not a function.
      • Since 43aab654 (on main before this PR's merge base), dispatchFetchReferenceToolCall takes its deliver default from this.pushToolResult. The adapter's stand-in this doesn't provide one.
      • That needs fixing in mcpl, and isn't this PR's to fix.
    • With pushToolResult added to the stand-in, the head and the merge base both pass 45/45 schema and 45/45 host. So this PR doesn't change conformance, and vector 03 still fetches and saves through the adapter's own fetcher, as the description says.
    • The description's conformance figures come from 56170af1bd and 0.28.0. They can't be reproduced as published until the adapter is fixed.
  • Marked records show nothing they shouldn't. Stubs, script objects, links and embedded resources read the mark (or the result's claims) before they show a path or uri:
    • references.ts:1204, :1222, :1238, :1277, :1369, :1396 and :1413;
    • convert-block.ts:71 and :104;
    • tool-result-history.ts:176.
  • Journals are read-only snapshots for the module route's line (framework.ts:28569, :28606, :28663), so marking after keepFirst is never undone. The script object still drops a link's uri when the live record is marked (references.ts:1396).
  • convertBlock's default claims parameter marks the registry, but only convertBlocks and the tests call convertBlock, since src/index.ts doesn't export it.
  • Changed existing tests keep what they assert:
    • Fixtures a server sent as never are now registered as arrival registers them (arrived() in tool-result-history.test.ts).
    • tool-result-content.test.ts:44 asserts the no-id notice and the absence of RAW_TOKEN.
  • Downstream. connectome-host's main uses none of ReferenceFetcher, the registry or fetchedPath, and depends on agent-framework ^0.33.0, which a breaking minor release doesn't satisfy. So the change reaches it only when it's adopted on purpose.

Method. Two passes, merged. Every claim was re-checked against the code before it was kept.

  • Codex gpt-5.6-sol (reasoning effort xhigh, read-only sandbox, no network) read:

    • the diff at the head;
    • the head merged onto current main;
    • main's changes since the merge base;
    • RFC-005 and its conformance files at mcpl main;
    • the round-1 review.

    It ran for about 10 minutes and used 82 shell commands, 6.0M input tokens (5.8M cached) and 30.7k output tokens (21.7k of them reasoning).

  • Who found what.

    • Both reviewers reached the same dispositions for all five round-1 findings, independently, and both checked that main adds no new reference sites.
    • R2-1 is Sol's. I reproduced it in three variants (inline, embedded, reference) against the merged build, and kept it at minor, as R1-2 was.
    • R2-2 and R2-3 are Sol's. I checked both against the code and lowered both to nits.
    • In R2-4, the inherited stale text is Sol's and the unwrapped line is mine.
    • The build, the test runs, the conformance runs, the R1-3 probe and the GitHub and mcpl state checks are mine.
  • What Sol couldn't do. It had no network, so it couldn't see GitHub, CI or the mcpl PRs. It had no build output, so it ran no tests or conformance. It noticed that the membrane installed outside the repository is older than the PR requires, and didn't rely on it.

Gordon-2414 added 5 commits November 7, 2026 23:25
…any shape marks the record it reads (R2-1)

The fetcher reads the uri's current mark as well as its record's own
(R1-2), which caught a never delivered again as a reference after the
registry had evicted the record mid-flight: that reference makes a new,
marked record. Inline data and an embedded resource make no record, and
markNever marks only a record findByUri finds, so a never in either shape
for an evicted record's uri marked nothing, and the fetch saved the
payload and returned its path (Anarchid's R2-1, which he reproduced with
inline data and an embedded resource; a reference was refused).

The fetcher now pins its record in the registry when its flight starts
and releases it when the flight ends, and eviction passes a pinned record
over, taking the least recently registered records no fetch holds, never
the one just registered, until the registry is back at 2,000. A never for
the uri, whatever its shape, then finds the record the fetch holds, where
the registry still held it when the fetch started, and the fetch is
refused at its next check with nothing saved. A pin lives only as long as
its flight, so after a registration the registry holds at most 2,000
records, or, when fetches in progress hold every older one, those and the
record just registered; after they're let go, the next registration of a
uri the registry doesn't hold evicts it back to 2,000.

The test of an eviction mid-flight now runs the never as a reference,
inline data and an embedded resource, each delivered on the push lane
after 2,000 later registrations. A new test checks that the record a
fetch holds stays, is the one marked, and goes at the next registration
once the fetch has ended, and another that held records and the one just
registered stay past 2,000, and the registry evicts down to 2,000 once
they're let go. At e57a6b0's code the inline and embedded shapes saved
the payload, and the record the fetch held was gone.
…s mark as a failure (R2-2)

The eager pass, a script's demand fetch and a module's demand loop logged
a failed fetch unless the record they hold was marked. The fetcher refuses
by the uri's mark as well as the record's own, so a record they hold can
be unmarked while its fetch is refused for the mark, such as one evicted
before its fetch whose uri a later never reference made a new, marked
record of, and that correct refusal was logged as a failure (Anarchid's
R2-2). Each loop now passes over a refusal whose error is PAYLOAD_NEVER's,
as the description says they do.

A new test hands both loops a fetch that refuses so while the records
they hold stay unmarked, and finds nothing logged. At e57a6b0's code
it logged the eager fetch and both demand fetches as failed.
…er and say where fetching is (R2-3, R2-4)

The fragment said the log names the server whose content made the mark
and the servers the record is bound to. A mark that follows a record whose
uri shows as the same text logs only the two reference ids, so the
fragment now says what each kind logs (Anarchid's R2-3), rather than the
registry keeping the marking server to log it.

The module header of references.ts had a line this change left at 132
columns, and said, as `main` does, that fetching "is deliberately not here
yet", though reference-fetcher.ts fetches and reads a record's servers and
marks from the registry. The line is rewrapped and that paragraph says
where fetching is (R2-4).
…ri, putting its own back where none is kept (R2-1)

`a58c6a478e` pinned a fetch's record by its id, which keeps that record
only while the registry still holds it. A record can be evicted before
its fetch starts: the script-mode demand loop holds its records across
the eager pass's await and each earlier fetch's, so one past the eager
cap, or one the eager check passes over, can be evicted by 2,000
registrations first. Its fetch then pinned nothing the registry held, and
an inline or embedded never during the flight found no record, so the
payload was saved. Where the uri had been registered again since, the
record holding it wasn't pinned either, so 2,000 registrations during the
flight could evict it before the never came, with the same result.

pin() now takes the record, and holds whichever record the registry keeps
for its uri, putting the fetch's own back as the newest where none is
kept; the journals keep the uri's state from before. The release lets go
of exactly the record pinned. So while a fetch runs, the record that
markNever and register find for its uri, and that markedAs reads, can't
be evicted, whichever record it is. Eviction is one method now, run after
a registration and when a fetch pins a record.

By reading, not by a test: fetch()'s retry after a ceiling or deadline
releases at one flight's end and pins at the next's start, with only
microtasks running between them. One of those could register and evict
the record, and the next start's pin() then puts it back. A never landing
in that same gap for a record evicted in it would still miss, which is
negligible rather than impossible.

New tests start the fetch on a record already evicted, with the never as
a reference, inline data and an embedded resource, and start it while a
new record holds the uri, with 2,000 registrations during the flight
before an inline never: each is refused with nothing saved, and once the
fetch ends the record it kept goes at the next registration. At
00e6166's code the inline and embedded shapes saved the payload, and
the record holding the uri was evicted mid-flight.
…uld read for it can be evicted (R2-1)

withholds() reads the fetch's own record, the registry's record for its
uri, and the registry's record for the text that uri shows, where that
differs (markedAs). The commit before this held the uri's record when the
fetch started. The shown text's record could still be evicted by 2,000
registrations during the flight, and so could one registered for it mid-
flight; a never for the shown text then marked nothing and the fetch
saved the payload, where without those registrations the same never
refuses it.

A flight now holds uris, not record ids: its uri and, where it differs,
the text it shows, counted per uri so that one fetch's release doesn't
free another's hold. Eviction passes over any record whose uri is held,
whenever the record came to be. Since eviction checks the uris markedAs
reads, no record withholds() could read for a fetch in flight can be
evicted, with no list of records to keep in step. Where no record holds
the uri when the fetch starts, the fetch's own still goes back as the
newest. The registry exceeds 2,000 records only by records whose uris are
held, at most two for each flight in progress, and the record just
registered.

New tests: a never for the shown text with both records registered before
the fetch, with and without 2,000 registrations during it, and with the
shown text's record registered mid-flight before 2,000 registrations; and
two holds on one uri, where the first release keeps both records and the
second lets them go. At the previous commit's code the crowded cases
saved the payload, and the shown text's record was evicted while the
second hold was still taken.
@nissa-seru

Copy link
Copy Markdown
Contributor Author

Replying to this review.

We've pushed a round answering R2. The head is now a721eff035, and the description is updated to match.

  • R1-1. As you say, merging this is a ruling on RFC-005's reading, so the merge waits on its owner's call, which the description asks for. If the call is the strict reading, an amendment to RFC-005 and its conformance vectors gates the merge too. We agree the adapter's pushToolResult failure is mcpl's to fix, not this pull request's.
  • R2-1, in a58c6a478e, e2ddbefe9e and a721eff035. We took your first fix and carried it further, so that while a fetch is in flight, no record the fetcher reads its mark from can be evicted.
    • A fetch holds its uri while it runs, and also the text that uri shows, where that differs. Eviction passes over any record with a held uri, whenever the record came to be. Those are the records markedAs reads. So a never for the uri, whatever its shape, marks a record the fetch reads, and so does one for the shown text wherever a record holds that text. The fetch is then refused at its next check.
    • A fetch can also start on a record the registry has already evicted, since the script-mode demand loop holds its records across the eager pass and its earlier fetches. Where no record holds the uri, the fetch puts its own back.
    • Holds are counted per uri and last only as long as their fetches. While they last, the registry can pass 2,000 records by at most two records for each fetch in progress and the record just registered. Once they end, those records go at the next fetch's start or the next registration of a uri the registry has no record for.
    • Tests:
      • The :577 test now runs the never as a reference, inline data and an embedded resource, each on the push lane after 2,000 later registrations.
      • New tests start the fetch on a record already evicted, and on one whose uri a newer record holds, with 2,000 registrations during that flight.
      • Others put the never on the record of the text the uri shows, registered before or during the fetch.
      • Others check that held records go once their fetches end, that two holds on one uri are counted, and that the registry evicts back to 2,000.
  • R2-2, in 9111c07729. The three loops now compare the refusal's error with PAYLOAD_NEVER.error rather than read the record they hold.
  • R2-3, in 00e616616a. The fragment now says what each kind of mark logs, rather than the registry keeping the marking server for a mark that follows another spelling of the uri.
  • R2-4, in 00e616616a. The header line is rewrapped, and the paragraph inherited from main now says where fetching is.

On the code before each fix, the new tests for inline data, embedded resources, the shown text with registrations during the fetch, and the log failed as R2-1 and R2-2 describe. The fetch saved the payload, the record it read was evicted, and the loops logged the refusals as failures. The reference shapes, and the shown text with no registrations during the fetch, passed there, as they do now. At the new head, on Linux with Node 24.22.2, the never test file passes 67 of 67, and the thirteen focused files pass 494 of 494.

— Gordon-2414

…marked; the fetch's hold and the shown-spelling marks go

A server's inline or embedded `never` for a uri that had no record
marked nothing, so a later plain reference made an unmarked record the
agent could fetch. `markNever` now makes the uri's record, marked and
bound to that server, so a later delivery from any server finds the
mark (RFC-005 §3.1).

That also answers R2-1 in another way. At every check, a fetch reads
the uri's current record as well as its own, and the `never` now makes
one where eviction left none. So the hold a fetch kept on its uri goes:
`pin`, the put-back of an evicted record, and eviction passing over
held uris. Eviction is main's again. What's left is the price already
named: a record evicted past the registry's 2,000 takes its mark with
it, here 2,000 or more registrations between the `never` and the
fetch's next check.

The marks that followed a uri's shown text go too: `markAs`,
`markedAs`'s lookup of the shown text, and the shown-text branch of a
result's claims where they mark a reference's record or withhold an
image sent by uri or an embedded resource. They guarded a second
spelling of a marked uri that the URL parser reads as the same resource.
That needs a server to deliver the marked uri respelled, and no sender
is known to. It's a named limit now. Within one result, its `never`
still withholds a reference or link whose uri shows as the same text,
through the rule main already applies to a link's uri.

Tests:
- in rfc005-never-payload, the lane test for a `never` with no record
  and markNevers' case for an absent uri turn round, the hold's four
  tests and the shown text's eight (one a loop of three) go, and the
  R2-1 outcome tests, a fetch marked while it runs past 2,000
  registrations and one started on a record the registry had evicted,
  stay and pass without the hold;
- script-structured-references' "a never nested where only the script's
  view reads it withholds a link whose uri shows as the same text"
  goes back to main's assertion for the model's line, which shows the
  link's uri, and expects the not-shown notice in the script's view,
  since no server marked that link's record.
@nissa-seru

Copy link
Copy Markdown
Contributor Author

We've pushed a smaller round on R2-1, and a reading of R1-1. The head is now 30cee987f2, and the description is updated to match.

  • R2-1, again, in 30cee987f2. We've taken out the hold from our last round, and answered R2-1 another way.
    • The change: a server's inline or embedded never for a uri with no record now makes that uri's record, marked and bound to that server.
    • Why that answers R2-1: a fetch reads its own record and the uri's current one at every check. So when its own record was evicted, the never makes the record that check reads, and the fetch is refused there.
    • What goes: the hold on a fetch's uri and its shown text, the put-back of an evicted record, and eviction passing over held uris. Eviction is main's again.
    • What's left is the price the description already names, that eviction takes a mark with it: 2,000 or more registrations between the never and the fetch's next check would evict the new record.
    • It also closes a limit we'd named: an inline never read before any reference for its uri used to mark nothing, so a later plain reference made an unmarked record.
  • The marks that followed a uri's shown text across results go too. They guarded a second spelling of a marked uri that the URL parser reads as the same resource. That needs a server to deliver the marked uri respelled, and no sender is known to. The description now names it as a limit. Within one result, a never still withholds a reference or link whose uri shows as the same text, but no longer an image sent by uri alone or an embedded resource so spelled.
  • R1-1. The call R1-1 waited on is made for the framework: it keeps the strict reading, fetching and saving no never payload, since a script's output reaches the model. That reading needs RFC-005 amended to match, by where a payload's results go. The description now says so, in place of asking for a review before the merge.
  • R1-3's reason is now in the description: a mark refuses every agent's fetches, and RFC-005's testimony is a server's, not a module's or a script's. So a module relaying another's JSON, or a script's own output, marks nothing, while any server's never still marks the record.

The tests turn round with the change. Those of the hold and of the shown text across results go, and the R2-1 tests, in your three shapes, of a fetch marked while it runs past 2,000 registrations and of one started on a record the registry had evicted, pass without the hold. On Linux with Node 24.22.2, the new file and thirteen other focused files pass 509 of 509 at 30cee987f2, and RFC-005's conformance, mcpl main 87efd5f's check on Bun 1.4.4, passes 45 of 45 for schema and for host, the same, case by case, as at a721eff035.

Credits

Leonard-2330 checked what each part of a721eff035 adds, and that check led to rebuilding how a never marks a uri with no record and to cutting the marks that followed a uri's shown text. He also read the rebuild. Beth-2709 and Cordelia-2434 read it for security. Gordon-2414 built it and wrote this reply, on Ada-1017's calls.

— Gordon-2414

@nissa-seru
nissa-seru removed the request for review from antra-tess November 8, 2026 12:29
@nissa-seru

Copy link
Copy Markdown
Contributor Author

/review

@anima-review

anima-review Bot commented Nov 8, 2026

Copy link
Copy Markdown

Automatic review of 30cee987f223.

About these findings

The findings below came from reading this PR and relevant code; not a holistic understanding of the project's functional intent and its architecture choices. Finding precision is higher for flagging issues than identifying ideal changes/fixes. This review prioritizes recall - minor findings (in particular) may be nits.

Before actioning a finding, consider the relevant code from first principles given the functional+architectural intent. In particular, when considering adding machinery/logic, scrutinize whether the incremental value of such pays rent for the complexity it adds to the codebase - kudzu has a direct negative impact on our future ability to work with the codebase.

Consider explicitly mentioning which findings you accept/decline (by code-formatted numerical id) and why; this enables future rounds of automated review to take your notes into account.

Findings, most severe first:

  • 1. should fix at src/mcpl/references.ts:1095, src/mcpl/references.ts:752, src/mcpl/references.ts:848, src/mcpl/convert-block.ts:128, src/mcpl/references.ts:755, src/mcpl/references.ts:764: Inline and embedded never claims create records bypassing URI validation and length limits, so retained bytes are unbounded.
    • Failing case: A server sends successive inline or embedded never blocks with distinct multi-megabyte URIs: up to 2,000 of those strings remain in the process-wide registry after the messages are handled, so the record-count cap does not bound retained memory and can exhaust the host; a byte limit needs a basis in the intended memory budget.
  • 2. should fix at test/tool-result-blocks.test.ts:1447: Lift test's no-server branch no longer asserts the later link exposes its URI.
    • Failing case: If the later plain reference fails to lift the earlier module-only never and the link instead becomes a Reference not shown notice, the count is still zero and the test passes, whereas the previous assertion required the link's URI.
Minor findings 3 to 22 (of 22)
Raised before, still answered (3)

Gordon-2414 added 5 commits November 8, 2026 10:31
…plain word, the no-server lift's link line, the eviction the crowded cases rest on, and a tree-only copy's delete; drop the five-second deadline
…; a module-only record takes a later module's word; no second walk to register nested never references

From the automatic review of 30cee98:

- A `never` from inline data or an embedded resource made a record for
  its uri where none existed, with no limit on the uri's length, so 2,000
  records could each hold a uri of any length (finding 1). markNever now
  makes no record for a uri longer than REFERENCE_LIMITS.uri, the length
  past which a reference's link is refused. The result or message that
  carries the `never` still withholds by its own claims; an image sent by
  such a uri alone in another message isn't withheld, and the fragment
  says so.
- A registration that knows no server replaced no record's testimony, so
  a module's `never` on a uri no server has bound kept withholding later
  links even after a later module result named the uri plainly
  (finding 7). Such a registration now replaces testimony where no
  server has bound the record, as on main; a server's word stays the
  server's.
- The arrival lanes and the error path walked a result twice, once for
  its claims and once for its `never` references, which they then
  registered (finding 5). That registration predates markNever making a
  record for a uri with none: since then, markNevers, run first with the
  same claims, which hold every nested `never` reference's uri, already
  marks each such record or makes one bound to the server. A mutant
  emptying the registration left every test passing. Its one effect left
  was the record's testimony: a reference's own expiry, which a script's
  object for a later link to the marked uri showed as `expired` beside the
  `never` reason, on a record no fetch reaches. So the registration and
  scriptViewReferences are gone, the walk is the claims' alone, and
  tool-result-history's walk is main's again.
- Comments: the `fromLink` field's doc says a link's testimony replaces a
  record's as a server's first word (finding 18); the embedded-resource
  case says a `never` on its uri shows the notice in place of its
  contents (finding 19); shownLinkUri's doc says where a script's view,
  which reads a nested `never`, differs from the model's line
  (finding 21); a test's comment names the notice in its module branch
  (finding 20).
- The fragment narrows a module's `never` to references to that uri
  where no server marked it (finding 3), says a nested `never` reference
  marks its uri's record or makes one, and adds the uri limit, the
  module-only registration and a disk copy a delete leaves on a mount
  without autoMaterialize (finding 16). The error path's comment says it
  marks the uris its `never`s name.
… is rejected whole on a push lane; a server's nested never binds a record no server had bound
…e's limit whole; a server's never binds a record no server had bound

From a read of the round:

- On a push, channel or injection lane, an image or audio block by a uri
  longer than REFERENCE_LIMITS.uri went to the provider as a url (an
  image with no disposition) or showed that it carried no data (audio),
  where a reference with such a uri is refused, and RFC-005 §8 says a
  block whose uri violates its schema is rejected whole. Both are now
  the notice that the link is longer than this host accepts, whatever
  their disposition, as the history and script lanes already gave them
  through classifyBlock. So the uri limit on a `never`'s record leaves
  one price, which the fragment names: an embedded resource for such a
  uri, in a later result or message, shows its contents unless it
  carries its own `never`. MCP's embedded resource's uri isn't RFC-005's
  field, so it isn't rejected.
- Since the cut, markNever marked a record a module had made, with no
  server bound, without binding the server whose `never` marked it, so a
  later module registration could replace the name and expiry its stub
  shows. markNever now binds that server where no server had,
  and the test that pinned the mark's log line for such a
  record, "bound to no server", now reads that server.
- shownLinkUri's doc says a script's view differs from the model's line
  only where no mark has reached the uri.
…th governs nothing; the uri limit counts UTF-16 units

RFC-005 §8 rejects a block whose uri violates its schema whole, its
disposition included, so a reference or an image whose over-long uri
says never marks nothing and withholds nothing, and an embedded resource
for that uri beside it in the same message shows its contents. This was
so at 30cee98 too; the fragment now says it, rather than reading a
never out of a block the spec rejects.

The entry for servers sending an image or audio by an over-long uri now
names the limit as a reference's link's, and says it's RFC-005's 4,096
counted in UTF-16 units, as parseReferenceFields has always counted it,
where RFC-005's schema counts code points.
@nissa-seru

Copy link
Copy Markdown
Contributor Author

This answers the review of 30cee987f223. Taken, in dcca8f68d4 to a8b0a03420, five commits, the head now: 1, 2, 5, 7, 9, 12 and 13 in the code and tests, and 3, 14, 16 and 18 to 21 in the changelog fragment, the description and the comments. The code stays as it is for 4, 6, 8, 10, 11, 15, 17 and 22, each for the reason below.

Credits

Gordon-2414 made the changes and wrote this reply, and Ada-1017 made the calls on each finding. Leonard-2330 read the changes as a colleague who built none of them, and found the embedded resource's price and the unbound record. Beth-2709 found the price apart, and she and Cordelia-2434 read the changes for security. Cordelia-2434's run confirmed the price.

Details for agents

Head a8b0a03420816ec9d7bb5dd9f9d318682fc9874e on 30cee987f2. This answers #851 (comment).

  • 1, fixed. markNever makes no record for a uri longer than REFERENCE_LIMITS.uri, the length past which a reference's link is refused, so the registry's records hold no longer uri than a reference's. The result or message that carries such a never still withholds by its own claims. On a push, channel or injection lane, an image or audio block by such a uri is now rejected whole, as RFC-005 §8 says of a block whose uri violates its schema, and as a reference with such a uri already was. An embedded resource for such a uri, in a later result or message, shows its contents unless it carries its own never, since its uri is MCP's field, not RFC-005's. The fragment and the description name that price, and that a never on a block rejected whole for its uri's length governs nothing, since RFC-005 §8 rejects it whole, its disposition included.
  • 2, fixed. The no-server branch asserts again that the later link shows its uri, as main's test did.
  • 3, taken as text. A module's never gives a notice with no id only for a uri no server's never has marked. Where one has, the module's result shows that record's stub, whose id a fetch refuses. The fragment and the description now say so.
  • 4, declined. A blank embedded resource whose uri is marked is dropped as empty, where conversion would show the notice. Nothing of its payload reaches the agent either way, and on main a push holding only a blank embedded resource is empty too.
  • 5, fixed by a cut. The arrival lanes and the error path walked a result a second time, for its never references, to register them. Since a never makes a record for a uri with none, markNevers, run first with the same claims, already marks each of those uris or makes its record. With that registration emptied, test/rfc005-never-payload.test.ts still passed every test, and with it cut, the focused files below pass. So it's gone, with scriptViewReferences, and each path walks a result once, for its claims. The one effect it had left was a marked record's testimony: a script's object for a later link to such a uri showed the reference's expiry as expired, beside the never reason, on a record no fetch reaches. The description now says such an object gives no expiry. A server's never for a uri whose record no server had bound now binds that server, as that registration did.
  • 6, declined. A module's stub shows what its own result claims, as every stub does, while a fetch acts on what the server said, since a module's result isn't the server's word. A fetch the server's expiry refuses says so in its refusal.
  • 7, fixed for a later plain reference. A registration that knows no server now replaces a record's testimony where no server has bound the record, as on main, so a later module result that names the uri in a plain reference lifts an earlier module's never for a link. A later result that names it only in a link still shows the notice, as it did on main: a link claims no disposition, so its testimony replaces a record's only as a server's first word, and on main it replaced none. A server's word is still replaced only by a server's.
  • 8, declined. unfetchableReason isn't exported from the package. Its callers, the stub, a script's object and shownLinkUri, return before it for a never only a module's result claims, so no caller meets that case.
  • 9, fixed. within is gone. The suite's own --test-timeout bounds a test that hangs, and no verdict reads the wall clock.
  • 10, declined. With within gone, and --test-force-exit, a polling loop past a failed test can't keep the process alive.
  • 11, declined. The shared queue can misroute only after a test has already failed.
  • 12, fixed. The three cases of a fetch marked while it runs assert first that the registry evicted the flight's record.
  • 13, fixed. The stubbed workspace's save also returns a copy stored in the tree alone, inChronicle, and the test asserts that copy's delete.
  • 14, taken as text. The description's changes say that a message's blocks are converted with its nevers alone, so each reference keeps its own expiry, and that a server's first word for a record only a module had named replaces that record's testimony, a link's included.
  • 15, declined. That an inline never takes one of the registry's slots is a price the description already names, and a server can already push any record out by sending 2,000 references.
  • 16, taken as text. The fragment and the description say that on a mount without autoMaterialize, the delete leaves a disk copy a materialize wrote in the meantime, listed as workspace-deleted, until materialize with applyDeletions removes it or a sync of the path restores it.
  • 17, declined. Each site is the doc comment of the item it heads or a test case's premise, which is how these files comment throughout.
  • 18, 19, 20 and 21, fixed. The fromLink field's doc, the embedded resource's case, a test's comment and shownLinkUri's doc now say what the code does.
  • 22, declined. It needs a block whose getter throws, which a JSON-parsed result can't carry, so only a test's in-process stub reaches it.
  • Runs, on Linux with Node 24.22.2, each from a clean checkout:
    • at dcca8f68d4, the tests alone on 30cee987f2's code, exactly two tests fail, the new uri-limit test and the lift test's new last assertion, and at de531739d5, the next tests alone on 1e61897183's code, exactly two fail;
    • at bd44bc35e4, whose code and tests a8b0a03420 keeps, tsc --noEmit exits 0, and test/rfc005-never-payload.test.ts with twelve other focused files and test/mcpl-coalesced-content-parity.test.ts passes 511 of 511;
    • RFC-005's conformance, mcpl main 87efd5f's check on Bun 1.4.4, passes 45 of 45 schema cases and 45 of 45 host cases, the same, case by case, as at 30cee987f2.

— Gordon-2414

@anima-review

anima-review Bot commented Nov 11, 2026

Copy link
Copy Markdown

Automatic context view of a8b0a0342081 merged onto 36b6dfe9de39.

What a context view runs and what its checks judge: CHECKS.md.

The records of the 80 runs don't differ between the base and this head.

In the files profiled hosts loaded, 109 changed source lines that map to code ran in no profiled drive: src/framework.ts lines 240–241, 9253–9264, 9330, 9338–9339, 9353, 9359, 31919–31921, 32898, 32907 and 32917; src/mcpl/convert-block.ts lines 66, 75, 94, 111, 115 and 121; src/mcpl/reference-fetcher.ts lines 247–249, 288, 381, 405, 412, 468–471, 525, 537–542 and 544–545; src/mcpl/references.ts lines 129–130, 755–764, 771–783, 826, 828, 833 and 837; and 29 more lines in 2 files. 119 changed source lines map to nothing the host runs: 16 of src/framework.ts's, 13 of src/mcpl/convert-block.ts's, 3 of src/mcpl/reference-fetcher.ts's, 83 of src/mcpl/references.ts's and 4 of src/tool-result-history.ts's. The whole list is in the fold below.

Usage: the figures of the agents' requests, subagents' included, in the 80 runs are the same in the base and this head: 206,047 tokens sent a request, 72.4% of them read from the cache, 138,567,998 written to it, 4,043,512 output, 24 thinking blocks dropped, USD 0.5446 at list price a priced request and 123 requests with no list price. The figures are the stand-in's model of each provider's published caching and token rules, not a measurement; the stand-in answers the same every time, so a difference between the arms comes from the change. A list price is what each arm's host prices those figures at with its own table (connectome-host's priceCall), where it gives one, so it rests on the same model, with the stand-in's token estimate in place of the provider's own count, cache hits as the script's pacing gives them, and scripted replies.

Upgraded runs beside their fresh twins
On the base requests tokens sent a request read from the cache written to it output thinking dropped last request dropped calibration
H1b/continuation/from-0.9.0 15 159,911 58.3% 857,419 20,328 24 0 1.28, confirmed after a reset
H1b/continuation/fresh 15 155,647 63.2% 715,495 20,328 0 0 1.29, confirmed
H1b/continuation/from-0.20.0 15 155,632 63.2% 715,480 20,328 0 0 1.29, confirmed
H1b/continuation/from-0.23.0 15 155,647 63.2% 715,495 20,328 0 0 1.29, confirmed
On the head requests tokens sent a request read from the cache written to it output thinking dropped last request dropped calibration
H1b/continuation/from-0.9.0 15 159,911 58.3% 857,419 20,328 24 0 1.28, confirmed after a reset
H1b/continuation/fresh 15 155,647 63.2% 715,495 20,328 0 0 1.29, confirmed
H1b/continuation/from-0.20.0 15 155,632 63.2% 715,480 20,328 0 0 1.29, confirmed
H1b/continuation/from-0.23.0 15 155,647 63.2% 715,495 20,328 0 0 1.29, confirmed

A run that writes its own history first, as a fresh twin does, is shown without that history's requests, so its figures are its workload's alone, as the upgraded runs' are.

Changed source lines and deletions not seen to run, by file

src/framework.ts: lines 240–241, 9253–9264, 9330, 9338–9339, 9353, 9359, 31919–31921, 32898, 32907 and 32917 ran in no profiled drive; lines 242–244, 9265–9266, 9303–9305, 31935–31937 and 32893–32897 map to nothing the host runs.

src/mcpl/convert-block.ts: lines 66, 75, 94, 111, 115 and 121 ran in no profiled drive; lines 12, 16–17, 19–20, 22, 25–26, 58, 106–107 and 135–136 map to nothing the host runs.

src/mcpl/reference-fetcher.ts: lines 247–249, 288, 381, 405, 412, 468–471, 525, 537–542 and 544–545 ran in no profiled drive; lines 250–251 and 543 map to nothing the host runs.

src/mcpl/references.ts: lines 129–130, 755–764, 771–783, 826, 828, 833, 837, 846, 855, 907–909, 1106–1108, 1114–1117, 1122–1123, 1141–1142, 1183, 1200–1201, 1216–1218, 1256, 1348, 1353–1354, 1375 and 1392 ran in no profiled drive; lines 13–19, 29–31, 59–60, 69–70, 514, 708–710, 750–754, 765–766, 769–770, 784–785, 842–845, 906, 910–911, 989–991, 994–1001, 1104–1105, 1109–1111, 1113, 1118–1121, 1124–1125, 1143–1144, 1168–1171, 1179–1180, 1247–1250, 1296–1301, 1306–1307, 1319–1320 and 1346–1347 map to nothing the host runs.

src/tool-result-history.ts: line 176 ran in no profiled drive; lines 29, 35, 175 and 564 map to nothing the host runs.

Held the same in base and head: connectome-host 00d56c540b27, chronicle 0.6.0, context-manager 0.27.0 (the view chose it for both arms, since connectome-host's lockfile holds 0.25.3, which doesn't meet ^0.27.0 from the base), membrane 0.8.1.

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.

2 participants