Files
deepseek-harness/.agents/notes/implemented/feature/2026-08-26-generic-file-upload.md
T
Yichen Jiang 9be99a0e28 feat(web): upload composer images when they are added
An ordinary Session's composer now uploads each image draft through the
background upload queue as soon as it is added. The raw upload route stages
the body as a prompt image when the request names its media type: the Host
reads at most the per-image byte limit, then validates, normalizes, and
stores the image as prompt admission would. The prompt cites the receipt in
a file part; prompt admission resolves it to the stored image, applies the
image-input check, and counts stored images toward maxImagesPerMessage.

Send waits for every image receipt, and a failed image upload turns its
thumbnail into a retry control. Commands and subagent continuations keep
encoding images inline.
2026-10-21 13:50:19 +08:00

13 KiB

Agent Note: Generic file upload

Status: implemented

English | 中文

Problem

Sessions accepted pasted or dropped images as durable attachments, but no other file kind could enter a conversation: the composer refused non-image bytes, and the only file affordance was the @ path mention, which requires the file to already be readable from the host workspace. A user could not hand the agent a PDF, a data file, or an archive from a browser, and DeepSeek Chat users expect the paperclip-and-file-card upload flow (#2984).

The command plane had already established a whole-envelope rule for images: a command consumes the text and attachments together or visibly refuses the submission. Its input.images declaration and image-only RPC payload made generic files a separate refusal path even after ordinary messages could carry them. Plan and goal entry therefore needed the same attachment vocabulary without weakening executor enforcement or silently clearing a draft.

Decision

Files and images use separate storage schemes and one ordered message attachment list. Images keep their normalized-attachment pipeline unchanged. Other picked files enter one FIFO background-upload queue and are stored byte-for-byte with no type or size limits, then staged in-process for the same Session. Equal bytes have one canonical object under DSH_HOME/attachments/v1/file-objects/<digest-prefix>/<digest>; each read-only model-facing path at DSH_HOME/attachments/v1/files/<digest-prefix>/<digest>/<name> is a hard link, so different display names do not duplicate content. The configurable queue defaults to two active transfers. The independent dual-face file-upload Cordis package owns transfer and staging. Its Client fileUpload service accepts the owning Session identity together with a Blob, exact bytes, or a one-shot ReadableStream<Uint8Array>; it uses that identity to assemble the raw route request or invoke the generated Remote directly. Its Host fileUploads service owns the streaming route, encoded Remote fallback, command receipt resolver, and receipt tables keyed by the receiving Agent's Session object. Encoded admission and attachment-error recognition go through ctx.attachments, whose provider owns byte storage. Session Controller registers the resolver for cold ordinary Agents and consumes receipts during prompt admission; Session objects do not expose file transfer. Served Web pages hand each active body to a dedicated upload Worker: Blob bodies use XMLHttpRequest for total-aware byte progress, while streams transfer ownership and feed Fetch incrementally. Worker-hosted pages transfer the body through their page-owned Fetch carrier to the Host Worker, which reads bounded chunks. The page thread never reads, base64-encodes, or JSON-serializes generic file bytes. The authenticated Host route opts into Connection's streaming request mode, so the bridge dispatches before consuming the body and the local store hashes and writes bounded chunks to a private staging object. Completion atomically publishes the content-addressed object; cancellation or failure removes the staging file. Upload operations and draft state belong to the resident conversation root, so navigation to another Session does not stop the transfer; returning to the original Session shows the same card and progress. Removing the file card skips its queued transfer or aborts its active transfer. The sent message carries a structured FileBlock ({ name, bytes, attachmentId }) inside user/message; no new session event type exists. Request assembly in LlmRuntime projects every file block, including nested tool-result occurrences, to one deterministic handle naming the file and its saved read-only path. The handle tells a main agent to include that path when delegating file work and to report when the child's execution environment cannot see it. No provider receives file bytes; the model reads the stored copy with existing file tools on demand.

Default mode, active Plan Mode, and an active goal share ordinary queue, follow-up, and steer attachment admission. /goal create and edit and /plan entry accept the same ordered image and file batches; attachment-bearing control subcommands reject before changing state. Command submissions cite completed staged-file receipts, so generic file bytes are not read or copied again. Session Controller resolves every file receipt to a durable reference before ctx.attachments.admitPromptContent() receives the complete ordered content list; attachment admission persists images and passes file references unchanged. The Host rejects upload completion after the original Agent is disposed and deduplicates prompt retries already identified by rpcId. Prompt receipts remain available through failure recovery, then retire after the accepted queue or history occurrence is observed; command-only receipts remain until Session disposal. Submission echoes register through beginSubmission before serialization, carry mixed attachments through pendingSubmissions, correlate by rpcId, retire once after queue or history observation, and keep drafts on failure. Before submission, the composer presents the ordered batch in one non-wrapping horizontal rail: images use 64 by 64 pixel thumbnails and generic files use 240 by 64 pixel cards with a 16 pixel radius, blue document glyph, filename plus extension and size, and a hover or keyboard removal action. QueueDock uses the same order with compact thumbnails and file cards. After submission, Chat presents the ordered batch in one right-aligned wrapping attachment flow. A lone image retains the large gallery presentation; a message with multiple attachments uses 64 by 64 pixel image tiles and 240 by 64 pixel file cards, so files and images share each row until the available width forces a wrap. Trajectory keeps the user-message preview compact and adds a localized generic-file count. Paperclip, drag, and paste classify by MIME; send waits for every upload. Continuable subagent composers disable attachment intake, and the Host rejects any file block received for a child session.

Alternatives considered

  • Store uploads in the project root (the OpenHands / ChatGPT /mnt/data shape). Rejected: files in the user's tree can be edited, moved, or deleted, which breaks the session log's stable reference, pollutes git status, and requires a workspace to exist. DSH_HOME keeps frozen read-only bytes with content addressing; execution-world visibility is served by the projected path and can gain a copy-into-workspace step later.
  • A dedicated read_file tool. Rejected: the handle line points at an ordinary path, so the existing read tool (and the model's other file tools) already covers it; a second read tool would split model behavior without adding capability.
  • Eager content injection into the prompt. Rejected for files exactly as it was for @ mentions (web file references): relevance is unknown at attach time, and lazy tool reads keep every content access logged.
  • Type whitelist and size caps. Rejected for now: DeepSeek Chat's whitelist exists because its server parses uploads, while this harness only stores bytes and lets the model read them. The streaming carrier and store keep memory proportional to bounded chunks, so a cap would govern disk and transfer policy rather than request safety.
  • A new attachment/file session event. Rejected: the block-inside-user/message pattern images already use satisfies model-visible ⟺ logged without widening the event vocabulary.
  • Block every command while attachments are present. Rejected: /goal objectives and /plan entry need reference files, so a global refusal would leave those files with no command route to the model.
  • Automatically send attachments after any successful command. Rejected: state commands such as /model and /compact do not imply model-visible input. The command producer owns whether an admitted batch becomes a user message.
  • Store objective attachments in the goal domain. Rejected: one ordinary logged user message gives later goal rounds the same history without widening the goal schema or duplicating attachments into round prompts.
  • Consume attachments on any command success. Rejected: /goal pause or /plan off would clear unrelated cards. Consumption follows the producer's explicit success; grammar forms that cannot use attachments return an error.
  • Enforce declarations only in the client. Rejected: direct RPC callers can bypass the composer. CommandRuntime enforces declaration, store availability, same-Session file-receipt resolution, image base64 admission, and image limits before entering the handler.
  • Expose uploads through ctx.connection and pass that object through Session constructors. Rejected: Connection does not own file transfer, and the intermediate object layers do not use the dependency. The independent upload service accepts an explicit Session identity and lets browser and Worker-hosted pages replace the carrier without widening Connection or Session.

Consequences

  • Uploaded bytes are never deleted and carry no size limit, so DSH_HOME growth is unbounded; garbage collection stays deferred with the image-retention question.
  • The projected path is execution-environment-local: a sandboxed or remote filesystem that cannot map DSH_HOME yields an explicit no-path handle, and the model reports that it cannot read the file. A store-backed read fallback is deferred.
  • Staged uploads live in Host memory per Session; a Host restart between upload and send surfaces FILE_NOT_STAGED and the client re-adds the file.
  • The browser-to-Host transfer is one streaming HTTP request without resumable offsets; retrying after connection or Host failure retransmits the file from byte zero.
  • A ReadableStream upload consumes and transfers the stream once; retrying requires a newly created stream, and progress cannot report a total unless the producer supplies length through a future API.
  • The browser upload Worker is emitted from the string form of a self-contained function. Runtime imports require a standalone Worker entry bundled by tsdown.
  • Commands must declare attachment acceptance. Rejected commands preserve the text draft and every attachment card for retry.
  • Subagents do not upload files independently. A main agent delegates with the saved path; only same-machine or inherited-history children whose execution environment maps that path can read it.
  • Uploaded files do not appear in @ completion yet; that integration is a planned later phase.
  • The command registry admits a tagged CommandSubmitAttachment union whose file member carries a staged receipt, then hands handlers one frozen ordered ImageBlock | FileBlock array. The registry never schedules model input; /goal adds one logged follow-up and /plan steers the ordered blocks.
  • Menu popup selection does not submit the composer envelope. Attachments remain visible until the user submits a command line or removes them.
  • Declaration, missing-store, unknown-receipt, and image-limit rejections occur before handler entry. A producer grammar rejection and cancellation after image admission may leave unreferenced content-addressed objects for deferred attachment garbage collection.

Testing

Unit and integration coverage pins verbatim and streamed storage, name sanitization, a declared 2.19 GiB request passing through the streaming bridge without aggregate buffering, raw and RPC wire admission, Blob and transferable ReadableStream carriers, bounded background-upload concurrency, progress and cancellation, cross-Session upload residency, command file-receipt submission without a second byte read, native and PTC projection under read-only and workspace-write permissions, upload staging, ordered queue and steer submission, queue-to-steer conversion, pending echo retirement and failure recovery, active plan and goal intake, /plan and /goal mixed attachments, subagent refusal, composer and Chat attachment layouts, and Trajectory file summaries. The keyless file-upload-round snapshot records the browser upload, model file read, and rendered answer.

The Trajectory reference documents ledger counts and inspector attachment layouts.

The input echo admission reference owns placement-specific retirement and delayed Inbox suppression for submissions with or without attachments.

The composer image upload on add note extends this background-upload queue to the images of an ordinary Session's draft.