LearnNewsExamplesServices
Frontmatter
id15636
titleFileUpload: pluggable upload-transport seam (XHR default)
stateClosed
labels
enhancementdeveloper-experienceai
assigneesneo-gpt-emmy
createdAtJul 21, 2026, 2:55 PM
updatedAtJul 24, 2026, 8:37 PM
githubUrlhttps://github.com/neomjs/neo/issues/15636
authorneo-kimi-phoebe
commentsCount0
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtJul 24, 2026, 8:37 PM

FileUpload: pluggable upload-transport seam (XHR default)

Closed Backlog/active-chunk-8 enhancementdeveloper-experienceai
neo-kimi-phoebe
neo-kimi-phoebe commented on Jul 21, 2026, 2:55 PM

Context

Neo.form.field.FileUpload speaks exactly one transport: XHR multipart (src/form/field/FileUpload.mjs:439-490), with fetch hardcoded for document delete (src/form/field/FileUpload.mjs:633) and scan-status polling (src/form/field/FileUpload.mjs:658). Progress, abort, and completion are welded to XHR events.

That is fine for apps with an HTTP backend. It excludes a growing class of Neo apps whose only backend channel is a message transport — websocket or worker message port — where the "backend" is an agent-side service and there is no HTTP plane at all. The Agent Harness direction (ADR 0020, #13012) increases the share of apps in exactly that situation: a harness-hosted app talking to an agent-side capability service cannot upload a file without standing up an HTTP endpoint it otherwise does not need.

Live latest-open sweep: checked latest 20 open issues at 2026-07-21 ~13:00Z; no equivalent found. A2A in-flight claim sweep (last 60 min): no competing claim.

The Problem

The field's state machine — ready → starting → uploading → processing → downloadable / not-downloadable → deleted, plus upload-failed / scan-failed — is transport-agnostic in shape, but every byte-moving step is bound to one implementation:

  • upload(file) constructs XMLHttpRequest + FormData inline (src/form/field/FileUpload.mjs:439-490).
  • deleteDocument() calls fetch(me.documentDeleteUrl) (src/form/field/FileUpload.mjs:633).
  • checkDocumentStatus() calls fetch(me.documentStatusUrl) (src/form/field/FileUpload.mjs:658).
  • Progress is XHR upload.progress events; abort is xhr.abort().

An app that wants upload-over-websocket today must fork the field or reimplement its UI/state machine — both wrong, since the state machine, drag-and-drop surface, validation (types, maxSize), and error vocabulary are the reusable parts.

The Architectural Reality

Precedent for a sub-namespace under a field: src/form/field/trigger/ (Base.mjs + concrete triggers). The same shape fits here: the field keeps state, vdom, and validation; a transport hierarchy owns the byte pipe.

The current implicit server contract (POST multipart → JSON with documentId; DELETE on discard; optional scan-status poll) is the XHR transport's contract — it should live on that transport, not on the field.

The Fix

Introduce a pluggable upload-transport seam:

  1. New sibling namespace src/form/field/fileUpload/ with:
    • Transport.mjs — abstract base defining the contract: upload({file, onProgress}) → Promise<{documentId, …}>, abort(), deleteDocument(documentId), and optional checkDocumentStatus(documentId).
    • Xhr.mjs — the default transport: today's XHR/fetch logic extracted byte-for-byte, honoring uploadUrl, documentDeleteUrl, documentDeleteMethod, documentStatusUrl, headers, and the beforeRequest header-injection event.
  2. New field config uploadTransport (class or instance; default Xhr). The field delegates upload / abortUpload / deleteDocument / checkDocumentStatus to it and keeps everything else.
  3. Progress and abort map onto the contract: transports report progress via callback and must honor abort() semantics.

The WebSocket chunked transport itself (begin/chunk/commit framing, binary frames where supported) is a follow-up ticket once the seam exists.

Contract Ledger Matrix

Target Surface Source of Authority Proposed Behavior Fallback Docs Evidence
FileUpload.upload() mechanics current XHR impl at src/form/field/FileUpload.mjs:439 delegated to uploadTransport default Xhr transport = current behavior JSDoc on config existing field examples green
new config uploadTransport this ticket class or instance of fileUpload/Transport fileUpload/Xhr JSDoc + field docs unit spec
fileUpload/Transport contract this ticket abstract base, no impl subclass required JSDoc contract spec with a test-double transport
uploadUrl / documentDeleteUrl / documentStatusUrl configs current field consumed by Xhr transport, unchanged unchanged unchanged existing examples green

Decision Record impact

none — Body-side component API seam; no ADR governs form-field transports. Aligned with (not governed by) the ADR 0020 harness direction as motivation.

Acceptance Criteria

  • Default-config FileUpload behaves identically to today (state transitions, progress, abort, delete, error paths) — existing examples and component coverage green
  • src/form/field/fileUpload/Transport.mjs + Xhr.mjs exist; XHR logic fully extracted from the field
  • A test-double transport can drive upload → progress → done, abort, and delete without any XHR
  • Unit specs cover the seam contract and the Xhr transport's preserved behavior
  • JSDoc complete on new classes and the new config (Contextual Completeness gate)

Out of Scope

  • The WebSocket transport implementation (separate follow-up ticket)
  • Any server-side reference upload service or protocol specification
  • Changes to the field's state vocabulary, validation, or drag-and-drop surface

Avoided Traps / Gold Standards Rejected

  • Welding ws-chunk framing directly into the field — binds one app's protocol into a shared component; the seam keeps framing per-transport.
  • Mandating base64 in the contract — the transport decides framing; a ws transport may use binary frames.
  • Widening to a generic "upload service" abstraction — the field's need is a byte pipe, not a service model; YAGNI until the second transport lands.

Related

  • #13012 (Agent Harness epic — the motivating app class)
  • ADR 0020 (learn/agentos/decisions/0020-*.md) — harness direction; motivation, not authority

Origin Session ID: d8a51237-4fcc-4171-8071-a391da0be361

Retrieval Hint: "FileUpload XHR transport seam pluggable upload harness"

tobiu referenced in commit 1804b38 - "feat(form): add pluggable FileUpload transport (#15636) (#15833)" on Jul 24, 2026, 8:37 PM
tobiu closed this issue on Jul 24, 2026, 8:37 PM