Repository navigation
feat(server): file attachments, upload-first pipeline, and document originals - #253
Merged
Merged
Conversation
Document attachments persist only their extracted text as `<id>.txt`, and that text is already inlined into the turn text. Codex, Cursor, and Grok still read the `.txt` and sent it as a base64 image, and OpenCode sent it as a file part labelled with the original document's mime (for example application/pdf). Every adapter now sends only real images natively; the document reaches the agent once, as the inlined text. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
ProviderService appends each attachment's absolute on-disk path to the turn text so the agent's tools can open the real file, and Claude gets the attachments directory as an allowed additional directory. Avi Code addition: a document's stored file is its extracted text, so its line says "Text extracted from attached document" rather than implying the original PDF or DOCX lives at that path. Ported from pingdotgg#5757 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Server and contracts half of the upload-first pipeline. Clients call `attachments.createUploadUrl` for a signed, short-lived upload URL, POST the bytes there, and send the turn with the pending attachment id instead of an inline data URL. The Normalizer claims the pending upload for the thread (copying it, so a failed bootstrap can retry), and dispatch failures clean up the claimed copies. Stale pending uploads are swept at startup and while issuing URLs. `attachments.delete` removes an abandoned pending upload, and the environment advertises `attachmentUploads`. Inline data URL images and the fork's inline document attachments keep working unchanged, so the current web client needs no change. Avi Code additions: thread.fork accepts and cleans up uploads like thread.turn.start, and a client can never send a persisted document without its extracted text. Brings along PROVIDER_SEND_TURN_SUPPORTED_IMAGE_MIME_TYPES from pingdotgg#6574, which the upload input schema needs. Ported from pingdotgg#8048 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Server and contracts half of generic file attachments. `ChatFileAttachment` (any mime, up to 50MB) joins the attachment union, and `attachments.createUploadUrl` takes `type: "file"`. File ids carry their extension (`<thread>-<uuid>-pdf`), which is how the server finds `<id>.pdf`, keeps it through reverts, and serves it as a download with the caller's filename and a safe Content-Type. Uploads stream to disk and partial files are always removed. The environment advertises `fileAttachments`. Claude, Codex, Cursor, and Grok send images natively and reach every other attachment through the path line in the turn text. OpenCode sends images, text, and PDFs up to 20MB as native file parts; its text generation sends images only. Avi Code notes: documents are never sent natively by any adapter, since their text is already inlined. `ChatUnknownAttachment` (with `document` excluded from its catch-all pattern) is defined but not yet in the `ChatAttachment` union: its open `type: string` would stop the web chat view and timeline from narrowing on `type === "image"`, so it lands with the web port that moves those reads to type guards. Ported from pingdotgg#8235 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Avi Code addition, the fork half of the hybrid attachment design. A client can upload a document's original through `attachments.createUploadUrl` (`type: "file"`) and send the document with that pending id alongside its extracted text. The server claims the original like a file upload, so the persisted document id carries the original's extension (`<thread>-<uuid>-pdf`) and the original sits at `<id>.pdf` next to the extracted `<id>.txt` (a `.txt` original is its own extracted text). The path line then points agents at the real file, reverts keep both files, failed dispatches remove both, and the asset route serves the original as a download. The extracted text is still inlined into the turn and no adapter sends a document natively. Documents without an upload id, including every document persisted before this change, keep today's shape and behavior. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…-server # Conflicts: # CHANGELOG.md
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
TheDarkPhantom
enabled auto-merge
September 30, 2026 11:58
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.
Document attachments were being sent to Codex, Cursor, and Grok as base64 images (the stored file is the extracted
.txt), and OpenCode sent that.txtlabelled with the original mime (e.g.application/pdf). Separately, upstream has shipped an upload-first attachment pipeline, generic file attachments up to 50MB, and path lines that let agents open attachments by path. This PR fixes the bug and ports the server and contracts half of those features. It uses a hybrid model that keeps the fork's document type.What changed
toOpenCodeFileParts, plus text generation).ProviderServiceappends[Attached image "x.png" is saved at: /abs/path]to the turn text, and Claude gets the attachments dir inadditionalDirectories. Legacy documents say[Text extracted from attached document "x.pdf" is saved at: …txt], so agents are not told a.txtis the PDF.attachments.createUploadUrl/attachments.deleteRPCs (operate scope), a signedPOST /api/attachments/upload/<token>route, claiming pending uploads in the Normalizer (also forthread.fork), cleanup when dispatch fails, a pending-upload sweep, and theattachmentUploadscapability.ChatFileAttachmentup to 50MB, ids that carry the extension (<thread>-<uuid>-pdf), streamed uploads, download headers for file assets, thefileAttachments.maxUploadBytescapability, and OpenCode native file parts for images, text, and PDF up to 20MB.fileupload id. The server then keeps the original at<id>.<ext>next to<id>.txt, the path line points at the original, reverts keep both files, and failed dispatches remove both.Compatibility
types.tsandofflineTurnOutboxStore.ts.OrchestrationMessageandThreadMessageSentPayload.ChatUnknownAttachmentis defined (its pattern excludesdocument) but is not in theChatAttachmentunion yet. Its opentype: stringstopsattachment.type === "image"narrowing inChatView.tsxandMessagesTimeline.tsx, which are out of scope here. It should land with the web port.Verification
vp test runpasses for contracts, attachments, Normalizer, ProjectionPipeline, ProviderService, the Codex/Claude/OpenCode adapters, http, the server upload routes, and the integration test.tsgois clean for server, contracts, shared, client-runtime, and web.vp run check:avicodepasses.🤖 Generated with Claude Code