Skip to content

fix(T13868): concurrent first docs updates no longer wedge the second device's stream - #2359

Merged
kryptobaseddev merged 2 commits into
mainfrom
fix/T13868-doc-attachment-identity
Oct 18, 2026
Merged

kryptobaseddev merged 2 commits into
mainfrom
fix/T13868-doc-attachment-identity

Conversation

@kryptobaseddev

Copy link
Copy Markdown
Owner

Task: T13868 (critical, under T12323). Found by the journal gate on #2254's build (S8).

Root cause

Each device makes its first docs update of the same synced doc concurrently. Each one lazily mints its own conduit_attachments row for the doc (T13734), with conduit_attachment_versions children and a conduit_doc_heads row. Two defects combine:

  1. The wedge. To apply the peer's earlier segment, the second device rewinds its own unsequenced transaction. The rewind undoes its conduit_attachments INSERT but skips append-only children. Its own conduit_attachment_versions rows still point at the doc, so the immediate FK check refuses the delete. The frame then rolls back, and every later cloud sync fails with a bare "FOREIGN KEY constraint failed". The peer's segment stays staged.
  2. Version links sealed as NULL. conduit_attachment_versions and conduit_attachment_approvals declared attachment_uid, parent_version_uid and version_uid as refs. A ref is translated through the target's LOCAL key (slug, id), but these columns already hold uids. The sealer found no row and sent NULL, so peers stored versions with no doc and no parent.

Fix

  • apply/applier.ts rewindTxns: sets PRAGMA defer_foreign_keys = ON. Rewind, apply and replay share the frame's transaction, so the parent is back (from the stream or the replay) before COMMIT checks. The applier already does this for a display-key move (applyOne, write-api.ts).
  • row-identity-registry.ts: the two tables drop those refs. The columns travel verbatim, like conduit_doc_heads and the slug aliases already do.
  • apply/frame.ts: a deferred FK that still fails at COMMIT throws E_SYNC_APPLY_FK naming the child uid and its parent table (AC3, "or the error names the row"). The frame still rolls back whole.

Residual (reviewed, accepted for this fix)

Once a page rebases, FKs stay deferred for the rest of that frame, the same as after a key move today. If an incoming op in that page has a dangling FK, it no longer voids as a per-op guard conflict. The frame instead fails at COMMIT, now with the rows named. The applier's own dangling-ref detection runs before the write, so the FK is a backstop. A follow-up could restore immediate checks after the replay.

Tests

  • two-device-e2e.test.ts: concurrent first updateDocBySlug on both devices, in both pull orders. Neither device has anything left staged, foreign_key_check is clean, there is one conduit_attachments row, and docs, versions and heads are identical on both devices.
  • apply-frame.test.ts: a deferred FK refused at COMMIT names conduit_doc_slug_aliases <uid> → conduit_attachments, rolls back, and leaves no transaction open.
  • Evidence from the previous agent (team-sync), single-file runs before the owner's no-local-heavy-runs directive:
    • With the fix, both new two-device tests passed.
    • Mutation without the FK deferral: both failed with "FOREIGN KEY constraint failed".
    • Mutation without the refs change: both failed on doc-state equality.
  • Not run locally: the new apply-frame.test.ts case and the neighbouring test files never ran (they were queued when the directive landed). GitHub CI's Unit Tests and Type Check are the evidence.

Gates (static, run here)

  • OK: gate 28 lint-no-raw-table-writes, gate 32 lint-skill-coverage --base origin/main, gate 36 lint-sync-schema --base origin/main, gate 37 lint-row-identity-coverage --base origin/main, gate 38 lint-sync-write-invariants --base origin/main, and the changeset lint.
  • Biome: deferred by the machine budget, so it was not run here. CI's Lint & Format covers it.

Known follow-ups (from the changeset)

  • Peers that already stored versions with NULL links get them back through one repair pass on the device that wrote them.
  • On a store that is already wedged, the staged segment applies on its next sync.

🤖 Generated with Claude Code

kryptobaseddev and others added 2 commits October 17, 2026 10:24
… device's stream

Rewinding a local txn undid its conduit_attachments INSERT while its
append-only conduit_attachment_versions children still pointed at it, so
the immediate FK check refused and every later cloud sync failed with a
bare 'FOREIGN KEY constraint failed'. Rewind, apply and replay share one
frame transaction, so the rewind now defers FKs to COMMIT; the parent is
back (stream or replay) before the check.

conduit_attachment_versions / _approvals declared their uid columns as
refs, which translate through the target's LOCAL key (slug, id): the
sealer found no row and sent NULL, so peers stored versions with no doc and
no parent. Those columns already hold uids; they now travel verbatim like
conduit_doc_heads.

A deferred FK that still fails at COMMIT now names the child row and its
parent table (E_SYNC_APPLY_FK).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kryptobaseddev
kryptobaseddev merged commit bad79aa into main Oct 18, 2026
84 checks passed
@kryptobaseddev
kryptobaseddev deleted the fix/T13868-doc-attachment-identity branch October 18, 2026 22:12
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