COLLAB: redo after delete is merged as a concurrent edit and duplicates pasted content (server, session.rs) #292

Closed
opened 2026-09-28 06:28:04 +00:00 by kayg · 4 comments
Owner

Found by the #265 editor-undo job (job/editor-undo, merged to dev at f9c0a066). The editor side (Yjs history key handling and paste/IME capture boundaries) is fixed and passes 50 local undo/redo rounds. The real-server adversarial probe still fails:

FINDING editor isolated editor paste history storm seed=25608414: mixed paste undo 3 changed editor content

The evidence points to crates/calternal-collab/src/session.rs: conflict-shadow merging can treat a redo after a deletion as a concurrent edit, so content is duplicated. This is data corruption (a merge blocker class under CLAUDE.md), so it has priority.

Acceptance:

  • Reproduce with tests/adversarial/editor.mjs seed 25608414, then add a Rust regression test in calternal-collab that replays the same update sequence (paste → undo → redo, interleaved with a deletion) and asserts the final document.
  • Fix the shadow/merge rule so that an update which only replays or undoes the client's own earlier operations is never merged as a concurrent edit. Keep real concurrent edits from two clients working (add a two-client test).
  • Rerun the adversarial editor probe for 20 seeds including 25608414; all must pass.
  • Do not weaken existing collab tests.
Found by the #265 editor-undo job (job/editor-undo, merged to dev at f9c0a066). The editor side (Yjs history key handling and paste/IME capture boundaries) is fixed and passes 50 local undo/redo rounds. The real-server adversarial probe still fails: ``` FINDING editor isolated editor paste history storm seed=25608414: mixed paste undo 3 changed editor content ``` The evidence points to `crates/calternal-collab/src/session.rs`: conflict-shadow merging can treat a redo after a deletion as a concurrent edit, so content is duplicated. This is **data corruption** (a merge blocker class under CLAUDE.md), so it has priority. Acceptance: - Reproduce with `tests/adversarial/editor.mjs` seed 25608414, then add a Rust regression test in calternal-collab that replays the same update sequence (paste → undo → redo, interleaved with a deletion) and asserts the final document. - Fix the shadow/merge rule so that an update which only replays or undoes the client's own earlier operations is never merged as a concurrent edit. Keep real concurrent edits from two clients working (add a two-client test). - Rerun the adversarial editor probe for 20 seeds including 25608414; all must pass. - Do not weaken existing collab tests.
Author
Owner

Starting #292 on job/collab-redo, based on f9c0a06609f267e718509dedf07aa340a8de9a50 (dev). I am tracing the server conflict-shadow path and reproducing the editor history sequence before changing it.

Starting #292 on `job/collab-redo`, based on `f9c0a06609f267e718509dedf07aa340a8de9a50` (`dev`). I am tracing the server conflict-shadow path and reproducing the editor history sequence before changing it.
Author
Owner

Finding: merge_conflict_shadow applied a deleting editor connection's later redo update to the pre-delete mirror. The live document already applied that redo, so apply_named_blocks could insert the pasted root blocks a second time. The change now keeps that connection's later history on the live path while other connections still merge through the mirror. Rust regression tests cover paste → undo → redo and a second client's edit to a deleted block; both focused tests pass.

Finding: `merge_conflict_shadow` applied a deleting editor connection's later redo update to the pre-delete mirror. The live document already applied that redo, so `apply_named_blocks` could insert the pasted root blocks a second time. The change now keeps that connection's later history on the live path while other connections still merge through the mirror. Rust regression tests cover paste → undo → redo and a second client's edit to a deleted block; both focused tests pass.
Author
Owner

Reproduction evidence: the Rust regression stores the actual Yjs 13.6.33 update event bytes for paste, undo and redo. It keeps the paste out of the durable baseline, then applies a second client's root-block deletion before replaying the history. With the connection guard disabled, the redo produces a conflict-shadow update and the regression fails. With the guard enabled, the final live document matches the editor's single paste. The separate two-client test confirms that another client's edit to a deleted block still produces a shadow merge.

Reproduction evidence: the Rust regression stores the actual Yjs 13.6.33 `update` event bytes for paste, undo and redo. It keeps the paste out of the durable baseline, then applies a second client's root-block deletion before replaying the history. With the connection guard disabled, the redo produces a conflict-shadow update and the regression fails. With the guard enabled, the final live document matches the editor's single paste. The separate two-client test confirms that another client's edit to a deleted block still produces a shadow merge.
Author
Owner

Completed #292 on job/collab-redo.

Implemented in crates/calternal-collab: after a root-block deletion, the deleting connection's local undo/redo history is excluded from conflict-shadow merge. This prevents redo from reintroducing pasted text as a concurrent edit. Added Rust regressions for paste → undo → redo after deletion and for a second client's edit to the deleted block. Updated tests/adversarial/editor.mjs to accept validated deterministic seed lists; the real local-server editor probe passed all 20 seeds, 25608414–25608433.

