Skip to content

Harden legacy fragment decoding (zlib-wrapped deflate + arx dictionary drift) - #72

Merged
baanish merged 4 commits into
mainfrom
codex/codec-backcompat
Jun 19, 2026
Merged

baanish merged 4 commits into
mainfrom
codex/codec-backcompat

Conversation

@baanish

@baanish baanish commented Jun 19, 2026

Copy link
Copy Markdown
Owner

Stacked on #71. Two independent back-compat fixes surfaced by decoding a corpus of real, sha256-verified agent-render links (not synthetic): of 12 verbatim legacy fragments, 5 failed to decode — 1 from a real bug fixed here, 3 from dictionary drift addressed here, and 1 from source-side truncation (not a code issue). Each fix is its own commit.

1. Decode legacy zlib-wrapped deflate fragments

An older encoder version emitted zlib-wrapped deflate (78 da …), but the current decoder uses fflate's raw inflateSync, so those links fail with invalid-json. One real verified link (a "cline/kanban analysis" envelope) is a complete, valid zlib stream that the current build rejects.

Decode now tries raw inflate first (current output is never at risk of header mis-detection) and falls back to unzlibSync for the zlib-wrapped legacy form. Encoding is unchanged (still raw deflate) — decode-only, wire-safe.

2. Pin arx dictionary content to its version; flag mismatch on decode

Three real arx/arx2 links (dictVer=1) decode to garbage: brotli and the dictionary substitution run, but public/arx-dictionary.json content drifted from what those fragments were encoded with while its version stayed 1. Old links silently mis-decode with no signal.

  • tests/arx-dictionary-pin.test.ts pins each dictionary's version to its canonical content hash. Changing slots now fails CI unless the version is bumped in the same commit, so content and version can't drift apart silently again.
  • decodeFragmentAsync now appends a dictionary-version hint to the error when an arx payload fails to decode or isn't a valid envelope, instead of a generic message. (Already-encoded drifted links can't be recovered without the original dictionary bytes; this makes the failure legible and stops future drift.)

Test plan

Note (not in this PR)

The same real corpus retired the trained-compression-dictionary idea: the 112 KB dict that gave +10.5% on the synthetic held-out set gives −0.74% on these real payloads — it was overfit to the generator. Combined with the drift hazard above, a trained compression dictionary is not worth shipping. Findings recorded in memory.

🤖 Generated with Claude Code

@coderabbitai

coderabbitai Bot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e33033d9-5fb9-45e8-97d9-ab12ee617b9f

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/codec-backcompat

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jun 19, 2026

Copy link
Copy Markdown

Deploying agent-render with  Cloudflare Pages  Cloudflare Pages

Latest commit: 9697483
Status: ✅  Deploy successful!
Preview URL: https://73bd4770.agent-render.pages.dev
Branch Preview URL: https://codex-codec-backcompat.agent-render.pages.dev

