Skip to content

feat(server): file attachments, upload-first pipeline, and document originals - #253

Merged
TheDarkPhantom merged 7 commits into
mainfrom
feat/file-attachments-server
Sep 30, 2026
Merged

TheDarkPhantom merged 7 commits into
mainfrom
feat/file-attachments-server

Conversation

@TheDarkPhantom

Copy link
Copy Markdown
Owner

Document attachments were being sent to Codex, Cursor, and Grok as base64 images (the stored file is the extracted .txt), and OpenCode sent that .txt labelled 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

  1. fix: documents are no longer sent as images. Every adapter sends only real images natively. Documents reach providers once, as the text the Normalizer already inlines. Covered by tests per adapter: Codex (runs locally), Cursor and Grok (mock ACP agent, CI only on Windows), and OpenCode (toOpenCodeFileParts, plus text generation).
  2. Path lines (feat(server): agents can now open the images you paste into chat pingdotgg/t3code#5757). ProviderService appends [Attached image "x.png" is saved at: /abs/path] to the turn text, and Claude gets the attachments dir in additionalDirectories. Legacy documents say [Text extracted from attached document "x.pdf" is saved at: …txt], so agents are not told a .txt is the PDF.
  3. Upload-first pipeline (feat(web): upload image attachments before sending pingdotgg/t3code#8048, server + contracts): attachments.createUploadUrl / attachments.delete RPCs (operate scope), a signed POST /api/attachments/upload/<token> route, claiming pending uploads in the Normalizer (also for thread.fork), cleanup when dispatch fails, a pending-upload sweep, and the attachmentUploads capability.
  4. Generic file attachments (feat(server): accept PDF, ZIP, and other file uploads up to 50MB pingdotgg/t3code#8235, server + contracts): ChatFileAttachment up to 50MB, ids that carry the extension (<thread>-<uuid>-pdf), streamed uploads, download headers for file assets, the fileAttachments.maxUploadBytes capability, and OpenCode native file parts for images, text, and PDF up to 20MB.
  5. Document originals (fork): a document can carry a pending file upload 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

  • The current web client is unchanged and keeps working. It still sends inline images and inline documents. The only web edits are type plumbing in types.ts and offlineTurnOutboxStore.ts.
  • Old persisted documents (plain id, no new fields) decode and behave exactly as before. This is tested against OrchestrationMessage and ThreadMessageSentPayload.
  • No migrations.
  • ChatUnknownAttachment is defined (its pattern excludes document) but is not in the ChatAttachment union yet. Its open type: string stops attachment.type === "image" narrowing in ChatView.tsx and MessagesTimeline.tsx, which are out of scope here. It should land with the web port.

Verification

  • Scoped vp test run passes for contracts, attachments, Normalizer, ProjectionPipeline, ProviderService, the Codex/Claude/OpenCode adapters, http, the server upload routes, and the integration test.
  • tsgo is clean for server, contracts, shared, client-runtime, and web.
  • vp run check:avicode passes.

🤖 Generated with Claude Code

TheDarkPhantom and others added 7 commits September 30, 2026 05:36
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>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XXL labels Sep 30, 2026
@TheDarkPhantom
TheDarkPhantom merged commit e9cd2f1 into main Sep 30, 2026
12 checks passed
@TheDarkPhantom
TheDarkPhantom deleted the feat/file-attachments-server branch September 30, 2026 12:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant