Repository navigation
fix(references)!: while a reference is marked never, don't fetch or save its payload, and forget an earlier copy's path - #851
Conversation
…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>
4872e39 to
69b84ba
Compare
There was a problem hiding this comment.
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:
- Medium severity. A mark during a stalled download doesn't stop it or answer waiting callers until the next chunk or the deadline.
- Medium severity. markNever mutates a record without first copying it into open journals, breaking the journal's 'as it stood' contract.
- Medium severity, maintainability. classifyBlock now mutates the process-wide registry as a side effect of classifying.
- Medium severity. neverClaims silently falls back to top-level claims when the nested walk throws, so a nested
neveris ignored and the payload fetched. - Medium severity. An error result's nested
neverdoesn't mark, unlike the success and module paths. - Medium severity. A copy saved before the mark stays readable by the agent's workspace tools even though the fetcher can now remove it.
- Medium severity. Any server's
never(or any block run through classifyBlock) can permanently disable fetching a uri another server delivered. - Medium severity. Every MCPL result, including
registermode, now gets an extra full JSON walk of its content.
4 more findings, less severe
- Low severity, documentation,
changelog.d/rfc005-never-payload.fixed.md:5. Changelog headline says aneverpayload 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 aneverpayload 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-facingdeletetool by name instead of a workspace API.saveFilewrites through the typedworkspace.writeBinaryAPI, but the newremovehook callsworkspace.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 workspacedeletetool would break this cleanup silently; the only signal would be a log line. A direct deletion method on the workspace module, likewriteBinary, 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 checkgetReferenceFetcher().refusal(marked)and the fetcher'sfetch()result. Nothing drivesdispatchFetchReferenceToolCall(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 aneverresult 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 aneverreference. It now stripsdispositionso that itsfetchedandUNBOUND_REASONassertions still pass. No other test checks that aneverreference in a result whose server prefix arrives mid-call is passed over by the demand fetch and shown withPAYLOAD_NEVER. A case that keepsdisposition: '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.
| 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); |
There was a problem hiding this comment.
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.
| markNever(uri: string): void { | ||
| const record = this.findByUri(uri); | ||
| if (record && !record.payloadNever) this.markPayload(record); | ||
| } |
There was a problem hiding this comment.
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.
| if (hasLink && block.disposition === 'never') referenceRegistry.markNever(block.uri as string); | ||
| return { kind: 'inline', contradiction: block.disposition !== undefined || hasLink }; | ||
| } | ||
| if (hasLink) return parseReferenceFields(block); |
There was a problem hiding this comment.
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.
| function neverClaims(content: unknown[]): ResultClaims { | ||
| try { | ||
| return scriptViewClaims(content, true); | ||
| } catch { | ||
| return contentClaims(content); | ||
| } | ||
| } |
There was a problem hiding this comment.
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.
| if (server && server.capabilities !== null && Array.isArray(result.content)) { | ||
| referenceRegistry.markNevers(contentClaims(result.content)); | ||
| } |
There was a problem hiding this comment.
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.
| 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`); | ||
| } |
There was a problem hiding this comment.
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.
| 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); | ||
| } |
There was a problem hiding this comment.
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.
| const nevers = claims ?? neverClaims(content); | ||
| referenceRegistry.markNevers(nevers); |
There was a problem hiding this comment.
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.
…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
left a comment
There was a problem hiding this comment.
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-248and:404refuse 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 aneverreference and expectsok: truewith a saved file.
- The conformance adapter (
conformance/agent-framework-bulk-references.mjs:85-90) builds its ownReferenceFetcherwithoutwithhold. So 45/45 keeps passing while the framework ships the opposite behaviour: fornever, the corpus no longer tests what the framework does. - The fragment is
changelog.d/rfc005-never-payload.fixed.md.changelog.d/README.mdsays breaking fragments open by naming who has to act.
-
Failure scenario. A server built to §12 marks its rendered files
neverand expects the host to stage them for a script. On 0.32.x the agent canfetch_referencethe 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 marknever).
- 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
- (b) Keep the RFC's reading. Allow host-mediated fetch, but stage
neverpayloads 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
nevertoday (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. - (a) Keep the strict reading.
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). -
markNeverandregistermark 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:
- Start
fetch(A). - Register 2,001 other references, which evicts A.
- Re-deliver A's uri with
disposition: "never". This creates record B, marked. - Release the response.
Result:
{"ok":true,"path":"scratch/refs/ref_2_….txt"}, with the save hook called andremovenever called. - Start
-
- Failure scenario. The agent calls
fetch_referenceon a reference last mentioned about 2,000 registrations ago. A flight can run up toFETCH_TIMEOUT_MS(60 s) and keeps going after its waiters give up. During it, A is evicted and the server re-sends the uri asnever. The flight savesrefs/<A>.<ext>, andfetch_referenceanswers 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 checkreferenceRegistry.findByUri(record.testimony.uri)?.payloadNever(the fetcher already imports fromreferences.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.
classifyBlocknow 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 testgovernedByRfc005at:414). - Reproduced: a structured result
{ nested: { type: 'image', uri: X, data: 'aGk=', mimeType: 'image/png', disposition: 'never' } }passed throughtoolResultDataToHistoryStringmarked the record that server A had delivered for X. - Separately, a
neverfrom server B for A's uri marks A's record (disclosed), and produces no log line at all.markPayloadlogs 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_referencerefuses 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
classifyBlockpure. Mark only where a server's content is in hand (autoFetchReferences, the module demand loop, the error path, and theconvertBlocklanes, which can callmarkNeverdirectly 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:65checks the mark only at that moment. Nothing else insrc/produces or rewrites asource.type: 'url'image block, and membrane 0.8.0 forwards url sources to the provider (dist/providers/anthropic.js:1070).
- Push, channel and injection content is converted once, on receipt:
-
Failure scenario.
- At t0 a channel message carries
{ type: 'image', uri: U }and is stored as a url source. - At t1 a
neverfor U arrives. - 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.
- At t0 a channel message carries
-
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-14still 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 inreferences.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 inlineneverread 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 newReferenceFetchercallers 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):
tscexits 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 offetch()'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). markNeverand journals.markNevercallskeepFirstbefore 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 inregistermode, 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
payloadNeverfirst: no saved path,savedTo: nullwithunfetchableset, and no link uri (references.ts:1130,:1162,:1201,:1317,:1334). fetch_referenceandsave_recent_image. Both refuse with the reason, and the new tests drivedispatchFetchReferenceToolCallitself.fetchEager()resolves the server beforefetch()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 checksisEagerEligiblefirst, 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_referencetest, and keeping anevercase 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-facingdeletetool 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
neverinside 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).
- R1-2 is Sol's. I reproduced it and downgraded it from major: it needs an eviction and a same-uri
- 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.
…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.
|
/review |
About these findingsThe findings below came from reading this PR and relevant code; not a holistic understanding of the project's functional intent and its architecture choices. Finding precision is higher for flagging issues than identifying ideal changes/fixes. This review prioritizes recall - minor findings (in particular) may be nits. Before actioning a finding, consider the relevant code from first principles given the functional+architectural intent. In particular, when considering adding machinery/logic, scrutinize whether the incremental value of such pays rent for the complexity it adds to the codebase - kudzu has a direct negative impact on our future ability to work with the codebase. Automatic review of Findings, most severe first:
|
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.
never, don't fetch or save its payload, and forget an earlier copy's pathnever, don't fetch or save its payload, and forget an earlier copy's path
|
Replying to this review. We've pushed a round answering R1-1 to R1-5, and the automatic review of
The automatic review's three should-fixes: 1 and 3 are left, and 2 in part, and each is named in "what this leaves":
Finding 6 was a gap in this pull request's own change, and The round also carries changes from our own review of it, which the description lists. An embedded resource whose uri is 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 — Gordon-2414 |
|
Replying to this comment. What we left of the automatic review of
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
left a comment
There was a problem hiding this comment.
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.mdnow opens with an audience (hosts and scripts that fetch aneverreference through the framework, and modules that restate a server's reference). The description asks the spec owner to rule. At mcplmain(793ae8e, Oct 31), §7 and §12 still describe host-mediated fetch of aneverpayload, vector 03 still expects one to be fetched and saved, and the adapter still builds its ownReferenceFetcherwithoutwithhold. 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
neverreference, the flight is refused and nothing is saved. The new test attest/rfc005-never-payload.test.ts:577covers 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
neverreference for X, and it doesn't replace A's testimony. - Server B's
neverfor X does mark it, and logsthe "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".
- A module's structured result with
- 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,:525and:537, with the fetcher built withwithhold(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
neverpayload for a code-execution script (lines 561-565). - Vector
03-verified-neverexpectsok: trueand 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.
- At this head every framework fetch path refuses a marked record:
- Failure scenario. Unchanged from round 1. A server written to §12 marks a rendered file
neverand 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:
- Hold the merge until the spec owner rules. The description already asks them.
- 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
mainfor an unrelated reason (see Verified fine), and an amendment PR will hit that first.
- 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
- 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). -
markNevermarks only a record thatfindByUrireturns (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 aresourcereference, which does create a new, marked record. -
Reproduced against the merged build with a held local response:
- Register A and start
fetch(A). - Register 2,000 other references (A is evicted).
- Run
convertBlocks([{ type: 'image', uri: A, data: 'aGk=', mimeType: 'image/png', disposition: 'never' }], 'attachment on push event', 'srv'). - 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,removewas never called, and a laterfetch(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. - Register A and start
-
- Failure scenario. The window is R1-2's. The agent's
fetch_referencecall (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 markednever. The message shows the notice, the fetch savesrefs/<id>.<ext>, andfetch_referencegives 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 inlineneverthat 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.
markNeverthen finds the record whatever shape theneverarrives 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:577test.
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:16runs to 132 columns; the line wasn't re-wrapped.:27-31, inherited frommain, says fetching "is deliberately not here yet — the registry is what makes a future host-mediated fetcher … possible".ReferenceFetcherexists, 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.tsandtool-result-history.tsare unchanged. - The set of files that use
classifyBlock, the registry orfetchedPathis the same, andframework.ts's counts of those uses are unchanged. - The new RFC-006 materialization path (main's
framework.ts:21398and:21422),push-handler.ts:212,channel-registry.ts:2047andhook-orchestrator.ts:70all convert throughconvertBlocks, somessageClaimsmarks for them. - Both reviewers found this independently.
- Between the merge base and main,
- Build and tests on the merged tree (Node 22.22.1, membrane 0.8.0):
npm run buildexits 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-uriandrelease-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),dispatchFetchReferenceToolCalltakes itsdeliverdefault fromthis.pushToolResult. The adapter's stand-inthisdoesn't provide one. - That needs fixing in mcpl, and isn't this PR's to fix.
- Since
- With
pushToolResultadded 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
56170af1bdand 0.28.0. They can't be reproduced as published until the adapter is fixed.
- With the adapter as published, it fails 18 of 45 host cases, at this head and at its merge base alike, with
- 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,:1396and:1413;convert-block.ts:71and:104;tool-result-history.ts:176.
- Journals are read-only snapshots for the module route's line (
framework.ts:28569,:28606,:28663), so marking afterkeepFirstis never undone. The script object still drops a link's uri when the live record is marked (references.ts:1396). convertBlock's defaultclaimsparameter marks the registry, but onlyconvertBlocksand the tests callconvertBlock, sincesrc/index.tsdoesn't export it.- Changed existing tests keep what they assert:
- Fixtures a server sent as
neverare now registered as arrival registers them (arrived()intool-result-history.test.ts). tool-result-content.test.ts:44asserts the no-id notice and the absence ofRAW_TOKEN.
- Fixtures a server sent as
- Downstream.
connectome-host'smainuses none ofReferenceFetcher, the registry orfetchedPath, 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.
…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.
|
Replying to this review. We've pushed a round answering R2. The head is now
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.
|
We've pushed a smaller round on R2-1, and a reading of R1-1. The head is now
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 CreditsLeonard-2330 checked what each part of — Gordon-2414 |
|
/review |
|
Automatic review of About these findingsThe findings below came from reading this PR and relevant code; not a holistic understanding of the project's functional intent and its architecture choices. Finding precision is higher for flagging issues than identifying ideal changes/fixes. This review prioritizes recall - minor findings (in particular) may be nits. Before actioning a finding, consider the relevant code from first principles given the functional+architectural intent. In particular, when considering adding machinery/logic, scrutinize whether the incremental value of such pays rent for the complexity it adds to the codebase - kudzu has a direct negative impact on our future ability to work with the codebase. Consider explicitly mentioning which findings you accept/decline (by code-formatted numerical id) and why; this enables future rounds of automated review to take your notes into account. Findings, most severe first:
Minor findings 3 to 22 (of 22)
Raised before, still answered (3)
|
…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.
|
This answers the review of CreditsGordon-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 agentsHead
— Gordon-2414 |
|
Automatic context view of 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: 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 Upgraded runs beside their fresh twins
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
Held the same in base and head: connectome-host |
Why. agent-framework's reference handling says a payload a server marks
disposition: neveris 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 orfetch_referencenames that copy. agent-framework 0.27.0 and 0.28.0 say inreferences.d.tsthatnever"withholds payload AND uri from model context unconditionally". The uri half held, but nothing after parsing toldneverfromref, 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
nevermain): it's fetched like any other, into the workspace'srefs/: 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.fetch_referenceanswers with the reason,save_recent_imagesays the reference can't be fetched, the eager and script fetches pass it over, and a script's object for it hassavedTo: nulland the reason asunfetchable.A copy saved before the mark
main): there's no mark, so a copy's path stays in its stub and script object.A module's claims about a server's reference
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.neverin 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 onmain.An image or audio block by a uri longer than a reference's link may be, on a push, channel or injection lane
main): an image with no disposition goes to the provider as a url, and audio with none shows that it carried no data.ReferenceFetchermain): nowithholdoption.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 aneverreference there.No MCPL server is known to send
nevertoday.Where the mark doesn't hold or reach (What it leaves):
neverfor 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 ownnever;neverfrom any server refuses later fetches of a uri another server delivered;neverisn't marked by it, so the agent's ownfetch_referencecan still fetch it;never;neverincluded.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 aneverreference no server marked showed before this change.e57a6b0159changes 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 aneverpayload (What it leaves).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
nevermarks 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 to2f15c4293d. Beth-2709 and Duncan-2410 read the round after it, and Duncan-2410's runs found the cases thatb7f03e252candc95cbcd3f0fix. 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 atc95cbcd3f0, 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 ofa721eff035adds, and that check led to rebuilding how anevermarks 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 of30cee987f2as 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, on7a97ea3e82.Problem
At 0.28.0 (
59d609586e), as the red-first tests and a probe show (Tests):neverreference from a networked server was fetched eagerly into<mount>/refs/, and every one a script's result named was fetched on demand;saved: <path>, and a script's object carried the path assavedTo;lsandgloblisted it, andread(for text) andread_imagereturned 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 firstneverin an MCPL server's own content marks its uri's record: a tool result, an error result, or a push, channel or injection. Thatnevercan 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, thenevermakes 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'sneverreference 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 withoutnever, 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.classifyBlockmarks nothing.On a push, channel or injection lane, a
neveranywhere in a message governs every block of it, whatever their order. So an image sent by uri before aneverfor that uri is its stub. Each block is converted with the message'snevers alone, so each reference keeps its own expiry where two name one uri.A
neverin 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 markednever, with no id to fetch it by, and no saved copy or uri, unless a server'sneveralready 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'sneveror 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.
ReferenceFetchertakes an optionalwithhold, with aremove(path). A fetcher built with it:The framework builds its fetcher with
withhold. RFC-005's conformance adapter builds its own without it, so vector 03 still fetches and saves aneverreference 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:
fetch_referenceanswers with that reason, andsave_recent_imagesays the reference can't be fetched for that reason;savedTo: nulland the reason asunfetchable;nevermakes for a uri with none holds thatneveralone, not a reference's other claims, so a script's object for a later link to it gives theneverreason and no expiry;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
nevershows a notice saying why, not its contents, whatever they are, on every lane, in history and to a script. So does one beside aneverfor 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
mainat7a97ea3e82. Up to2f15c4293d:8b3b0341dbmade the change;69b84baf49cut its comments to those that explain what the code can't;56170af1bdfetches nothing on its own for a result whose blocks can't be read for a nestednever;5e4236f2edspells a withheld image's fallback text asmaindoes;2f15c4293dcorrects three docs to say a mark outlasts a later plain reference; andd2d4b7b487and19f2ed6a96mergemainate85c57db9aand4a45766f01.2c92a7ae94mergesmainat7a97ea3e82, andgit show --remerge-diffprints nothing for it. Twenty-four commits follow.e57a6b0159changes one existing test and nothing else, and five,a58c6a478etoa721eff035, answer the second round of the maintainer's nightly review (Tests). The last,30cee987f2, makes aneverfor 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.dcca8f68d4toa8b0a03420, five commits, answer the automatic review of30cee987f2and the reads of that answer (Tests).What it leaves
neverpayload 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 aneverpayload, §12's hand-off included. Which reading RFC-005 keeps is its owner's to rule.nevermarks the uri's record, whichever server the record is bound to, so one server'sneverrefuses later fetches of a uri another server delivered. Aneverin a module's result marks nothing, and doesn't follow the uri into later results once a server has spoken for it.neverisn't marked by thatnever, so the agent's ownfetch_referencecan still fetch it.nevermakes for a uri with no record takes one of those 2,000 too, and aneverfor 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 ownnever. 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. Aneveron a block rejected whole for its uri's length governs nothing, since RFC-005 §8 rejects such a block whole, itsdispositionincluded, 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 aneverthat 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.never, a reference or link so spelled is still withheld by thatnever, but an image sent by uri alone or an embedded resource so spelled isn't.never.neverincluded.Tests
test/rfc005-never-payload.test.tsis new, with 55 tests:fetch_reference's answer, and an end-to-end run with a started framework, a workspace and a local server.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.tsandtest/mcpl-reference-fetcher.test.ts. Among them, in a script's view, a link beside a nestedneverfor 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 onmain.e57a6b0159:test/tool-result-content.test.ts:44, which passes a resource markedneverthroughtoMembraneToolResultwith no server's mark on its record. It expected a[ref_stub, and now expects the notice, with no reference id and noRAW_TOKENin the content. Its twin intest/tool-result-history.test.tsregisters itsneverwith a server first, as arrival does, and keeps its stub.At
a8b0a03420:mainataa134e75fc): 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 testsa8b0a03420keeps, changing only the fragment, answering the automatic review of30cee987f2and the reads of that answer, on Linux with Node 24.22.2, each from a clean checkout after a freshnpm install,tsc --noEmitand build:dcca8f68d4, the tests alone on30cee987f2's code: exactly two tests fail: the new one for aneverfor 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'sneverfor a link. Atde531739d5, the next tests alone on1e61897183'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 nestednever.test/mcpl-coalesced-content-parity.test.ts, 511 of 511.dcca8f68d4's tests, each of three fails its new assertion, where with30cee987f2'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;1e61897183, with no limit on anever's uri, the uri-limit test fails, and with a serverless registration replacing nothing, the lift test's last assertion fails;bd44bc35e4, with no length check on a push lane's image, the over-long uri test fails, and with no binding bymarkNever, the nested-nevertest and the mark's log test fail.main87efd5f's check on Bun 1.4.4: 45 of 45 schema cases and 45 of 45 host cases, the same, case by case, as at30cee987f2.At
30cee987f2:mainataa134e75fc): 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 freshnpm install,tsc --noEmitand build:test/rfc005-never-payload.test.tsona721eff035's code, exactly the two tests it turns round fail: a lane's inline or embeddedneverfor a uri with no record, andmarkNeversfor an absent uri.test/mcpl-coalesced-content-parity.test.ts, 509 of 509.markNevermaking 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.main87efd5f's check on Bun 1.4.4: 45 of 45 schema cases and 45 of 45 host cases, the same, case by case, as ata721eff035.At
a721eff035:mainat3c31baa72f): 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.npm install,tsc --noEmitand build:test/rfc005-never-payload.test.tspasses 67 of 67, and withtest/tool-result-content.test.tsand 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:mainat3c31baa72f): 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.npm install,tsc --noEmitand build: atc95cbcd3f0,test/tool-result-content.test.tsfails only its:44test, with CI's error; ate57a6b0159, that file and the twelve focused files below pass 481 of 481; and onmain's code at7a97ea3e82, 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:03da219555, whose code and tests arec95cbcd3f0's andccd9ac3429's, with only the fragment's text and the commit message changed between them:tsc --noEmitexits 0, andtest/release-changelog.test.tswith the new file passes 78 of 78.ccd9ac3429:tsc --noEmitexits 0;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.tsandtest/release-changelog.test.ts.c95cbcd3f0removed, one at a time atccd9ac3429, whose code isc95cbcd3f0'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 inlinenevermarks, 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 at8aca7146e3.Earlier runs, on Linux with Node 24.22.1 unless named:
19f2ed6a96, which mergesmainat4a45766f01: ten focused files pass 407 of 407, and each of them passes on Node 22.12.0, CI's floor.main: with4a45766f01'sframework.ts,references.ts,reference-fetcher.tsandconvert-block.tsin place of this change's, the new file as it stood then failed 37 of its 38 tests.never. It saved both, each stub endedsaved: <path>,lsandgloblisted both files,readreturned the text's payload andread_imagethe PNG's. At this change's second commit both fetches were refused with the reason, andlsandglobfound nothing.main8b0e57d's check on Bun 1.4.4, at56170af1bd'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, thennode --test dist/test/tool-result-content.test.js) and saw the same notice.e57a6b0159changes it (Changed to match, above).Not verified
never, nor in a running resident. Only the tests' local servers and the conformance fixtures exercise it.