Repository navigation
fix(T13842): docs fetch serves the merged head, not the last writer's raw input - #2338
Merged
Merged
Conversation
… raw input updateDocBySlug wrote the docs_attachments row (the bytes docs fetch and docs view read) from the incoming text, then projected the edit through the section CRDT. After a stale-base merge the conduit head held both writers' edits but the row held only the last writer's input, so readers silently lost the concurrent edit even where the merge was correct (probe on main: an append merge reported staleBaseMerged:true, the head had both lines, the fetched body had only B's). The projection now runs before the row write, and the row is written from the resulting head: mergedText when this write advanced the head, the head version's text when it lost the head race (an explicit lower HLC), and the incoming bytes only when the projection fails. Conflicts are unchanged: the head excludes a conflicting incoming edit, which stays in its own version. Tests (docs-update-concurrency.test.ts, both fail on main): a stale-base append merge's fetched body carries both writers' lines and equals the head; a write that loses the head race leaves the fetched body equal to the head. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ed-head # Conflicts: # packages/core/src/docs/docs-update.ts
…o fix/T13842-fetch-merged-head
…tion review-2253 (#2338): running the projection before the blob/row write meant a later blob, read-back or transaction failure left the conduit head advanced over an old row and reported a failure, and a retry re-projected as a duplicate. The merge is now previewed with previewDocUpdateThroughSections (#2364: the real projection inside its transaction, rolled back). The row bytes are the preview's head text (mergedText when it would advance, the current head's text when it would lose the head race); wouldChange and the body schema check run on those bytes. The blob and row are written, and only then is the projection committed. A failed preview writes the incoming bytes and leaves the head unchanged (W_DOC_PROJECTION_FAILED); a failed commit after the row write is reported with the head uid; a head that diverged between preview and commit is reported (W_DOC_HEAD_DIVERGED). The noop check stays keyed on the incoming bytes. #2326's prior-version link is computed once and passed to both preview and commit. Tests (docs-update-concurrency.test.ts): a blob write failing after the merge leaves head and history untouched (E_FILE_ERROR); a failed preview writes the incoming bytes with the head unchanged; a failed commit keeps the merged row and the old head. All three fail on the previous ordering; the two earlier T13842 tests still pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…g the merge preview previewDocUpdateThroughSections rolls back on the process-wide shared native handle, so any write another in-process writer put on that handle during the preview's awaits would be rolled back with it. docs-update now (1) settles every tracked background op first (bounded; the preview is skipped, incoming bytes written, when one is still running) and (2) runs the preview on the handle's writer queue via scheduleTaskBackground, so accessor transactions and committed background effects queued meanwhile start only after the rollback. The settle does not consume the shutdown receipt's ledger. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ed-head # Conflicts: # packages/core/src/docs/__tests__/docs-update-concurrency.test.ts
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.
Problem (T13842, critical)
updateDocBySlugwrote thedocs_attachmentsrow (the bytesdocs fetch/docs viewread) from the incoming text and only then projected the edit through the section CRDT. After a stale-base merge the conduit head held both writers' edits but the row held only the last writer's input, so readers silently lost the concurrent edit even where the merge was correct (probe on main: an append merge reportedstaleBaseMerged: true, the head had both lines,fetchContentlacked A's).Fix (
docs-update.tsonly; preview API from #2358)Order, per review-2253 option (a):
previewDocUpdateThroughSections(feat(T13842): read-only docs projection preview (previewDocUpdateThroughSections) #2358: the real projection inside its transaction, rolled back).mergedTextwhen the write would advance the head; the current head version's text when it would lose the head race (explicit lower HLC).wouldChangeand the body-schema check run on those bytes (review LOW).Failure handling: a failed preview writes the incoming bytes and leaves the head unchanged (
W_DOC_PROJECTION_FAILED); a failed commit after the row write is reported with the head uid (W_DOC_PROJECTION_FAILED); a head that moved between preview and commit is reported (W_DOC_HEAD_DIVERGED). The noop check stays keyed on the incoming bytes. #2326's prior-version link is computed once and passed to preview and commit.T13881: how the preview's rollback is kept from discarding other writes
The preview rolls back on the process-wide shared native handle, so a write another in-process writer lands on that handle during its awaits would be rolled back with it. Before the preview,
docs-update.ts:settleBackgroundOps, 5 s budget, non-consuming so the shutdown receipt keeps its ledger) — graph nodes, hook dispatches, manifest mirrors, and every othertrackBackgroundOpproducer. If one is still running after the budget, the preview is skipped (incoming bytes written, head unchanged,W_DOC_PROJECTION_FAILED), never run over it;scheduleTaskBackground, the same queue every accessor transaction and committed background effect uses, and the sync apply frame's serialization point), so any accessor transaction or committed effect queued during the preview starts only after its rollback.Not covered: an in-process writer that bypasses both the accessor queue and the background registry (a raw
getDb()write from concurrent code in a long-lived host). The CLI runs one command per process, so only tracked ops run beside it there. A dedicated connection or savepoint for the preview (T13881 AC option 1) would close that, insection-write-path.ts, and stays with T13881.Tests (
docs-update-concurrency.test.ts)vi.mockpassthrough feeds an explicit HLC into the real preview/projection);E_FILE_ERRORwith head and history untouched;Verified locally before the owner's no-local-heavy-runs directive: the first two fail on main, the last three fail on the previous ordering, all pass with the fix (concurrency + version-link 15/15; section-write-path, docversion, status-only, derive-links 45/45; tsc -b exit 0). The T13881 guard commit (1ec71e4) is verified by CI only, per the directive. Static checks on the final head: biome, gates 28/32/38 (
--base origin/main), changeset lint.#2335 (pre-merge heading refusal) was closed in favour of #2325's planner property plus this fix.
🤖 Generated with Claude Code