View logs

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 70a65f0e34

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +551 to +555
const resolved = resolveEnvelope(parsed, header.fragmentLength);
if (!resolved.ok && resolved.code === "invalid-envelope" && (codec === "arx" || codec === "arx2" || codec === "arx3")) {
// An ARX payload that decoded but is not a valid envelope almost always means the active
// dictionary differs from the one it was encoded with (see tests/arx-dictionary-pin.test.ts).
return { ...resolved, message: `${resolved.message} It may have been encoded with a different ARX dictionary version.` };

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject mismatched ARX dictionary versions

When a versioned legacy ARX fragment carries a dictionary version that differs from the active dictionary, this new logic only adds the hint after decode/validation has already failed. decodeArxFragmentPayload parses the numeric prefix but never checks it against the loaded dictionary version, so after a future dictionary bump an older fragment that still decompresses into a syntactically valid envelope can still be accepted and silently show garbled artifact content instead of being rejected as a version mismatch.

Useful? React with 👍 / 👎.

@kilo-code-bot

kilo-code-bot Bot commented Jun 19, 2026

Copy link
Copy Markdown

Code Review Summary

Status: No New Issues Found | Recommendation: Merge

Reviewed the incremental commit 96974833 ("Harden arx dictionary loading (adversarial review)"), decode/encode-safety hardening on top of the prior codec-backcompat work. No new issues found.

Incremental changes verified

  • Transient dictionary-load retry fix (src/lib/payload/fragment-arx.ts:65): loadArxDictionary/loadArx2OverlayDictionary resolve -1 (not reject) on a transient fetch failure and fall back to the built-in dictionary. The prior ensureArx* helpers cached that resolved promise for the page lifetime, so the external endpoint was never retried. The helpers now null the cached promise on a -1 result so a later call retries once the endpoint recovers (covered by tests/arx-dict-load-retry.test.ts); the current call still proceeds on the built-in fallback (intended degraded behavior). Concurrent callers await the same captured promise object, so nulling the module variable mid-flight is race-free.
  • Forward-incompatibility guard (src/lib/payload/fragment-arx.ts:47): compact arx tags (a/b/c) carry no dictionary version, so decoding against a dictionary NEWER than the build supports could silently produce a structurally-valid-but-wrong envelope. assertArx*NotNewerThanExpected rejects any active version > 1 — the pinned public/arx-*.json and built-in overlay are all v1, the built-in base is v0, every value ≤ the cap. The guard runs after both the fetched and sync-injected load paths and on the encode (candidate-build) path, so a CDN/asset skew can't slip through either way. Throws are caught in decodeFragmentAsync (src/lib/payload/fragment.ts:550) and surfaced as { ok: false }, matching tests/arx-dict-version-guard.test.ts. Constants cross-checked against tests/arx-dictionary-pin.test.ts.

Notes

  • The guard is deliberately asymmetric (reject newer, allow older): decoding with an OLDER dictionary (built-in v0 on a transient failure, or a stale CDN dictionary) leaves unsubstituted control bytes that fail JSON.parse, so it hard-fails rather than silently mis-decoding. Only a NEWER dictionary risks a valid-but-wrong envelope — exactly the case the guard closes.
  • Once a forward-incompatible dictionary is loaded, the build refuses all arx decode for the page lifetime (the resolved promise is cached, not re-nulled); a refresh re-fetches. Intended safe behavior for an operational version skew.

Remaining (non-blocking, carried forward)

The prior suggestion at src/lib/payload/fragment.ts:565 is unaffected by this commit (it is not in the incremental diff) and remains open: decodeArxFragmentPayload still parses parsedDictVersion from the legacy #agent-render=v1.arx.<dictVersion>.… remainder (src/lib/payload/fragment-arx.ts:123) but never compares it to the loaded dictionary version. The new compact-format guard added here is complementary (it covers the version-less compact tags); a future dictionary bump could still let a legacy fragment with a mismatched embedded version decode into a garbled envelope. Forward-looking hardening, not a regression — does not block merge.

Files Reviewed (4 files)
  • src/lib/payload/fragment-arx.ts
  • tests/arx-codec.test.ts
  • tests/arx-dict-load-retry.test.ts
  • tests/arx-dict-version-guard.test.ts
Previous Review Summaries (2 snapshots, latest commit 7e589e5)

Current summary above is authoritative. Previous snapshots are kept for context only.

Previous review (commit 7e589e5)

Status: No New Issues Found | Recommendation: Merge

The branch history was rewritten (force push) since the prior review, so this is a full re-review of all changed files. The new commit 7e589e5b is a clean follow-up that resolves two of the three prior external review comments and introduces no new issues.

Incremental changes verified

  • inflateDeflatePayload (src/lib/payload/fragment.ts:145) now re-throws the original raw-inflate error when the zlib-wrapped fallback also fails, instead of surfacing a misleading zlib error. This directly resolves the prior "silent error discard" nit. The single decode call site (decodePayload deflate case, line 166) covers both the sync and async decoders, so no path was missed.
  • tests/arx-dictionary-pin.test.ts adds a note that arx3 reuses public/arx-dictionary.json, resolving the prior "arx3 not covered" nit.

Remaining (non-blocking, already commented inline)

One prior suggestion remains open at src/lib/payload/fragment.ts:565: proactively rejecting arx fragments whose embedded dictVersion differs from the loaded dictionary, rather than only appending a hint after a failed decode. Confirmed accurate — decodeArxFragmentPayload parses parsedDictVersion (src/lib/payload/fragment-arx.ts:232) but never compares it to the loaded dictionary version, so a future dictionary bump could let an older fragment decode into a syntactically-valid-but-garbled envelope. This is a forward-looking hardening improvement, not a regression introduced by this decode-only PR, so it does not block merge.

Notes

  • Encoder is unchanged (still raw deflateSync); the zlib fallback is decode-only and wire-safe. Current output decodes via the first inflateSync attempt and is never at risk.
  • The arx dictionary-version hints are message-only; resolveEnvelope/normalizeEnvelope always set a message on the invalid-envelope path, so ${resolved.message} never renders undefined.
  • arx3 is correctly included in all arx-family codec checks (it is a registered codec in schema.ts).
Files Reviewed (3 files)
  • src/lib/payload/fragment.ts
  • tests/arx-dictionary-pin.test.ts
  • tests/fragment.test.ts

Previous review (commit 70a65f0)

Status: No Issues Found | Recommendation: Merge

This PR is a decode-only, wire-safe back-compat fix with two independent changes plus tests. The encoder is unchanged (still raw deflate), so no existing links are affected and no new attack surface is introduced — decoded output still flows through JSON.parse, the decoded-size limit, and envelope validation.

Key checks:

  • inflateDeflatePayload tries raw inflateSync first (exact inverse of the current deflateSync encoder, so current output is never at risk) and falls back to unzlibSync only on throw. If both throw, the existing try/catch in decodeFragment/decodeFragmentAsync maps it to invalid-json — no uncaught exception.
  • The arx dictionary-version hints are message-only. The resolveEnvelope invalid-envelope path always sets a message, so ${resolved.message} never renders undefined.
  • arx3 is correctly included in the new arx-family checks alongside arx/arx2 (it is a registered codec in schema.ts).
  • The dictionary pin test references real files (public/arx-dictionary.json, public/arx2-dictionary.json, both version: 1) and uses canonical re-serialization so formatting changes don't false-trip.
Files Reviewed (3 files)
  • src/lib/payload/fragment.ts
  • tests/arx-dictionary-pin.test.ts
  • tests/fragment.test.ts

Reviewed by GLM-5.2 · Input: 182.8K · Output: 18.7K · Cached: 330.1K

@greptile-apps

greptile-apps Bot commented Jun 19, 2026

Copy link
Copy Markdown

Greptile Summary

Two independent back-compat fixes for legacy ARX fragment decoding: (1) inflateDeflatePayload now tries raw deflate first then falls back to unzlibSync for zlib-wrapped streams emitted by an older encoder, and (2) fragment-arx.ts adds a hard forward-incompatibility guard that rejects any active dictionary whose version exceeds what the build expects, preventing silent mis-decodes from CDN/asset skew.

  • zlib fallback (fragment.ts): raw inflate is tried first; zlib is a decode-only fallback, encoding is unchanged. A new test builds a zlib-wrapped fragment by hand and verifies the round-trip.
  • version guard (fragment-arx.ts): EXPECTED_ARX_DICTIONARY_VERSION and EXPECTED_ARX2_OVERLAY_VERSION constants pin the supported version; both ensureArxDictionaryLoaded helpers now always run the assertion (fetch path and sync-injected path). The -1/fallback resolve path now clears the cached promise so a later decode can retry the external endpoint.
  • CI pin (arx-dictionary-pin.test.ts): canonically hashes both dictionary files and pins version + sha256; changing slots without bumping the version now fails CI immediately.

Confidence Score: 5/5

Safe to merge — both fixes are decode-only, encoding is unchanged, and the version guard correctly hard-fails rather than silently mis-decoding.

Both changes are well-scoped and well-tested. The zlib fallback is decode-only and does not touch the encode path, so current links are unaffected. The version guard adds a new rejection path for a condition (active dictionary newer than build expects) that previously caused silent mis-decodes; that is strictly safer than before. The dictionary-pin CI test closes the content-drift gap that caused the original corpus failures. No existing links are invalidated.

src/lib/payload/fragment.ts — the version guard's thrown exception reaches the generic catch and is surfaced as invalid-json with a hint that describes the wrong failure direction; worth a follow-up to preserve the original assertion message.

Important Files Changed

Filename Overview
src/lib/payload/fragment-arx.ts Adds EXPECTED_ARX_DICTIONARY_VERSION / EXPECTED_ARX2_OVERLAY_VERSION constants and assertion helpers; restructures ensureArxDictionaryLoaded/ensureArx2DictionariesLoaded to (a) clear the cached promise on -1/fallback responses to enable retry, and (b) always run the version guard regardless of whether the dictionary was fetched or sync-injected.
src/lib/payload/fragment.ts Adds inflateDeflatePayload with raw-then-zlib fallback for legacy zlib-wrapped deflate links; augments decodeFragmentAsync error handling to append a dictionary-version hint for arx codecs — but when the version guard throws, the original descriptive error is replaced with the generic hint text.
tests/arx-codec.test.ts Updates the dictionary-version skew test to assert hard-fail (ok: false) when active dictionary is newer than build supports, replacing the previous expectation that decoding would succeed with any version.
tests/arx-dict-load-retry.test.ts New test: verifies that a -1 resolve from loadArxDictionary clears the cached promise so subsequent decodes retry the fetch instead of reusing a poisoned no-op promise.
tests/arx-dict-version-guard.test.ts New test: exercises the forward-incompatibility guard end-to-end — encode with v1, inject v2 dict, confirm hard-fail, restore v1, confirm recovery; also covers the arx2 overlay dictionary guard.
tests/arx-dictionary-pin.test.ts New CI pin test: reads and re-serializes each dictionary file, hashes the canonical JSON, and asserts both version and content hash match the pinned values; includes a note that arx3 shares arx-dictionary.json.
tests/fragment.test.ts Adds a round-trip test that manually constructs a zlib-wrapped deflate fragment (the legacy encoder format) and verifies it decodes cleanly through inflateDeflatePayload's fallback path.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[decodeFragmentAsync] --> B{codec arx/arx2/arx3?}
    B -- no --> C[decodePayload deflate path]
    C --> D{inflateDeflatePayload}
    D --> E[inflateSync raw deflate]
    E -- success --> F[strFromU8 → JSON.parse]
    E -- throws --> G[unzlibSync legacy zlib fallback]
    G -- success --> F
    G -- throws --> H[re-throw original raw-inflate error]
    B -- yes --> I[decodeArxFragmentPayload]
    I --> J[ensureArxDictionariesLoaded]
    J --> K{isExternalDictionaryLoaded?}
    K -- no --> L[loadArxDictionary resolves version or -1]
    L -- version lt 0 --> M[clear cached promise fallback dict active]
    L -- version ≥ 0 --> N[keep cached promise]
    M --> O[assertVersion ≤ EXPECTED]
    N --> O
    K -- yes --> O
    O -- version gt EXPECTED --> P[throw forward-incompat error]
    O -- version OK --> Q[arxDecompress / arx2 / arx3]
    P --> R[caught → ok:false code: invalid-json + hint]
    Q -- success --> S[resolveEnvelope]
    S -- invalid-envelope + arx --> T[append dict-version hint to message]
    S -- ok --> U[return ok:true envelope]
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A[decodeFragmentAsync] --> B{codec arx/arx2/arx3?}
    B -- no --> C[decodePayload deflate path]
    C --> D{inflateDeflatePayload}
    D --> E[inflateSync raw deflate]
    E -- success --> F[strFromU8 → JSON.parse]
    E -- throws --> G[unzlibSync legacy zlib fallback]
    G -- success --> F
    G -- throws --> H[re-throw original raw-inflate error]
    B -- yes --> I[decodeArxFragmentPayload]
    I --> J[ensureArxDictionariesLoaded]
    J --> K{isExternalDictionaryLoaded?}
    K -- no --> L[loadArxDictionary resolves version or -1]
    L -- version lt 0 --> M[clear cached promise fallback dict active]
    L -- version ≥ 0 --> N[keep cached promise]
    M --> O[assertVersion ≤ EXPECTED]
    N --> O
    K -- yes --> O
    O -- version gt EXPECTED --> P[throw forward-incompat error]
    O -- version OK --> Q[arxDecompress / arx2 / arx3]
    P --> R[caught → ok:false code: invalid-json + hint]
    Q -- success --> S[resolveEnvelope]
    S -- invalid-envelope + arx --> T[append dict-version hint to message]
    S -- ok --> U[return ok:true envelope]
Loading

Fix All in Codex

Reviews (3): Last reviewed commit: "Harden arx dictionary loading (adversari..." | Re-trigger Greptile

Comment thread src/lib/payload/fragment.ts
Comment thread tests/arx-dictionary-pin.test.ts
baanish and others added 3 commits June 19, 2026 21:24
An older encoder version emitted zlib-wrapped deflate, but the current
decoder uses fflate's raw `inflateSync`, so those historical shared links
fail with `invalid-json`. Verified against a real sha256-checked link
(`#agent-render=v1.deflate.eNqN...`, a "cline/kanban analysis" envelope)
that the current decoder rejects but is a complete, valid zlib stream.

Decode now tries raw inflate first (so current output is never at risk of
header mis-detection) and falls back to `unzlibSync` for the zlib-wrapped
legacy form. Encoding is unchanged (still raw deflate); this is decode-only
and wire-safe.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Real sha256-verified arx/arx2 links (dictVer=1) decode to garbage: brotli
and the dictionary substitution run, but produce a non-envelope object. The
`public/arx-dictionary.json` content drifted from what those fragments were
encoded with while its `version` field stayed 1, so old links silently
mis-decode — a back-compat hazard with no signal.

- tests/arx-dictionary-pin.test.ts pins each dictionary's `version` to its
  canonical content hash. Changing slots now fails CI unless the version is
  bumped in the same commit, so content and version can never drift apart
  silently again.
- decodeFragmentAsync now appends a dictionary-version hint to the error when
  an arx payload fails to decode or isn't a valid envelope, instead of a
  generic invalid-json message.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…reuse

- inflateDeflatePayload: when the zlib-wrapped fallback also fails, re-throw the
  original raw-inflate error instead of a misleading zlib error, so genuinely
  corrupt deflate surfaces an accurate failure (greptile).
- arx-dictionary-pin test: document that arx3 reuses arx-dictionary.json, so the
  pin also covers arx3 and a future arx3-specific dictionary needs its own entry.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@baanish
baanish force-pushed the codex/codec-backcompat branch from 70a65f0 to 7e589e5 Compare June 19, 2026 15:56
Addresses two findings from an adversarial review of the stack:

- Transient dictionary-load failures were cached for the page lifetime.
  loadArxDictionary() resolves -1 (it falls back to the built-in dictionary
  instead of rejecting), so the ensure helpers treated a failed external fetch
  as "loaded" and never retried. Now a -1 result clears the cached promise: the
  current call still proceeds on the built-in fallback, but a later call retries
  the external dictionary once the endpoint recovers.

- Compact arx links carry no dictionary version (the tag implies the current
  dictionary), so decoding against a skewed dictionary (CDN/asset split or a
  future version bump) could silently produce a structurally-valid-but-wrong
  envelope. Decode now rejects any active dictionary NEWER than the build
  supports; the built-in (v0) and current (v1) dictionaries remain usable. A
  dictionary version bump is documented as a wire change that also needs new
  compact tags.

Tests: load-retry (transient -1 then recovery), version-skew guard for base and
overlay dictionaries, and the prior version-difference test updated to assert
the new hard-fail behavior.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@baanish
baanish changed the base branch from codex/compact-header to main June 19, 2026 16:48
@baanish
baanish merged commit a7b8564 into main Jun 19, 2026
10 checks passed
@baanish
baanish deleted the codex/codec-backcompat branch June 19, 2026 16:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant