Skip to content

Latest commit

 

History

History
271 lines (215 loc) · 13.9 KB

File metadata and controls

271 lines (215 loc) · 13.9 KB

Payload Format

Goals

The project uses a fragment-based payload so the raw artifact content stays in the browser and is not sent to the server during the request.

Payload contents are untrusted user content. Viewers, agents, and automations should render them as data, not treat artifact text as instructions, unless the artifact source is separately trusted.

Fragment shape

#p<payload>   (plain)
#l<payload>   (lz)
#d<payload>   (deflate)
#a<payload>   (arx)
#b<payload>   (arx2)
#c<payload>   (arx3, deprecated emit)
#e<payload>   (arx4, deprecated emit)
#f<payload>   (arx5)

The compact fragment is a single codec tag char followed by the payload. The tag encodes the codec so unsupported formats fail cleanly; the compact tag does not carry a dictionary version — arx-family tags imply the build's current dictionary (the build pins the newest supported version and rejects a newer one). The legacy #agent-render=v1.<codec>.<payload> form (arx-family carry an extra <dictVersion>. segment) still decodes for back-compatibility but is no longer emitted. Fragment URLs can look long because they carry the artifact payload in the browser-only fragment instead of sending it to the host during the page request.

Supported codecs:

  • plain - base64url-encoded JSON
  • lz - lz-string compressed JSON encoded for URL-safe transport
  • deflate - deflate-compressed UTF-8 JSON bytes encoded as base64url
  • arx - domain-dictionary substitution + brotli (quality 11) + binary-to-text encoding. The compact a tag identifies the arx codec but does not carry a dictionary version — it implies the build's current pinned dictionary (the build refuses to decode a forward-incompatible newer dictionary). Four wire shapes are tried and the shortest transport size wins (see computeTransportLength in fragment.ts — non-ASCII Unicode may count longer after percent-encoding): base76 (ASCII-only, 77 fragment-safe chars), base64url (standard RFC 4648 alphabet A-Za-z0-9-_, no padding, prefixed with B. for detection), base1k (Unicode, 1774 chars from U+00A1–U+07FF), and baseBMP (high-density Unicode, ~62k safe BMP code points from U+00A1–U+FFEF, ~15.92 bits/char). BaseBMP produces ~32% fewer characters than base1k and ~60% fewer than base76 for the same compressed bytes. BaseBMP payloads are prefixed with a U+FFF0 marker for detection. The viewer’s arxDecompress auto-detects the wire shape (including the rare case where a base76 length prefix is also B. — it tries base64url first and falls back to base76 if Brotli fails). The substitution dictionary is served at /arx-dictionary.json with a pre-compressed /arx-dictionary.json.br variant; the viewer tries the .br file first on default loads and falls back to JSON. The arx2 overlay dictionary follows the same .br-then-JSON default load pattern.
  • arx2 - tuple-envelope transport + arx2 overlay substitution + the shared arx dictionary + brotli (quality 11) + the same four binary-to-text wire shapes. The compact b tag identifies arx2 but does not carry a dictionary version — it implies the current pinned shared arx dictionary and arx2 overlay. Existing arx links remain valid; async auto-selection keeps arx2 in the pool as the conservative Brotli tuple codec (needed for some CSV regressions).
  • arx3 - deprecated emit. Same compressed bytes as arx2. The only difference was scoring baseBMP by visible character count instead of serialized URL length, so Unicode won artificially and then Discord markdown / WhatsApp percent-encoding detonated the link. Existing #c links still decode. Do not mint new arx3 links.
  • arx4 - deprecated emit. ARX2's tuple/overlay pipeline with Brotli replaced by the deterministic context mixer, plus a prior id char, but it kept arx3's broken visible-length baseBMP policy. Existing #e links still decode. Do not mint new arx4 links.
  • arx5 - ARX 4.5: the sane mixer codec. Same context mixer, priors, and wire shapes as arx4 (arx4-codec.ts), scored with ARX2's honest serialized transport length for every wire including baseBMP. The compact f tag identifies arx5. The payload still carries one extra leading char, the prior id (m, c, j, s, or n); m/c/j need /arx4-priors.json (pre-compressed /arx4-priors.json.br tried first). If the asset is unavailable the encoder falls back to s. Auto-selection prefers arx5, then arx2. It is roughly 100x slower than Brotli, which is why the whole arx family is async-only.

The encoder now also supports a packed wire representation (p: 1) that shortens key names before compression. Packed mode is transport-only; decoded envelopes normalize back to the standard shape.

