Skip to content

fix(T13842): docs fetch serves the merged head, not the last writer's raw input - #2338

Merged
kryptobaseddev merged 8 commits into
mainfrom
fix/T13842-fetch-merged-head
Oct 17, 2026
Merged

kryptobaseddev merged 8 commits into
mainfrom
fix/T13842-fetch-merged-head

Conversation

@kryptobaseddev

@kryptobaseddev kryptobaseddev commented Oct 17, 2026 •

Copy link
Copy Markdown
Owner

Problem (T13842, critical)

updateDocBySlug wrote the docs_attachments row (the bytes docs fetch / docs view read) 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 reported staleBaseMerged: true, the head had both lines, fetchContent lacked A's).

Fix (docs-update.ts only; preview API from #2358)

Order, per review-2253 option (a):

  1. Preview the merge with previewDocUpdateThroughSections (feat(T13842): read-only docs projection preview (previewDocUpdateThroughSections) #2358: the real projection inside its transaction, rolled back).
  2. Row bytes = the preview's head text: mergedText when the write would advance the head; the current head version's text when it would lose the head race (explicit lower HLC).
  3. wouldChange and the body-schema check run on those bytes (review LOW).
  4. Write the blob and the row.
  5. Commit the projection.

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:

  • settles every tracked background op (settleBackgroundOps, 5 s budget, non-consuming so the shutdown receipt keeps its ledger) — graph nodes, hook dispatches, manifest mirrors, and every other trackBackgroundOp producer. 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;
  • runs the preview on the handle's writer queue (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, in section-write-path.ts, and stays with T13881.

Tests (docs-update-concurrency.test.ts)

  • stale-base append: fetched body carries both writers' lines and equals the head;
  • lost head race: fetched body equals the head (a thin vi.mock passthrough feeds an explicit HLC into the real preview/projection);
  • a blob write failing after the merge returns E_FILE_ERROR with head and history untouched;
  • a failed preview keeps the incoming bytes, head unchanged;
  • a failed commit keeps the merged row and the old head.

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

kryptobaseddev and others added 3 commits October 16, 2026 20:59
… 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
kryptobaseddev and others added 5 commits October 17, 2026 10:20
…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
@kryptobaseddev
kryptobaseddev merged commit cfbeb2a into main Oct 17, 2026
41 checks passed
@kryptobaseddev
kryptobaseddev deleted the fix/T13842-fetch-merged-head branch October 17, 2026 20:09
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