From fb0063de48749915b4033dc99437f0c89ac6eb69 Mon Sep 17 00:00:00 2001 From: Chinesezjc Date: Thu, 30 Jul 2026 21:28:54 +0800 Subject: [PATCH] fix(tool-web): unexport renderFetchOutput so its memo stays behind the frozen path renderFetchOutput has no external consumer: only formatFetchOutput and fetchMetaFromValue call it, both through the registry, which deep-freezes the result value. Exporting it let a hypothetical caller mutate a cached input or the returned RenderedFetch and desync the card's truncated flag from the model text. Drop it from the barrel and document that the memo needs no defensive copy because every caller is internal and read-only. --- packages/web/tool-web/src/fetch.ts | 14 +++++++++----- packages/web/tool-web/src/index.ts | 2 +- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/packages/web/tool-web/src/fetch.ts b/packages/web/tool-web/src/fetch.ts index 924f878d8a..d642615874 100644 --- a/packages/web/tool-web/src/fetch.ts +++ b/packages/web/tool-web/src/fetch.ts @@ -266,10 +266,14 @@ interface RenderedFetch { * limits the source prefix processed synchronously, then applies again where the * complete output — header, rendered body, and footer — is known. * - * The tool registry calls this once through `output.render` and again through - * `output.presentationMeta`, both with the same frozen result value; the - * conversion is memoized per `(result, maxOutputChars)` so the synchronous DOM - * parse and turndown walk run once, not twice, on the same body. + * Package-internal: the only callers are {@link formatFetchOutput} and + * {@link fetchMetaFromValue}, both reached through the tool registry, which + * deep-freezes the result value before calling `output.render` and + * `output.presentationMeta`. The conversion is memoized per + * `(result, maxOutputChars)` so the synchronous DOM parse and turndown walk run + * once, not twice, on that same frozen value. Keeping it unexported means no + * caller can mutate a cached input or the returned {@link RenderedFetch}, so the + * memo needs no defensive copy. * * @param result - the seam's fetch outcome. * @param maxOutputChars - cap on the complete returned string; a cut body gets @@ -277,7 +281,7 @@ interface RenderedFetch { * @returns the complete `Fetched (HTTP )`-headed text and whether * the provider, a source cut, or the cap trimmed the content. */ -export function renderFetchOutput(result: WebFetchResult, maxOutputChars: number): RenderedFetch { +function renderFetchOutput(result: WebFetchResult, maxOutputChars: number): RenderedFetch { const byCap = renderCache.get(result) ?? new Map() const cached = byCap.get(maxOutputChars) if (cached !== undefined) return cached diff --git a/packages/web/tool-web/src/index.ts b/packages/web/tool-web/src/index.ts index f9236ecc4f..397e2bf7bb 100644 --- a/packages/web/tool-web/src/index.ts +++ b/packages/web/tool-web/src/index.ts @@ -14,7 +14,7 @@ import { applyWebFetchTool } from './fetch.ts' export { WEB_SEARCH_MAX_RESULTS, applyWebSearchTool, formatSearchOutput, parseSearchArgs, presentSearchCall, presentSearchResult, searchMetaFromValue, searchMetaFromResult } from './search.ts' export type { WebSearchMeta } from './search.ts' -export { applyWebFetchTool, formatFetchOutput, renderFetchOutput, parseFetchArgs, presentFetchCall, presentFetchResult, fetchMetaFromValue, fetchMetaFromResult } from './fetch.ts' +export { applyWebFetchTool, formatFetchOutput, parseFetchArgs, presentFetchCall, presentFetchResult, fetchMetaFromValue, fetchMetaFromResult } from './fetch.ts' export type { WebFetchMeta } from './fetch.ts' /** Cordis plugin name used by loader diagnostics. */