Skip to content

fix: keep shallow re-export and parent lookups working on inconsistent stored data - #1172

Merged
zxch3n merged 3 commits into
mainfrom
shallow-export-fallback-and-resolver-err
Sep 30, 2026
Merged

zxch3n merged 3 commits into
mainfrom
shallow-export-fallback-and-resolver-err

Conversation

@zxch3n

@zxch3n zxch3n commented Sep 30, 2026 •

Copy link
Copy Markdown
Member

Summary

Two follow-ups from reviewing the 1.16.4 release PR (#1121). Both turn a hard failure on bad stored data into a recoverable one.

1. Export an unprunable shallow root verbatim (1febed11)

Re-exporting a shallow doc at its own root (#1123) filters the cached root state. When the state was inconsistent (a container's header parent does not match the container that references it), the export returned an error. The error was not cached, so every export({ mode: "snapshot" }) redid the walk and failed again, and only fork() still worked. Documents already hit by the #1161 family can hold such a root.

Now the error is logged and the root is exported verbatim, as before #1123. Nothing referenced is dropped, and the fallback is memoized like a successful check.

2. Report an unparsable change block instead of panicking (85e8dabb, fc2a196e)

ChangeStore::creator_resolver (#1159) panicked when a lazily loaded block could not be parsed. It runs under the state lock from is_deleted / has_container / get_path, which return bool / Option, so there is no Err to return from them.

  • Every reader of the change store records a block it cannot decode or parse (ChangeStore::parse_failures), not only the resolver. The resolver answers CreatorOp::Corrupt, treated like Absent for that lookup.
  • checkout, diff, revert_to, import and export (so also fork_at and merge) return DecodeError("cannot parse change block ...") once a block is recorded. export checks again when it is done, so it cannot return bytes that silently miss the block.
  • The check is not in _checkout_without_emitting or the exporters: undo, checkout_to_latest and a detached fork call those and unwrap. The first version of this PR had it there and moved the panic to those three; fc2a196e fixes that (review feedback).

Snapshot import already validates KV checksums, so only a forged or truncated-and-rechecksummed block reaches this path.

Known limits (documented in context/arena-parent-links.md)

  • undo, checkout_to_latest and fork behave as before this PR on such a doc. They can still panic when they need the broken block itself: AppDag::ensure_lazy_load_node panics with "unparsed vv don't match with change store".
  • An import or checkout that is the first to read the block finishes on the partial history; only export re-checks at the end.
  • Local edits made after a block was recorded cannot be exported from that document. The current state (get_deep_value) is what can be salvaged.

Behavior change to review

  • The forged-header test used to assert that all 48 exports fail; it now asserts that all succeed and keep the content.
  • LoroDoc::merge returns the export error instead of unwrapping it.

Validation

  • pnpm test: 1744 passed, 37 skipped; doctests pass. pnpm test-loom: 9 passed. pnpm check: clean.
  • New tests in change_store.rs: the resolver answer, every reader recording, a doc with a truncated block (using a container that a healthy history does find), an export that is the first to read the block, and a_recorded_parse_failure_does_not_panic_where_no_error_can_be_returned. The last one fails with the panic from the review when the check is put back into the internal checkout.

🤖 Generated with Claude Code

zxch3n and others added 2 commits September 30, 2026 16:16
Re-exporting a shallow doc at its own root filtered the cached root state
and returned an error when the state was inconsistent. The error was not
cached, so every export failed again and only fork() worked. Fall back to
the verbatim root, as exporters before #1123 did: nothing referenced is
dropped. See context/internal-encoding.md.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…eator resolver

The resolver runs under the state lock from queries that cannot return an
error. It now records the block and answers CreatorOp::Corrupt; import,
export and checkout then return a DecodeError. See
context/arena-parent-links.md.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

WASM Size Report

  • Original size: 3319.12 KB
  • Gzipped size: 1104.80 KB
  • Brotli size: 773.78 KB

…rned

The check sat in _checkout_without_emitting, so once a block was recorded,
undo, checkout_to_latest and a detached fork panicked on the new error
(they unwrap it). Check it in checkout, diff, revert_to, import and
export instead; merge propagates the export error.

Every reader of the change store now records a block it cannot parse, not
only the creator resolver, and export checks again when it is done, so it
cannot return updates that silently miss the block.

See context/arena-parent-links.md, "A block that cannot be parsed".

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@zxch3n
zxch3n merged commit 883284a into main Sep 30, 2026
2 checks passed
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