Repository navigation
fix(mcpl): fetch_reference's failures say what happened to the reference and what calling again can do - #531
Merged
nissa-seru merged 5 commits intoOct 18, 2026
Conversation
…nce and what calling again can do
Every failure of the reference fetcher is now two texts: a clause about
the reference ("the "srv" server answered HTTP 503", "it claims 10000
bytes, over the 1000 bytes this call allows", ...) and a sentence saying
what calling again can do: try later, wait for a fresh reference, pass a
larger max_bytes (no more than the server's budget has left), ask whoever
runs the host, or nothing. fetch_reference reads
`fetch_reference couldn't fetch "<id>": <clause>. <remedy>`.
save_recent_image gives both where a reference can't be fetched, through
anima-research#518's refusal(). The eager and script-demand log lines read the clause.
- FetchOutcome becomes a union whose failure side carries both texts, so
a failure without a remedy doesn't compile. anima-research#518's Refusal
{reason, ofReference} becomes {error, remedy}: each clause has its own
subject.
- Each refusal before dialing says what could change it: a server this
host no longer has (whoever runs it can add it back), a server with no
address (nothing), a claim past the host's own ceiling (no max_bytes or
setting lifts it).
- The scheme check reads the uri's own protocol, so a ws: or wss:
reference on its server's origin is refused before dialing. The
unreachable plaintext-from-a-secure-origin branch goes; an http:
reference from a server dialed over wss: is refused by the origin
comparison, and a test pins that.
- A server address that doesn't parse is refused as having no address.
- The budget's stop partway shows the server's running total, as the
next refusal does, since other fetches may have spent part of it.
- The save hook logs why it returns null; a save that throws is a
storage failure, logged, not the fetch's own error.
- A thrown fetch's clause takes its cause's words, and an
AggregateError's attempts' when it has none of its own.
- anima-research#518's changelog entry, not yet released, quotes the no-address
refusal's old words, so it now quotes these, and this change's own
entry doesn't repeat them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…red, and whyFetchFailed drops a branch nothing reaches The storage failure's remedy tells the agent whoever runs the host can see why in its log, so a test now pins each null the framework's save hook returns to its log line: no workspace, no writable mount, the write's error. It runs through the framework's own fetcher and fetch_reference. whyFetchFailed's fallback to an AggregateError's code had no case that reaches it (an AggregateError from net carries its attempts), so it goes: with no words and no attempts, the outer error's words stand. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ailed request's remedy holds for every cause What an internal review of this change found: - A thrown fetch's remedy now holds whatever the cause: "Trying again later may work; if it fails the same way, whoever runs this host can check its connection to the server." A cause that repeats on every try (a port fetch refuses, a name that doesn't resolve, a certificate this host doesn't trust) was told only that a later try may work. - A claim past the host's own 256 MiB is refused for that first, before the server's budget, since nothing lifts it, not even a budget whoever runs the host raises. A download past it is told the same. - A server address that doesn't parse gets a clause of its own, where it read as a server this host doesn't connect to over a network. - The origin clause names the server's address as it's configured (wss://...), not its normalized origin (https://...); the comparison still reads ws as http and wss as https. - A digest that doesn't match tells the agent that fetching may work once a fresh reference arrives, which refreshes this very record, where it said the reference can't be fetched here. - The save-throw log line bounds the cause, as the agent's text does. - Comments: noAddressReason's and beforeDial's say what each covers, and the ws: scheme check says fetch refuses such a uri before connecting. Tests: the refusal-order test puts a claim past the host's ceiling before a spent budget; the request-failure tests, the origin clauses and the digest remedy take the new words; the save-throw test's cause carries a newline; the two ceiling-remedy tests pin the reached count between max_bytes and the bytes sent before the pause. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d before dialing A reference uri such as http://user:secret@host/x passed the origin check, since an origin drops the userinfo, and reached fetch, which refuses such a url before connecting with a message that holds the whole uri. That message became the clause fetch_reference gave the agent, and the eager pass's log line, so the password reached the agent's context and the host's log, with a remedy inviting a retry that can't work. Such a uri is now refused in the origin check, before the scheme check, with a clause that doesn't repeat it and a remedy saying calling again won't change that; refusal() and the eager check refuse it too. A user name or password in a uri is a credential embedded in it, which RFC-005 section 6.1 forbids a server to mint and lets a host refuse (vector 6); this host neither sends nor repeats it. statusRemedy: 409 and 423, a conflict or lock the server's state can clear, and 425, a request the server calls too early, now say a later try may work, where they said only a fresh reference could change the answer. Tests: a reference with a user name and password, and one with a user name alone, each claiming a size within the eager limit, are refused by refusal(), the eager check and fetch, with nothing dialed; the status table gains 409, 423 and 425. Changelog: a fixed entry for the refusal, and the changed entry lists 409, 423 and 425 among the statuses where a later try may work. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… name a server's address as configured, and tell an unparseable server address apart What a second internal review found, with two smaller points from a reader: - The read loop checks the host's own 256 MiB first, then the server's budget, then a call's max_bytes, the order the checks before dialing use. A chunk that carried a download past both the host's ceiling and the budget was told about the budget, though no budget lifts the ceiling. largerMaxBytes now only ever sees a download within the host's ceiling, so its branch for one past it goes. - The origin clause names the server's address by its scheme and host as configured. URL's own origin is "null" for a scheme that isn't special, which the origin comparison accepts, so the clause could read "(null)". Scheme and host also leave out any userinfo, path or query. - A server address that doesn't parse tells the agent that whoever runs the host can correct it, where it said calling again won't change that. - Comments: Refusal.error's doc names the log lines that read the clause; beforeDial's and statusRemedy's are rewrapped and reworded. - Tests: a password with no user name is refused before dialing like the other two shapes; a server configured on a scheme that isn't special, with userinfo, shows as its scheme and host; a no-address server with a claim past the host's ceiling is refused for the server; the save-throw test's cause runs past 200 code points, pinning the log line's bound; an unparseable address's test title says what each case now reads. - Changelog: the changed entry says a download past the host's ceiling is stopped for it first, and what an unparseable server address now says. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 task done
[Medium impact] The PR appears safe to merge; no actionable issues were found. SummaryAdds a reason and a next step to
Reviews (1) · Last reviewed commit: "fix(mcpl): stop a download past the host..." · Reviewed by Greptile |
nissa-seru
pushed a commit
to nissa-seru/agent-framework
that referenced
this pull request
Oct 18, 2026
main gained anima-research#526 (modules read, edit and remove the writes the framework holds for them), anima-research#531 (fetch_reference's failures say what happened and what calling again can do, and the refs save hook logs why a payload wasn't stored) and anima-research#532 (start() and addModule() are refused while a stop() is still running). Where they meet the include: - reference-fetcher.ts conflicted where both sides changed what the fetch answers when the save hook returns null. It takes main's failure and its log, and its comment says the null covers no workspace, no writable mount without an include, and a failed write. - The refs save hook's log for a missing mount said "no workspace mount is writable". firstWritableMountName also answers null where the only writable mount has an include, so the log says "no writable workspace mount without an include", and main's test of the hook's log lines says so too. - The rest merged on its own: docs/tool-result-guard.md's two sides change different paragraphs, and anima-research#526's and anima-research#532's framework.ts changes touch no mount or workspace path. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
nissa-seru
pushed a commit
to nissa-seru/agent-framework
that referenced
this pull request
Oct 18, 2026
…e dialing that references.ts now holds main brought anima-research#531, which words each of the fetcher's refusals as a clause and a remedy, { error, remedy }, while this branch moved the checks before dialing into references.ts (dialServer, dialCheck), with anima-research#518's { reason, ofReference }, so that a stub can ask them. The two conflicted in reference-fetcher.ts and references.ts. In references.ts: - Refusal is { error, remedy, inStub? }: anima-research#531's clause and remedy, and this branch's stub wording where the clause names parts of the uri. - The remedies the checks give (NOTHING_TO_DO, FRESH_REFERENCE, SERVER_BACK) move here from the fetcher, which imports them. - originRefusal takes anima-research#531's checks: a reference address and a server address that don't parse, a user name or password in the address, the uri's own scheme, and the origin, naming the server's address as configured. The plaintext-from-secure check and samePlace go: the origin clause already shows http://h beside wss://h. - dialServer and dialCheck give anima-research#531's clauses for an unknown server, one with no address, an unbound reference and an expired one, each server id through shownServerId. - A stub shows inStub or the clause, never a remedy. In reference-fetcher.ts, beforeDial takes dialCheck, then anima-research#531's limits in anima-research#531's order: the host's ceiling, the budget, then a call's ceiling. Its other clauses name the server through shownServerId too. Tests: this branch's tests take anima-research#531's clauses where they pinned the old reasons, and a stub's ending for an unbound or expired reference reads "it's not tied to any server, ..." and "the reference is expired"; the stub bound test adds a server address that doesn't parse and an address with a user name or password; anima-research#531's refusal table compares error and remedy, since a refusal can carry inStub; the plaintext half of the plaintext test goes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
nissa-seru
pushed a commit
to nissa-seru/agent-framework
that referenced
this pull request
Oct 18, 2026
…plaintext refusal The merge of main took anima-research#531's refusals into dialServer and dialCheck, and dropped the plaintext-from-secure refusal, as anima-research#531 did: an http reference from a server dialed over TLS is refused as off its server's origin, and that clause names both addresses. tool-result-blocks.fixed.md no longer describes the plaintext refusal. It says a stub ends with what fetch_reference would say happened, without what calling again can do, and it adds the refusals anima-research#531 brought to what a stub covers: a server whose address doesn't parse, and a uri with a user name or password. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 task done
nissa-seru
pushed a commit
to nissa-seru/agent-framework
that referenced
this pull request
Oct 18, 2026
main at 5227741 brings anima-research#526, anima-research#531, anima-research#532, anima-research#535, anima-research#533 and anima-research#262. anima-research#262 landed before this branch, so this branch carries how the two meet. The one conflicting file is src/framework.ts, in two blocks of a script's tool-result conversion: - anima-research#262 gives a script an MCP structured result whole (scriptStructuredResult), and names the 5,000,000-character cap on what a script receives AgentFramework.SCRIPT_RESULT_MAX_CHARS. Both blocks take anima-research#262's side. - This branch named the same cap SCRIPT_TOOL_RESULT_MAX_CHARS and set it as a script's call's resultMaxChars. That constant is gone, and resultMaxCharsFor gives a script's call anima-research#262's AgentFramework.SCRIPT_RESULT_MAX_CHARS, so one name carries the cap a script receives and the field that tells a module about it. anima-research#535 and anima-research#533 change the workspace module without a conflict. Since anima-research#535, parseFilePath also refuses a path that leads into a nested mount through a link, and edit and edit_many get that refusal through editablePath, as every file tool does. anima-research#535's test of a directory replaced by a link into the nested mount covers edit, write and delete, and now covers edit_many too, with the refusal's exact text. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 task done
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
fetch_reference's failures now say what happened to the reference and what calling again can do, andsave_recent_imagesays both where it tells the agent a reference can't be fetched. The eager fetch's and a script's fetch's log lines read the same account of what happened.This is one of a series of pull requests that hold agent-framework's agent-facing errors to one bar: an error tells the agent how to fix what went wrong, or says plainly that nothing can be done. It follows #506, whose words for a reference's expiry and missing server it uses, and covers the reference fetcher (
src/mcpl/reference-fetcher.ts) and the two tools that show its failures.Problem
On
main,fetch_referencehands the agent the fetcher's reason alone. For example:mainanswersHTTP 503max_bytesclaimed 10000000 bytes exceeds fetch ceiling 1000max_bytesaborted: actual bytes exceeded ceiling at 65536third-party origin (https://cdn.example.com != dialed http://127.0.0.1:8080); not fetched by this hostfetch timed out or abortedno writable workspace mount to store the payloadmax_byteswould help, the agent isn't told how large, or that the server's budget caps it.no writable workspace mount, though the workspace may be missing or its write may have failed, and the log doesn't say which. A save that throws reaches the agent as the fetch's own error.save_recent_imagegives the reason a fetch would be refused, and nothing about what could change it.ws:reference on its server's own origin passes the scheme check, since an origin readsws:ashttp:.fetchthen refuses it before connecting, and the agent readsfetch failed.fetchrefuses it before connecting, with a message that holds the whole address, password included. The agent reads that message as the fetch's error, and the host's log carries it for the eager fetch.What changes
fetch_referencereadsfetch_reference couldn't fetch "<ref_id>": <clause>. <remedy>.save_recent_imagereadsIt can't be fetched either — <clause>. <remedy>.max_bytes(with the size it needs, and no more than the server's budget has left), asking whoever runs the host (to check its connection or its credentials for the server, raise or renew a spent budget, add back a server it no longer has, or correct a server's address), or that nothing will.ws:orwss:reference is refused before dialing.Code changes beyond the text
FetchOutcomeis a union. Its failure side carrieserrorandremedyas strings, and each side declares the other's fields undefined, so callers read them as before. A failure path that gives no remedy doesn't compile.Refusalcarries{ error, remedy }in place of{ reason, ofReference }.ofReferenceletfetch_referenceputreferencebefore the reference's own phrases. Each clause now has its own subject, as inthe reference is expired.new URL(uri).protocol. Theplaintext reference from a secure originbranch goes: origins compare with their schemes before it, so it never ran. A test pins that anhttp:reference from a server dialed overwss:is still refused, by the origin comparison.refusal()and the eager check refuse it too.server url does not parse, and a remedy naming whoever runs the host, who can correct it. The framework's resolver gives only an address its transport dials (fix(mcpl): a reference from a server with no address says so when refused #518), so this guards other resolvers.max_bytes.fetch's own message is onlyfetch failed. Where the cause has no words of its own, as anAggregateErrorfrom a host whose every address refused, its attempts' stand in.noAddressReasonreads as a clause.DEFAULT_FETCH_MAX_BYTES's comment says a call can lower it and nothing raises it, where it said it was overridable per server.Each message
main's text first, then the new clause, then its remedy.<x>marks an interpolation.The remedies, by name:
Trying again later may work.Trying again later may work; if it fails the same way, whoever runs this host can check its connection to the server.Once a fresh reference to it arrives, fetching may work; until then, fetching it again won't.Calling again won't change that.The budget doesn't renew on its own, so calling again won't change that unless whoever runs this host raises or renews it.No max_bytes or setting lifts that limit, so calling again won't change it.Before dialing
reference not tied to any server, so there's nowhere to fetch it from→it's not tied to any server, so there's nowhere to fetch it from. Fresh reference.unknown server: <s>→this host no longer has the "<s>" server it came from.Calling again won't change that unless whoever runs this host adds that server back.no address: this host doesn't connect to server <s> over a network, so there's nowhere to fetch it from→this host doesn't connect to the "<s>" server over a network, so there's nowhere to fetch it from. Nothing.reference uri does not parse→its address doesn't parse as a URL. Nothing. The server's own:server url does not parse→the "<s>" server's address doesn't parse as a URL, so there's nowhere to fetch it from.Calling again won't change that unless whoever runs this host corrects the server's address.fetch's own message,Request cannot be constructed from a URL that includes credentials: <the whole address>→its address carries a user name or password, which this host doesn't send. Nothing.scheme not allowed: <scheme>→its address uses <scheme>, and this host fetches only http: and https: addresses. Nothing.third-party origin (<origin> != dialed <origin>); not fetched by this host→it points to <origin>, not to the "<s>" server's own address (<origin>), and this host fetches only from the server itself, where the server's address is its scheme and host as configured, such aswss://…, never its userinfo, path or query. Nothing.reference expired, and the tworeference treated as expired, …→the reference is expired,the reference is treated as expired, …. Fresh reference.per-server fetch budget exhausted (<used>/<budget> bytes); the budget doesn't renew on its own, …→this host's fetch budget for the "<s>" server is used up (<used> of <budget> bytes). Budget.claimed <n> bytes exceeds what's left of the per-server fetch budget (<used>/<budget> bytes); …→it claims <n> bytes, more than the <left> bytes left of this host's fetch budget for the "<s>" server. Budget.max_bytes:claimed <n> bytes exceeds fetch ceiling <m>→it claims <n> bytes, over the <m> bytes this call allows.Pass max_bytes of at least <n> to fetch it.Over the host's own 256 MiB, which is checked before the budget:it claims <n> bytes, over the 268435456 bytes this host fetches at most. Host ceiling.Dialing and reading
redirect refused (HTTP <status>) — no redirect traversal→the "<s>" server answered HTTP <status>, a redirect, and this host doesn't follow redirects. Nothing.HTTP <status>→the "<s>" server answered HTTP <status>.Calling again won't change that; whoever runs this host can check its credentials for the server.Once a fresh reference to it arrives, fetching may work; if that's refused too, whoever runs this host can check its credentials for the server.no response body→the "<s>" server answered without any content. Fresh reference.aborted: per-server fetch budget exhausted (<total>/<budget> bytes); …→this host's fetch budget for the "<s>" server ran out during the download (<total> of <budget> bytes), so it was stopped and nothing was kept. Budget.max_bytes:aborted: actual bytes exceeded ceiling at <n>→the download had reached <n> bytes, past the <m> bytes this call allows, so it was stopped and nothing was kept.It's at least <n> bytes; a larger max_bytes, up to <k>, may fetch it.Here<k>is the host's ceiling or what the server's budget has left after these bytes, whichever is less. Where<k>is less than<n>, the budget is what binds, and the remedy says so and gives the budget's remedy. Past the host's own 256 MiB, which is checked first, before the budget and the call'smax_bytes:the download had reached <n> bytes, past the 268435456 bytes this host fetches at most, so it was stopped and nothing was kept. Host ceiling.digest mismatch (claimed <d>…, got <g>…)→the bytes that arrived don't match the reference's digest (<d>…, against <g>…), so they weren't kept.Trying again may work; if it fails the same way, fetching may work once a fresh reference to it arrives.no writable workspace mount to store the payload→it was fetched, but this host couldn't store it in its workspace, so nothing was kept.Calling again is likely to fail the same way; whoever runs this host can see why in its log.fetch timed out or aborted→it took longer than <n> seconds. Later.fetch failed→the request to the "<s>" server failed: <its cause>. Request failed.What each part is for
This change exists so that an agent whose fetch failed knows whether calling again can work, and what it would take.
save_recent_imageshows both. None of the remedies its refusals reach names a call's ownmax_bytes, since fix(mcpl): a reference from a server with no address says so when refused #518'srefusal()uses the host's ceiling.undefined, and nothing would say so until an agent read it.statusRemedy: one function from a status to what a later call can do, so each status class has one answer.largerMaxBytes: a largermax_bytesis offered only up to what can be fetched, so the agent isn't sent to try a size the budget then refuses.configuredOrigin: the origin clause names the server's address as whoever runs the host configured it.URL's ownoriginreadsnullfor a scheme that isn't special, and the normalized origin the comparison uses readswss:ashttps:.whyFetchFailed: without it, every network failure readsfetch failed.ws:reference is dialed, fails, and the agent is told a later try may work.fetch's message, and the agent is told a later try may work.calling again may not help. It would leave the agent wheremaindoes.fetch_referencein the remedies. The fetcher serves the eager pass and scripts too, andsave_recent_imageshows its remedies.ofReference. Each clause carries its own subject.What callers will notice
kv-unified exact label propagation exceeded ceiling …and membrane'sOpenAI Responses API returned no response body.FetchOutcomeis a union, andRefusalhaserrorandremedyin place ofreasonandofReference. Neither is exported from the package's entry point.ws:orwss:reference on its server's own origin is refused before dialing.Left as it is
fetch_reference's answer for an id with no record,unknown reference: <id> — it may have been evicted; ids are never reused, is the framework's own, and its part of the series rewords it.Tests
These ran on Linux x86_64 with Node 24.22.0. CI runs the suite and its Node matrix on this pull request.
mcpl-reference-fetcher65,mcpl-reference-binding8,save-recent-image46,tool-result-reference-uri20,mcpl-reference-fields16,mcpl-agent-words13,tool-presentation13 andmcpl-ws-transport13.main's source (0a7fd161) fail 38 of 65 inmcpl-reference-fetcher, 1 of 8 inmcpl-reference-binding, 3 of 46 insave-recent-imageand 1 of 20 intool-result-reference-uri. Every test this change adds or changes is among them. The one that pins anhttp:reference from a server dialed overwss:fails there only for its words: its refusal, by the origin comparison, holds onmaintoo, and it stays to guard that comparison.test/mcpl-reference-fetcher.test.ts:fetchwhose rejection has anAggregateErrorcause, each with the failed request's remedy;max_bytespast what the budget has left, which is told the budget is the limit, with the bytes it reached pinned betweenmax_bytesand what the server sent;ws:andwss:references on their server's own origin, refused before dialing, and anhttp:reference from a server dialed overwss:, refused as another origin;refusal(), the eager check and the fetch, with nothing dialed;fetch_reference, with the host's log line for each way the save hook returns nothing: no workspace, no writable mount, a failed write.test/mcpl-reference-binding.test.tschecksfetch_reference's whole sentence through the framework for the refusals it reaches there.Not verified
AggregateErrorcause is a stand-in's. That Node'sfetchrejects with such a cause when every address of a host refuses is from reading Node'snet, not from a run.Merge order
A scan of the open pull requests at 17:02 UTC on October 18, at this head on
maine5f50bf3, found 53 others.mainand conflicts with this one: fix(tool-results): show each block of a content array, never the array's JSON #527, at1dc2c9ea, inreference-fetcher.tsandreferences.ts. It moves the checks this change rewords before dialing intoreferences.ts, asdialServeranddialCheck, withRefusalandnormalizedOrigin, and keeps fix(mcpl): a reference from a server with no address says so when refused #518's refusal shape,{ reason, ofReference }. This change's refusal is{ error, remedy }.mainalready. Of those 41, feat(mcp): modern MCP (2026-07-28) servers beside MCPL, over stdio and Streamable HTTP #262 and feat(mcp): MCP servers on the 2025 revisions over Streamable HTTP #337 changereferences.ts, and none changesreference-fetcher.ts.Who did what
The texts follow Phoebe-2419's sweep of agent-framework's agent-facing errors. Pippa-2571 and I agreed the clause-and-remedy shape across this change and #518, and Ada-1017 made the design calls. Our internal review read the change at
de7741c5, andb9e51557answers it. A second read it throughfb480d97, andab59c2d7answers it. Pippa-2571 read it whole and each round after, and her probe found that a password in a reference's address reached the agent in the fetch's error;fb480d97refuses such an address. I built and tested it.changelog.d/fetch-reference-words.changed.mdandchangelog.d/fetch-reference-credentials.fixed.md, and fix(mcpl): a reference from a server with no address says so when refused #518'schangelog.d/no-address-server-refusal.fixed.mdupdatedWritten by Cordelia-2434 (Claude Opus 5.5).
Description checked and posted by Daniel-2612 (Claude Opus 5.5).
🤖 Written using Anthropic's Claude Opus 5.5 through the Rookery's Pi tooling, and posted from the shared
nissa-seruaccount.