docs/references/ai/chat-attachments.md
How a user's attached files reach the model on a chat turn.
One rule, per attachment: if the provider+model can take it as a native
input, send the native file; otherwise send its extracted text, inlined
and capped. The read_file tool only exists to page the overflow of large
text — it is never the only way the model sees content.
This is deliberate: visibility must not depend on the model choosing to call a tool. A weak (or non-tool-calling) model still sees every attachment, and a provider that handles a modality natively keeps doing so — no capability regression.
Decided per file part in prepareChatMessages
(src/main/ai/messages/attachmentRouting.ts):
| Attachment | Native when | What the model receives |
|---|---|---|
| image | model is vision | native image part (inline) |
| image | non-vision, OCR finds text | OCR text, inline (capped) |
| image | non-vision, no OCR text (or OCR unconfigured/failed) | native image part (inline base64) |
| provider+model native PDF | native PDF part (inline) | |
| otherwise | extracted text, inline (capped) | |
office (docx/xlsx/pptx/odf) | — | extracted text, inline (capped) |
| text / code | — | decoded text, inline (capped) |
| extensionless | — | decoded text when content is text; otherwise unsupported note |
| audio | model is audio-capable | native audio part (inline) |
| audio | otherwise | short note ("can't process audio") |
| video | model is video-capable | native video part (inline) |
| video | otherwise | short note ("can't process video") |
| other (binary: zip/exe/…) | — | short note ("unsupported file type") |
data: URL
by materializeNativeFilePart (src/main/ai/messages/fileProcessor.ts), which
also normalizes the mediaType to the on-disk MIME. The provider gets the real
file as a user-message part. (The function is named for the boundary: provider
File-API upload for large files would slot in behind the same signature.)[could not read this file]. note rather than dropping the
file or failing the request.fileEntryId is never written into the prompt.Only fileEntryId-backed (first-party chat) images enter the OCR path. Gateway /
external file parts (no fileEntryId) are still eagerly materialized, but an
image is omitted for a non-vision model because it was never an explicit OCR
fallback. Other gateway/external file types keep their existing behavior.
Extracted text is bounded so multi-turn context stays in control:
cap chars + a trailer:
[truncated N/total — call read_file("name", offset=N) for more][truncated N/total]Default cap ≈ 8k chars/file (tunable).
read_file — text-only overflow toolsrc/main/ai/tools/fileLookup.ts + tools/adapters/aiSdk/builtin/ReadFileTool.ts.
{ filename, offset?, limit? }. The filename is the model-facing
handle (unique, normalized — see collectFileAttachments), resolved to an
entry id against a per-request allow-list. The model never sees or guesses
entry ids, and can only read files attached to the current conversation.applies: scope.hasFileAttachments). It pages over-cap text; when
everything inlines within the cap the model simply never needs to call it.assistant-files MCP server. Its model-facing handles are stable, opaque
hashes of FileEntry ids. The server rebuilds its allow-list from the current
session transcript when each tool call runs, so deleting a message revokes
access immediately. Ordinary Claude Code Agents receive neither this server
nor its attachment manifest.read_file carries no media result — no toModelOutput base64 re-read, no
resend re-materialization.Cherry Assistant also gets approval-gated save_attachment from the same
session-scoped server. It writes only new paths inside the session workspace and
never overwrites an existing file. No attachment state or write tools are added
to the shared chat runtime or to ordinary Agents.
| Concern | Owner |
|---|---|
| office/pdf/text → text | extractDocumentText (src/main/ai/messages/attachmentTextExtraction.ts) |
| image → text (non-vision) | FileProcessingService.ocrImage (src/main/features/fileProcessing/) |
ai/ reaches OCR through the FileProcessingService rather than deep-importing
the feature, keeping processor/handler internals in that domain. Both
extractDocumentText and the OCR path are path-free and cache their result by
content version (30 min), so the eager every-turn pass over history doesn't
re-extract or re-OCR the same file. extractDocumentText reads bytes through
FileManager.read (PDF via pdf-parse, office via
officeparser/word-extractor, known text extensions via
decodeTextWithAutoEncoding, and extensionless text via
decodeTextBufferIfText) and dispatches on the FileEntry canonical ext.
resolveNativeFileSupport
(src/main/ai/runtime/aiSdk/params/nativeFileSupport.ts) derives the
"native" column from (provider, model): image/audio/video ride on the model
capability alone (isVision / isAudio / isVideo, @shared/utils/model),
while PDF additionally requires a first-party provider (supportsNativePdf).
There is no pdf-compatibility middleware — native PDFs pass through inline,
non-native PDFs go through extraction.
fileEntryId never reaches the model (filename in, filename out).