/** *Metadataforanattachmentthatwasoffloadedtothemediastore. * *IncludedinParsedMessageWithImages.offloadedRefssothatcallerscan *persiststructuredmediametadatafortranscripts.Withoutthis,consumers *thatderiveMediaPath/MediaPathsfromthe`images`array(e.g. *persistChatSendImagesandbuildChatSendTranscriptMessageinchat.ts)would *silentlyomitalllargeattachmentsthatwereoffloadedtodisk.
*/
export type OffloadedRef = { /** Opaque media URI injected into the message, e.g. "media://inbound/<id>" */
mediaRef: string; /** The raw media ID from SavedMedia.id, usable with resolveMediaBufferPath */
id: string; /** Absolute filesystem path returned by saveMediaBuffer — used for transcript MediaPath */
path: string; /** MIME type of the offloaded attachment */
mimeType: string; /** The label / filename of the original attachment */
label: string;
};
export type ParsedMessageWithImages = {
message: string; /** Small attachments (≤ OFFLOAD_THRESHOLD_BYTES) passed inline to the model */
images: ChatImageContent[]; /** Original accepted attachment order after inline/offloaded split. */
imageOrder: PromptImageOrderEntry[]; /** *Largeattachments(>OFFLOAD_THRESHOLD_BYTES)thatwereoffloadedtothe *mediastore.Eachentrycorrespondstoa`[mediaattached:media://inbound/<id>]` *markerappendedto`message`. * *CallersMUSTpersistthislistseparatelyfortranscriptmediametadata. *Itisintentionallyseparatefrom`images`becausedownstreammodelcalls *donotreceivetheseasinlineimageblocks. * *⚠️Callsites(chat.ts,agent.ts,server-node-events.ts)MUSTalsopass *`supportsImages:modelSupportsImages(model)`sotext-onlymodelruns *offloadimagesasmediarefsinsteadofpassinginlineimageblocks.
*/
offloadedRefs: OffloadedRef[];
};
const MIME_TO_EXT: Record<string, string> = { "image/jpeg": ".jpg", "image/jpg": ".jpg", "image/png": ".png", "image/webp": ".webp", "image/gif": ".gif", "image/heic": ".heic", "image/heif": ".heif", // bmp/tiff excluded from SUPPORTED_OFFLOAD_MIMES to avoid extension-loss // bug in store.ts; entries kept here for future extension support "image/bmp": ".bmp", "image/tiff": ".tiff",
};
// Module-level Set for O(1) lookup — not rebuilt on every attachment iteration. // // heic/heif are included only if store.ts's extensionForMime maps them to an // extension. If it does not (same extension-loss risk as bmp/tiff), remove // them from this set. const SUPPORTED_OFFLOAD_MIMES = new Set([ "image/jpeg", "image/jpg", "image/png", "image/webp", "image/gif", "image/heic", "image/heif",
]);
/** *TypeguardforthereturnvalueofsaveMediaBuffer. * *AlsovalidatesthatthereturnedID: *-isanon-emptystring *-containsnopathseparators(/or\)ornullbytes * *Catchingabadshapehereproducesacleanererrorthanacrypticfailure *deeperinthestack,andistreatedasa5xxinfrastructureerror.
*/ function assertSavedMedia(value: unknown, label: string): SavedMedia { if (
value !== null && typeof value === "object" && "id" in value && typeof (value as Record<string, unknown>).id === "string"
) { const id = (value as Record<string, unknown>).id as string; if (id.length === 0) { thrownew Error(`attachment ${label}: saveMediaBuffer returned an empty media ID`);
} if (id.includes("/") || id.includes("\\") || id.includes("\0")) { thrownew Error(
`attachment ${label}: saveMediaBuffer returned an unsafe media ID ` +
`(contains path separator or nullbyte)`,
);
} return value as SavedMedia;
} thrownew Error(`attachment ${label}: saveMediaBuffer returned an unexpected shape`);
}
// Track IDs of files saved during this request for cleanup if a later // attachment fails validation and the entire parse is aborted. const savedMediaIds: string[] = [];
try { for (const [idx, att] of attachments.entries()) { if (!att) { continue;
}
if (sniffedMime && !isImageMime(sniffedMime)) {
log?.warn(`attachment ${label}: detected non-image (${sniffedMime}), dropping`); continue;
} if (!sniffedMime && !isImageMime(providedMime)) {
log?.warn(`attachment ${label}: unable to detect image mime type, dropping`); continue;
} if (sniffedMime && providedMime && sniffedMime !== providedMime) {
log?.warn(
`attachment ${label}: mime mismatch (${providedMime} -> ${sniffedMime}), using sniffed`,
);
}
// Third fallback normalises `mime` so a raw un-normalised string (e.g. // "IMAGE/JPEG") does not silently bypass the SUPPORTED_OFFLOAD_MIMES check. const finalMime = sniffedMime ?? providedMime ?? normalizeMime(mime) ?? mime;
if (!isSupportedForOffload) { if (shouldForceOffload) {
log?.warn(
`attachment ${label}: format ${finalMime} cannot be offloaded for ` + "text-only model, dropping",
); continue;
} // Passing this inline would reintroduce the OOM risk this PR prevents. thrownew Error(
`attachment ${label}: format ${finalMime} is too large to pass inline ` +
`(${sizeBytes} > ${OFFLOAD_THRESHOLD_BYTES} bytes) and cannot be offloaded. ` +
`Please convert to JPEG, PNG, WEBP, GIF, HEIC, or HEIF.`,
);
}
// Decode and run input-validation BEFORE the MediaOffloadError try/catch. // verifyDecodedSize is a 4xx client error and must not be wrapped as a // 5xx MediaOffloadError. const buffer = Buffer.from(b64, "base64");
verifyDecodedSize(buffer, sizeBytes, label);
// Only the storage operation is wrapped so callers can distinguish // infrastructure failures (5xx) from input errors (4xx). try { const labelWithExt = ensureExtension(label, finalMime);
// Track for cleanup if a subsequent attachment fails.
savedMediaIds.push(savedMedia.id);
// Opaque URI — compatible with workspaceOnly sandboxes and decouples // the Gateway from the agent's filesystem layout. const mediaRef = `media://inbound/${savedMedia.id}`;
// Record for transcript metadata — separate from `images` because // these are not passed inline to the model.
offloadedRefs.push({
mediaRef,
id: savedMedia.id,
path: savedMedia.path ?? "",
mimeType: finalMime,
label,
});
imageOrder.push("offloaded");
isOffloaded = true;
} catch (err) { const errorMessage = formatErrorMessage(err); thrownew MediaOffloadError(
`[Gateway Error] Failed to save intercepted media to disk: ${errorMessage}`,
{ cause: err },
);
}
}
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.