Final head: 9107f9e6fdfb279eb4899e036ef76cadffe65f24 (pushed; remote branch matches).

Gates:

  • cargo fmt --check: exit 0, no output.
  • cargo clippy --all-targets -- -D warnings:
        Checking calternal-collab v0.0.1 (/home/kayg/Developer/calternal-wt/collab-redo/crates/calternal-collab)
       Compiling calternal-server v0.0.1 (/home/kayg/Developer/calternal-wt/collab-redo/crates/calternal-server)
        Checking calternal-plugin-video v0.0.1 (/home/kayg/Developer/calternal-wt/collab-redo/crates/plugins/video)
       Finished `dev` profile [unoptimized + debuginfo] target(s) in 54.06s
    
  • Workspace cargo test: exit 0; 72 test groups, 1,357 passed, 0 failed, 12 ignored. The collaboration unit suite reported:
    test result: ok. 15 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
    
  • bun run check:
    svelte-check found 0 errors and 0 warnings
    
  • bun run test:
     Test Files  107 passed (107)
          Tests  701 passed (701)
    
  • 20-seed editor probe final output:
    PASS editor isolated editor paste history storm seed=25608433 ms=23155
    PASS editor all areas seed=25608414 historySeeds=25608414,25608415,25608416,25608417,25608418,25608419,25608420,25608421,25608422,25608423,25608424,25608425,25608426,25608427,25608428,25608429,25608430,25608431,25608432,25608433
    
  • cargo clean: Removed 18729 files, 16.8GiB total; removed apps/web/build.

Known gap: the initial broad tests/adversarial/run.sh invocation exited 1 on findings outside the editor redo case: five old Index misses during staged rebuild in search chaos, an authz matrix fixture upload returning -1, and a broader browser text mismatch. The focused real-server editor probe passed all 20 seeds; those other scenarios were not changed or rerun here.

Decision not specified in DESIGN: use a per-connection skip for conflict-shadow merge after the deleting connection records root-block deletion. The regression confirms a separate connection remains eligible for shadow reconciliation. The editor probe seed list uses the EDITOR_SEEDS input and accepts only unsigned 32-bit seeds.

Completed #292 on `job/collab-redo`. Implemented in `crates/calternal-collab`: after a root-block deletion, the deleting connection's local undo/redo history is excluded from conflict-shadow merge. This prevents redo from reintroducing pasted text as a concurrent edit. Added Rust regressions for paste → undo → redo after deletion and for a second client's edit to the deleted block. Updated `tests/adversarial/editor.mjs` to accept validated deterministic seed lists; the real local-server editor probe passed all 20 seeds, 25608414–25608433. Final head: `9107f9e6fdfb279eb4899e036ef76cadffe65f24` (pushed; remote branch matches). Gates: - `cargo fmt --check`: exit 0, no output. - `cargo clippy --all-targets -- -D warnings`: ``` Checking calternal-collab v0.0.1 (/home/kayg/Developer/calternal-wt/collab-redo/crates/calternal-collab) Compiling calternal-server v0.0.1 (/home/kayg/Developer/calternal-wt/collab-redo/crates/calternal-server) Checking calternal-plugin-video v0.0.1 (/home/kayg/Developer/calternal-wt/collab-redo/crates/plugins/video) Finished `dev` profile [unoptimized + debuginfo] target(s) in 54.06s ``` - Workspace `cargo test`: exit 0; 72 test groups, 1,357 passed, 0 failed, 12 ignored. The collaboration unit suite reported: ``` test result: ok. 15 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out ``` - `bun run check`: ``` svelte-check found 0 errors and 0 warnings ``` - `bun run test`: ``` Test Files 107 passed (107) Tests 701 passed (701) ``` - 20-seed editor probe final output: ``` PASS editor isolated editor paste history storm seed=25608433 ms=23155 PASS editor all areas seed=25608414 historySeeds=25608414,25608415,25608416,25608417,25608418,25608419,25608420,25608421,25608422,25608423,25608424,25608425,25608426,25608427,25608428,25608429,25608430,25608431,25608432,25608433 ``` - `cargo clean`: `Removed 18729 files, 16.8GiB total`; removed `apps/web/build`. Known gap: the initial broad `tests/adversarial/run.sh` invocation exited 1 on findings outside the editor redo case: five old Index misses during staged rebuild in search chaos, an authz matrix fixture upload returning -1, and a broader browser text mismatch. The focused real-server editor probe passed all 20 seeds; those other scenarios were not changed or rerun here. Decision not specified in DESIGN: use a per-connection skip for conflict-shadow merge after the deleting connection records root-block deletion. The regression confirms a separate connection remains eligible for shadow reconciliation. The editor probe seed list uses the `EDITOR_SEEDS` input and accepts only unsigned 32-bit seeds.
kayg closed this issue 2026-09-28 09:23:16 +00:00
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
kayg/calternal#292
No description provided.