fix(cli): pull verifies what it wrote, and says when the bytes came back different - #91
Merged
Merged
Conversation
…ack different
Spec §6: "Readers SHOULD verify it and MUST report, not repair, a mismatch."
`verifyIntegrity` existed and `replay` called it. `pull` wrote a run to disk
and said `pull.done`.
So a download that arrived altered landed silently, and the first sign of it
would be a replay that diverged for a reason nobody could name. The zip's own
CRC catches a flipped bit; it cannot catch an archive that is internally
consistent and whose events simply are not the ones its manifest attests to,
which is the shape a storage bug on the far side takes.
`pull` now verifies the staged copy against the root in its own manifest —
before the swap, never after, because what fails verification must not be what
replaced a run `--force` merely asked to update. Verified end to end against a
gateway serving a well-formed archive with a mismatched root: the pull is
refused and the destination is untouched.
The second half is the round-trip nobody could explain. Push a run, pull it
back, and the bytes differ: a gateway is free to store the trace however it
likes, and this one re-serialises it — Go's `encoding/json` sorts a map's keys
and escapes `<`, `>` and `&` — then recomputes the root over its own bytes.
Both copies are internally consistent and neither is wrong. What is lost is the
one thing a digest is for: it no longer tells you the two are the same run.
Nothing said so. Now, when a local run of the same id is there to compare
against, pull reports which it is:
warn pull.reserialized run=… local=5a25766625… pulled=4d106ed7b6…
why="same events, different bytes — the gateway stores its own
serialisation and recomputes the root over it"
effect="the two copies can no longer be compared by digest, though
nothing was lost"
and `pull.differs`, loudly, when the events themselves are not the same. The
comparison sorts keys deeply at read time and touches no file: canonicalising
what this writer puts on disk would mean adopting one JSON encoder's escaping
rules as the format's own, which is a decision about the spec rather than a bug
fix, and nothing else in this repo re-serialises an event — `scrub` rewrites
`events.jsonl` by substituting text in the original lines for exactly that
reason.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
🐳 OrcaCode Review
✅ No findings — nothing to flag in this PR. Great work!
OrcaCode Review — Route Smarter. Ship Safer. Spend Less.
Engine-reported: 545 calls · 49.5M tokens · 99% cached
❤️ Share · Install OrcaCode Review
Free on GitHub — the review runs on your own OrcaRouter key. If it helped, a shout-out goes a long way.
Share: X · Reddit · LinkedIn
Follow: X · Discord · LinkedIn · OrcaRouter
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Orca-Code-Review — push 1
✅ no blocking findings
Two halves of the same question: is the run I just pulled the run that was pushed?
pullnever checkedSpec §6 — "Readers SHOULD verify it and MUST report, not repair, a mismatch."
verifyIntegrityexists and
replaycalls it.pullwrote a run to disk and saidpull.done.The zip's own CRC catches a flipped bit. It cannot catch an archive that is internally consistent
and whose events simply are not the ones its manifest attests to — which is the shape a storage
bug on the far side takes. That landed silently, and the first sign would be a replay that
diverged for a reason nobody could name.
Now verified against the root in the manifest that arrived with it, on the staged copy, before
the swap — what fails verification must not be what replaced a run that
--forcemerely askedto update:
The round trip nobody could explain
Push a run, pull it back, and the bytes differ. A gateway is free to store the trace however it
likes, and this one re-serialises it — Go's
encoding/jsonsorts a map's keys and escapes<,>and&— then recomputes the root over its own bytes.Both copies are internally consistent. Neither is wrong. What is lost is the one thing a digest is
for: it no longer tells you the two are the same run, so a real corruption and a benign re-encoding
look identical.
Nothing said so. Now, when a local run of the same id is there to compare against:
and
pull.differs, loudly, when the events themselves are not the same.What this deliberately does not do
It does not canonicalise what the writer puts on disk. Making the two roots equal means matching
Go's escaping of
<>&byte for byte — no canonicalisation standard does that (RFC 8785 sorts keysand leaves escaping minimal), so it would be adopting one encoder's quirk as the format's own.
That is a decision about the spec, not a bug fix.
It would also cut against the grain of the repo: nothing here re-serialises an event.
scrubrewrites
events.jsonlby substituting text inside the original lines rather than parsing andre-emitting, and #35 put it plainly — "the only correct thing to send is the bytes already on
disk." The comparison added here sorts keys deeply at read time and writes nothing.
Happy to follow up with the canonical-writer change if that is the call; it is small, and the
byte accounting is already worked out (key order plus 9 escapes, +45 bytes on the run I measured).
Verification
Three cases, end to end against the live gateway and a local stand-in:
pull.reserialized, both roots namedBoth tests fail without the change —
expected undefined to be definedfor the report, and thecorrupt pull silently succeeding for the other.
Suite: 10 failed / 2416 passed / 40 skipped — the same 10 pre-existing Windows platform failures
as
main, two more passing tests.prettier --check,tsc --build,scripts/conformance.mjs(61 events, 0 failures) and
scripts/fidelity.mjs --check(1 recording, 0 regressions) all clean.🤖 Generated with Claude Code