Fragment payloads are the trusted direct-sharing transport. For public posts, broad group sharing, social surfaces, or corporate proxy/link-scanning environments, prefer self-hosted UUID links so the visible URL is short and stable.

Envelope

{
  "v": 1,
  "codec": "plain",
  "title": "Artifact bundle title",
  "activeArtifactId": "artifact-1",
  "artifacts": [
    {
      "id": "artifact-1",
      "kind": "markdown",
      "title": "Weekly report",
      "filename": "weekly-report.md",
      "content": "# Report"
    }
  ]
}

Packed wire envelopes are also valid on the wire:

{
  "p": 1,
  "v": 1,
  "c": "deflate",
  "t": "Artifact bundle title",
  "a": "artifact-1",
  "r": [
    {
      "i": "artifact-1",
      "k": "markdown",
      "f": "weekly-report.md",
      "c": "# Report"
    }
  ]
}

Packed key map:

  • envelope: codec -> c, title -> t, activeArtifactId -> a, artifacts -> r
  • artifact: id -> i, kind -> k, title -> t, filename -> f, content -> c, language -> l, patch -> p, oldContent -> o, newContent -> n, view -> w

arx2 uses a tuple wire envelope instead of JSON object keys:

  • single artifact: [3, artifactTuple, envelopeTitle?]
  • multi-artifact bundle: [2, [artifactTuple, ...], envelopeTitle?, activeIndex?]
  • artifact tuples use kind codes: m markdown, c code, d diff, s csv, j json
  • trailing optional fields are trimmed; omitted optional slots before later values are encoded as null

Tuple fields:

  • markdown/csv/json: [kindCode, id, content, title?, filename?]
  • code: ["c", id, content, language?, title?, filename?]
  • diff: ["d", id, patch?, oldContent?, newContent?, language?, view?, title?, filename?]

Required support

  • kind
  • optional title
  • optional filename
  • content for markdown, code, csv, and json
  • patch or oldContent plus newContent for diffs

Limits

  • Supported fragment budget: 8,192 decoded visible fragment characters
  • Supported decoded payload budget: 200,000 characters
  • Discord markdown link limit: 2,000 characters for the full formatted [label](url) string
  • Larger payloads should fail with a clear error before rendering
  • Compression is selected automatically across packed/non-packed candidates. Live codecs (arx, arx2, arx5) optimize conservative percent-escaped transport length. Deprecated arx3/arx4 still optimize compact visible length when explicitly requested
  • Default sync codec priority is deflate -> lz -> plain
  • Default async codec priority is arx5 -> arx2 -> arx -> deflate -> lz -> plain
  • Optional budget-aware encoding can target strict limits and returns the shortest fragment when none fit
  • createGeneratedArtifactLink / createGeneratedArtifactLinkAsync return url, markdownLink (ready to paste verbatim in chat), markdownLinkLength, and discordMarkdownLinkWarning so agents do not need to reconstruct [label](url) themselves

When a payload does not fit the fragment budget or the target surface is hostile to long URLs, use UUID mode instead of weakening the fragment protocol. Current UUID mode stores the encoded payload server-side and is not zero-retention.

Codec benchmark

Running npm run bench:codecs checks a fixed corpus across markdown, a real code-bench report, code, diff, CSV, JSON, and multi-artifact bundles. The current committed baseline shows:

  • total arx: 5,544 brotli bytes
  • total arx2: 5,410 brotli bytes
  • total arx3: 5,410 brotli bytes
  • arx2 delta: 2.42% smaller overall
  • arx3 visible delta vs arx2: 60.48% fewer visible fragment characters
  • real code-bench report row: arx2 is 2,984 visible fragment characters; arx3 is 1,142

The gate fails if arx2 is less than 0.5% smaller overall, if arx3 is less than 35% smaller by visible characters overall, or if any individual corpus row regresses by more than 0.5%. The arx3 visible-character row is historical: auto-emit no longer uses that policy. Use npm run bench:codecs:update only when intentionally refreshing the committed baseline.

Chat-safe alphabets

Auto-selection measures every live wire with computeTransportLength in fragment.ts: RFC 3986 unreserved characters plus = stay 1, other ASCII punctuation counts as 3 (percent-escaped), and non-ASCII counts as 6/9/12 by UTF-8 width. That is the honest Discord/WhatsApp cost.

Researched surface constraints:

Surface What survives What detonates
Discord markdown [label](url) RFC 3986 unreserved A-Za-z0-9-._~ and =. 2,000-character limit on the whole formatted link. Unicode is canonicalized and percent-encoded (a BMP char becomes 9 characters). ) closes the destination.
Discord bare URL Same unreserved set. Unicode is still percent-encoded in the client. Dense BMP/base1k fragments explode past the message limit.
WhatsApp Bare https:// URLs only; no [label](url). Unreserved ASCII in the path/fragment is typically kept. *bold*, _italic_, and ~strike~ are formatting markers if URL detection fails. Unicode is often mangled or dropped from the tappable range. Trailing .,!) is stripped by linkifiers.
Browser fragment (WHATWG) Unreserved plus most sub-delims. Fragment percent-encode set is only C0, space, `"<>``. Does not predict chat apps. A browser-safe Unicode fragment is not Discord-safe.

Largest alphabet that is safe on both Discord markdown and WhatsApp without mangling: RFC 3986 unreserved A-Za-z0-9-._~ (66 chars). Adding = (already treated as chat-safe) makes 67. That is only ~0.7–1.2% denser than base64url's 64-character A-Za-z0-9-_.

Why a new base66/67 wire is not worth it:

  • base64url already uses 64 of those 66 characters and is proven in production #bB. / #fB. links
  • base76's extra punctuation (!$*()',;:@/) is either fatal ()) or 3x after chat escaping, so honest scoring already rejects it
  • base1k/baseBMP look shortest by visible character count and then explode 3–9x when a chat client percent-encodes them

arx5 therefore keeps the existing four wires and lets honest transport length pick. In practice that is base64url (or base76 when its punctuation does not inflate).

Active artifact behavior

The envelope can carry multiple artifacts. The shell uses activeArtifactId to decide which artifact opens first, and switching artifacts updates the fragment so the shared link stays truthful.

Internal viewer navigation, such as moving between files inside a multi-file diff, does not mutate the fragment. The fragment is reserved for payload transport and bundle-level state only.

Examples

Sample envelopes live in src/lib/payload/examples.ts for local development and documentation. The homepage uses precomputed sample link data that is checked against those generated examples in tests, keeping the large sample strings out of the initial shell chunk.

Markdown artifact example

{
  "v": 1,
  "codec": "plain",
  "title": "Maintainer kickoff",
  "activeArtifactId": "roadmap",
  "artifacts": [
    {
      "id": "roadmap",
      "kind": "markdown",
      "title": "Sprint roadmap",
      "filename": "roadmap.md",
      "content": "# Sprint roadmap\n\n- Render markdown directly in the viewer"
    }
  ]
}

Markdown artifacts use the content field and currently support client-side clipboard copy, file download, and browser print-to-PDF from the viewer shell. Mermaid fenced code blocks (```mermaid) within markdown content are rendered as interactive diagrams.

Code artifact example

{
  "v": 1,
  "codec": "plain",
  "title": "Viewer bootstrap",
  "activeArtifactId": "viewer-shell",
  "artifacts": [
    {
      "id": "viewer-shell",
      "kind": "code",
      "title": "viewer-shell.tsx",
      "filename": "viewer-shell.tsx",
      "language": "tsx",
      "content": "export function ViewerShell() {\n  return <main />;\n}"
    }
  ]
}

Code artifacts use the same content transport, plus optional language and filename hints for syntax-aware rendering, download naming, and clipboard copy of the source text.

Diff artifact example

{
  "v": 1,
  "codec": "plain",
  "title": "Patch review",
  "activeArtifactId": "patch",
  "artifacts": [
    {
      "id": "patch",
      "kind": "diff",
      "title": "hello.ts diff",
      "filename": "hello.patch",
      "patch": "diff --git a/hello.ts b/hello.ts\n--- a/hello.ts\n+++ b/hello.ts\n@@ -1 +1 @@\n-console.log('hello')\n+console.log('hello, world')\n",
      "view": "split"
    }
  ]
}

Real diff artifacts can contain multiple diff --git sections inside one patch string. The viewer parses that unified patch into a sequence of file diffs and preserves per-file boundaries.

CSV artifact example

{
  "v": 1,
  "codec": "plain",
  "title": "Metrics snapshot",
  "activeArtifactId": "metrics",
  "artifacts": [
    {
      "id": "metrics",
      "kind": "csv",
      "filename": "metrics.csv",
      "content": "artifact,kind,summary\nroadmap,markdown,launch-ready"
    }
  ]
}

JSON artifact example

{
  "v": 1,
  "codec": "plain",
  "title": "Artifact manifest",
  "activeArtifactId": "manifest",
  "artifacts": [
    {
      "id": "manifest",
      "kind": "json",
      "filename": "manifest.json",
      "content": "{\n  \"ready\": true\n}"
    }
  ]
}

Malformed JSON should still use kind: "json"; the viewer will show the parse error and a raw fallback instead of crashing.