DATA: typing in a note edited over MCP writes text twice (external write + live collab doc duplicate lines) #634

Open
opened 2026-10-01 12:29:33 +00:00 by kayg · 7 comments
Owner

Owner report (2026-10-01, calternal.cloud 3f258302a): typed text is written into two places

"Sometimes when I edit a document and write, it gets written into two places at once, even though I am the only user! This is a note created by the Aside browser [over MCP]. It only edited it twice and after that I was correcting it, but writing 'Gopal' at the last line also duplicated it on another line."
Data-integrity class (sync collision): merge blocker; fix first.
Likely mechanics (verify, do not assume):

  • The note was created/edited through MCP/API (Aside, 2 writes), which writes the Markdown file directly. The open editor holds a Yjs/collab document for the same note (calternal-collab). When a server-side file write lands, the collab doc is reloaded or merged from the new file while the old Yjs state is still live, so the text ends up in the doc twice (two blocks with the same content), and later keystrokes are applied at two positions (e.g. two blocks share one block ID / anchor, or two Y.Text bindings map to one ProseMirror node).
  • Related history: calternal.js #130 "sync mergeText with no base reorders note blocks"; the Notes restart/epoch findings #597–#600; the TS converter vs the Rust canonicalizer differing (bridge.ts).
    Do:
  1. Reproduce deterministically: open a note in the editor (a production build); in parallel, write it twice through the MCP/API note-update tool (as Aside did); then type at the end of the last line. Assert the document has exactly one copy of each line and the typed text appears once. Also: a file-level change via WebDAV/Files while the editor is open.
  2. Find the root cause in the external-write → collab reconciliation path (calternal-collab, the Notes plugin's write/merge, the editor's provider). Fix it so an external write is applied as one diff against the live doc (a three-way merge with the last-synced base), never a reload-on-top. Block IDs stay unique.
  3. Repair: a check finds notes with duplicated blocks created by this bug (identical adjacent block content + duplicate block IDs) and lists them. Do not auto-delete; offer a "Remove duplicates" action with Undo.
  4. Add the scenario to the robustness suite and the two-surface consistency matrix (MCP/API + editor + WebDAV on one note).
    Note for the owner's data: do not modify the owner's affected note; reproduce on fixtures. Per-crate gates (calternal-collab, plugins/notes, notes-core) + web + editor e2e. Time limit 5 h.
## Owner report (2026-10-01, calternal.cloud 3f258302a): typed text is written into two places "Sometimes when I edit a document and write, it gets written into two places at once, even though I am the only user! This is a note created by the Aside browser [over MCP]. It only edited it twice and after that I was correcting it, but writing 'Gopal' at the last line also duplicated it on another line." **Data-integrity class (sync collision): merge blocker; fix first.** **Likely mechanics (verify, do not assume):** - The note was created/edited through **MCP/API** (Aside, 2 writes), which writes the Markdown file directly. The open editor holds a **Yjs/collab document** for the same note (calternal-collab). When a server-side file write lands, the collab doc is reloaded or merged from the new file **while the old Yjs state is still live**, so the text ends up in the doc twice (two blocks with the same content), and later keystrokes are applied at **two positions** (e.g. two blocks share one block ID / anchor, or two Y.Text bindings map to one ProseMirror node). - Related history: calternal.js #130 "sync mergeText with no base reorders note blocks"; the Notes restart/epoch findings #597–#600; the TS converter vs the Rust canonicalizer differing (bridge.ts). **Do:** 1. **Reproduce deterministically:** open a note in the editor (a production build); in parallel, write it twice through the MCP/API note-update tool (as Aside did); then type at the end of the last line. Assert the document has exactly one copy of each line and the typed text appears once. Also: a file-level change via WebDAV/Files while the editor is open. 2. Find the root cause in the external-write → collab reconciliation path (calternal-collab, the Notes plugin's write/merge, the editor's provider). Fix it so an external write is applied as **one** diff against the live doc (a three-way merge with the last-synced base), never a reload-on-top. Block IDs stay unique. 3. **Repair:** a check finds notes with duplicated blocks created by this bug (identical adjacent block content + duplicate block IDs) and lists them. Do not auto-delete; offer a "Remove duplicates" action with Undo. 4. Add the scenario to the robustness suite and the two-surface consistency matrix (MCP/API + editor + WebDAV on one note). **Note for the owner's data:** do not modify the owner's affected note; reproduce on fixtures. Per-crate gates (calternal-collab, plugins/notes, notes-core) + web + editor e2e. Time limit 5 h.
Author
Owner

Started on job/dupwrite-634, base 3f258302a0. Tracing the live room's external-write three-way merge, Yjs branch identity and editor binding. All reproduction uses disposable fixture Notes; the owner's affected Note will not be read or changed.

Started on job/dupwrite-634, base 3f258302a0f2d6418ff60c9ce22cbb33e008ca99. Tracing the live room's external-write three-way merge, Yjs branch identity and editor binding. All reproduction uses disposable fixture Notes; the owner's affected Note will not be read or changed.
Author
Owner

Root cause verified against the original 3f258302a algorithm. A pre-delete conflict mirror still holds an older tree after an external replacement. Typing in an unchanged Yjs branch updates both the live room and mirror. Their shared replacement sits beside an external-only replacement, so whole-chunk equality fails; the merge reinserts the already-live typed block.

Deterministic regression with original merge function:
left: "last Gopal ^last634\n\nfirst Files\n\nlast Gopal ^last634"
right: "first Files\n\nlast Gopal ^last634"

Commit c939c75b6 aligns shared blocks inside conflict chunks in sequence and skips only copies already present in surviving live branches. Existing calternal-collab tests and the new mirror regression pass with the fix; touched-crate clippy passes. Working on read-only duplicate listing, explicit repair with Undo, and the production editor/MCP/WebDAV matrix.

Root cause verified against the original 3f258302a algorithm. A pre-delete conflict mirror still holds an older tree after an external replacement. Typing in an unchanged Yjs branch updates both the live room and mirror. Their shared replacement sits beside an external-only replacement, so whole-chunk equality fails; the merge reinserts the already-live typed block. Deterministic regression with original merge function: left: "last Gopal ^last634\n\nfirst Files\n\nlast Gopal ^last634" right: "first Files\n\nlast Gopal ^last634" Commit c939c75b6 aligns shared blocks inside conflict chunks in sequence and skips only copies already present in surviving live branches. Existing calternal-collab tests and the new mirror regression pass with the fix; touched-crate clippy passes. Working on read-only duplicate listing, explicit repair with Undo, and the production editor/MCP/WebDAV matrix.
Author
Owner

Further findings and committed fixes:

  • 7696bd648: 2,500 seeded cases exposed loss of one identical unanchored block when old-to-live and old-to-new alignment selected different copies. The merge now recognizes a shared novel replacement by ordered insertions and removed-content counts before choosing anchors. Both 2,500-case property tests pass.
  • Same-block concurrent content retains both versions. The live block keeps its link; the inserted copy gets a deterministic unique anchor. Replay reuses it. A derived anchor already owned by different content is skipped. Regression tests cover paragraph, quote, list and an occupied derived ID.
  • 7a92aa008: repair is a real keyboard-accessible button and its notice aligns with the editor's text gutter.
  • d345f9107: production MCP/API/editor/WebDAV case, read-only repair matrix, robustness extension, and 64/10,000-block benchmark profile.

Production editor run before final integration: MCP twice + API + editor + WebDAV + editor passed; explicit Remove duplicates and Undo preserve later typing; CSP reports 0. Six fixture screenshots cover 390/820/1440 in light and dark. No owner's Note was read or modified. Final integration gates and one robustness round follow.

Further findings and committed fixes: - `7696bd648`: 2,500 seeded cases exposed loss of one identical unanchored block when old-to-live and old-to-new alignment selected different copies. The merge now recognizes a shared novel replacement by ordered insertions and removed-content counts before choosing anchors. Both 2,500-case property tests pass. - Same-block concurrent content retains both versions. The live block keeps its link; the inserted copy gets a deterministic unique anchor. Replay reuses it. A derived anchor already owned by different content is skipped. Regression tests cover paragraph, quote, list and an occupied derived ID. - `7a92aa008`: repair is a real keyboard-accessible button and its notice aligns with the editor's text gutter. - `d345f9107`: production MCP/API/editor/WebDAV case, read-only repair matrix, robustness extension, and 64/10,000-block benchmark profile. Production editor run before final integration: MCP twice + API + editor + WebDAV + editor passed; explicit Remove duplicates and Undo preserve later typing; CSP reports 0. Six fixture screenshots cover 390/820/1440 in light and dark. No owner's Note was read or modified. Final integration gates and one robustness round follow.
Author
Owner
Production fixture captures for orchestrator review: - [duplicates-review-1440-paper-white.png](https://git.kayg.org/attachments/5029701b-24cc-4933-9137-dbe7d8cd806a) - [duplicates-review-1440-tokyo-night.png](https://git.kayg.org/attachments/5f035f59-7f29-4d2c-916c-491a1f7b242d) - [duplicates-review-390-paper-white.png](https://git.kayg.org/attachments/61602cfb-6572-4afc-a5d3-214e6861185d) - [duplicates-review-390-tokyo-night.png](https://git.kayg.org/attachments/1996406e-a6a2-4fb9-9ed8-59c1759020c5) - [duplicates-review-820-paper-white.png](https://git.kayg.org/attachments/a99297e7-bae6-4059-b12c-8593a6897bac) - [duplicates-review-820-tokyo-night.png](https://git.kayg.org/attachments/b9209ef5-bfd3-45ab-b1a8-94cd7ba0813a)
Author
Owner

$ cargo fmt --check
$ cargo clippy -p calternal-collab --all-targets -- -D warnings
Finished dev profile [unoptimized + debuginfo] target(s) in 1.21s
$ cargo test -p calternal-collab -- --test-threads=1
test result: ok. 30 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 3.02s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 1.44s
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 6.14s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 43.55s
test result: ok. 11 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 14.47s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 1.47s
test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 2.38s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 11.41s
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 2.43s
test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 16.50s
test result: ok. 15 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.05s
test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 15.75s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
$ cargo clippy -p calternal-plugin-notes --all-targets -- -D warnings
Finished dev profile [unoptimized + debuginfo] target(s) in 49.15s
$ cargo test -p calternal-plugin-notes -- --test-threads=1
test result: ok. 162 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 80.30s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.30s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
$ cargo clippy -p calternal-notes-core --all-targets -- -D warnings
Finished dev profile [unoptimized + debuginfo] target(s) in 11.02s
$ cargo test -p calternal-notes-core -- --test-threads=1
test result: ok. 519 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.32s
test result: ok. 19 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 5.89s
test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.08s
test result: ok. 7 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.61s
test result: ok. 12 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.02s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
$ cargo clippy -p calternal-server --all-targets -- -D warnings
Finished dev profile [unoptimized + debuginfo] target(s) in 46.85s
$ cargo test -p calternal-server -- --test-threads=1
test result: ok. 107 passed; 0 failed; 3 ignored; 0 measured; 0 filtered out; finished in 44.06s
$ bun run check (web)
svelte-check found 0 errors and 0 warnings
$ bun run test (web)
Test Files 148 passed (148)
Tests 1013 passed (1013)
$ bun run check (editor)
svelte-check found 0 errors and 0 warnings
$ bun run test (editor)
Test Files 18 passed (18)
Tests 402 passed (402)
$ NOTES_E2E_EXTERNAL_ONLY=1 bun apps/web/e2e/notes.mjs
notes external-write #634: MCP twice + API + editor + WebDAV + editor passed
notes duplicate check #634: lists fixture and leaves Markdown unchanged
notes duplicate repair #634: explicit removal and Undo preserve later text
CSP REPORTS notes: 0 across 1 pages
$ EDITOR_ONLY=1 EDITOR_AREA=external-write tests/adversarial/run.sh
PASS editor external Note body write during a live room seed=25608414 ms=12391
PASS editor all areas seed=25608414 historySeeds=25608414

$ cargo fmt --check $ cargo clippy -p calternal-collab --all-targets -- -D warnings Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.21s $ cargo test -p calternal-collab -- --test-threads=1 test result: ok. 30 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 3.02s test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 1.44s test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 6.14s test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 43.55s test result: ok. 11 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 14.47s test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 1.47s test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 2.38s test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 11.41s test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 2.43s test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 16.50s test result: ok. 15 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.05s test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 15.75s test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s $ cargo clippy -p calternal-plugin-notes --all-targets -- -D warnings Finished `dev` profile [unoptimized + debuginfo] target(s) in 49.15s $ cargo test -p calternal-plugin-notes -- --test-threads=1 test result: ok. 162 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 80.30s test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.30s test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s $ cargo clippy -p calternal-notes-core --all-targets -- -D warnings Finished `dev` profile [unoptimized + debuginfo] target(s) in 11.02s $ cargo test -p calternal-notes-core -- --test-threads=1 test result: ok. 519 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.32s test result: ok. 19 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 5.89s test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.08s test result: ok. 7 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.61s test result: ok. 12 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.02s test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s $ cargo clippy -p calternal-server --all-targets -- -D warnings Finished `dev` profile [unoptimized + debuginfo] target(s) in 46.85s $ cargo test -p calternal-server -- --test-threads=1 test result: ok. 107 passed; 0 failed; 3 ignored; 0 measured; 0 filtered out; finished in 44.06s $ bun run check (web) svelte-check found 0 errors and 0 warnings $ bun run test (web) Test Files 148 passed (148) Tests 1013 passed (1013) $ bun run check (editor) svelte-check found 0 errors and 0 warnings $ bun run test (editor) Test Files 18 passed (18) Tests 402 passed (402) $ NOTES_E2E_EXTERNAL_ONLY=1 bun apps/web/e2e/notes.mjs notes external-write #634: MCP twice + API + editor + WebDAV + editor passed notes duplicate check #634: lists fixture and leaves Markdown unchanged notes duplicate repair #634: explicit removal and Undo preserve later text CSP REPORTS notes: 0 across 1 pages $ EDITOR_ONLY=1 EDITOR_AREA=external-write tests/adversarial/run.sh PASS editor external Note body write during a live room seed=25608414 ms=12391 PASS editor all areas seed=25608414 historySeeds=25608414
Author
Owner

Finished dupwrite-634. Head: bab16f0dabc139bead1ac27dd47f1305588352f3. Branch: job/dupwrite-634.

No push or deploy. The required fetch/merge of origin/dev ran once and was already up to date at 3f258302a. The owner's affected Note was not accessed.

Duplicate Note writes: issue 634

Issue: #634

Change and evidence

An editor root-block deletion keeps a pre-delete mirror for conflict recovery.
MCP or API writes can change another paragraph while that mirror stays live.
Typing then enters both live branches. The old block merge checked equality
only for a whole conflict chunk. If one paragraph differed, it inserted the
shared typed block again.

The old code fails the fixture regression. The saved result has two copies of
last Gopal ^last634, with first Files between them. The fixed code keeps
one first paragraph and one last paragraph. The owner's Note was not accessed.

The fix aligns new and live content inside each conflict chunk. It applies
shared insertions once and keeps intentional repeated content. Before choosing
common anchors, a shared novel replacement is checked by insertion order and
removed-content counts. Two seeded tests cover 2,500 cases each. Existing
moves and block-order tests keep their expectations.

A true same-block conflict keeps both contents. The live block keeps its ID.
The inserted copy gets a deterministic unique ID. Replay reuses the same copy.
An existing different block with a derived ID forces another derivation.
Paragraph, quote, list and occupied-ID tests cover this rule.

The operator's read-only check lists identical adjacent blocks with a shared
explicit ID. The editor offers Remove duplicates. A dedicated UndoManager
tracks only that repair, so Undo preserves text typed later. The production
fixture checks that the scanner does not change the Markdown.

Files

  • crates/calternal-collab/src/lib.rs: merge plan and conflict identities.
  • crates/calternal-collab/src/markdown.rs: replace an owned anchor token.
  • crates/calternal-collab/src/session.rs: pre-delete mirror regression.
  • crates/calternal-collab/src/repeats.rs: read-only candidate detector.
  • crates/calternal-collab/tests/block_apply_property.rs: seeded shared edits.
  • crates/calternal-collab/Cargo.toml, Cargo.lock: existing workspace BLAKE3.
  • crates/calternal-server/src/wire.rs: operator candidate output.
  • apps/web/src/lib/notes/NoteView.svelte: repair notice and scoped Undo.
  • apps/web/src/lib/notes/anchors.ts, anchors.test.ts: exact-block repair.
  • apps/web/src/lib/notes/collaborationUndo.svelte.test.ts: mounted repair Undo.
  • apps/web/e2e/notes.mjs: production surface checks and profile.
  • tests/adversarial/editor.mjs: live MCP and WebDAV regression.
  • bench/notes-external-write.sh: external-write hot path.
  • docs/testing/notes-external-writes.md: consistency matrix and commands.
  • docs/perf/notes-external-write-634.json: measured release profile.
  • docs/audits/dupwrite-634.md: this report.

Validation

Before final gates, git fetch origin && git merge origin/dev ran once.
origin/dev was already up to date at 3f258302a.
All Rust gates ran per crate with four build jobs and no incremental output.
Tests used one thread per crate. No existing expectation changed.
cargo fmt --check exited 0 with no output.

The following gate excerpts are verbatim. The issue also has these results.

$ cargo clippy -p calternal-collab --all-targets -- -D warnings
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.21s
$ cargo test -p calternal-collab -- --test-threads=1
test result: ok. 30 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 3.02s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 1.44s
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 6.14s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 43.55s
test result: ok. 11 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 14.47s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 1.47s
test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 2.38s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 11.41s
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 2.43s
test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 16.50s
test result: ok. 15 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.05s
test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 15.75s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
$ cargo clippy -p calternal-plugin-notes --all-targets -- -D warnings
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 49.15s
$ cargo test -p calternal-plugin-notes -- --test-threads=1
test result: ok. 162 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 80.30s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.30s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
$ cargo clippy -p calternal-notes-core --all-targets -- -D warnings
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 11.02s
$ cargo test -p calternal-notes-core -- --test-threads=1
test result: ok. 519 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.32s
test result: ok. 19 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 5.89s
test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.08s
test result: ok. 7 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.61s
test result: ok. 12 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.02s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
$ cargo clippy -p calternal-server --all-targets -- -D warnings
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 46.85s
$ cargo test -p calternal-server -- --test-threads=1
test result: ok. 107 passed; 0 failed; 3 ignored; 0 measured; 0 filtered out; finished in 44.06s
$ bun run check (web)
svelte-check found 0 errors and 0 warnings
$ bun run test (web)
 Test Files  148 passed (148)
      Tests  1013 passed (1013)
$ bun run check (editor)
svelte-check found 0 errors and 0 warnings
$ bun run test (editor)
 Test Files  18 passed (18)
      Tests  402 passed (402)
$ NOTES_E2E_EXTERNAL_ONLY=1 bun apps/web/e2e/notes.mjs
notes external-write #634: MCP twice + API + editor + WebDAV + editor passed
notes duplicate check #634: lists fixture and leaves Markdown unchanged
notes duplicate repair #634: explicit removal and Undo preserve later text
CSP REPORTS notes: 0 across 1 pages
$ EDITOR_ONLY=1 EDITOR_AREA=external-write tests/adversarial/run.sh
PASS editor external Note body write during a live room seed=25608414 ms=12391
PASS editor all areas seed=25608414 historySeeds=25608414

The production build has six repair screenshots: 390, 820 and 1440 pixels,
in light and dark schemes. They are attached to the issue. The orchestrator
reviews visual quality. The captures are not in Git.

One time-boxed robustness round ran the external-write area on a real fixture
server. It passed. The final server gate passed the live-app child-process
checks, which timed out in an earlier parallel test run on the shared host.

Performance

The run used a prebuilt release server on perf-test under /root/perf.lock.
No build ran on the VM. Load average inside the lock was 0.14/0.12/0.08 before
and 1.13/0.34/0.16 after the run. The fixture Instance was removed.
Each case has 12 sequential writes and one eight-write burst.
CPU and RSS refer to the server process. RSS is sampled, not a peak measure.

Metric 64 blocks 10,000 blocks Matching baseline
Request-to-editor p50 71.70 ms 546.10 ms None
Request-to-editor p95 159.10 ms 755.58 ms None
Server CPU during sequential samples 24.03% 223.99% None
Server RSS after sequential samples 111.84 MiB 952.36 MiB None
Eight-write burst duration 92.92 ms 630.02 ms None
Server RSS after burst 112.41 MiB 952.55 MiB None
Successful / stale burst writes 1 / 7 1 / 7 None

docs/perf/baseline.json is at 369ab6a2f9fc673e3564b94857fbecfeb04df404.
It measures Notes import and route load, which are different operations.
This is the first comparable external-write-to-editor profile. It does not
establish a regression against that baseline.

The first profile attempt stopped at its caret assertion: the fixture tail
was below the viewport. The harness now scrolls before finding the hit point.
The corrected profile passed, including one copy of text typed in each tail.

Known gaps

  • Repair flags only identical adjacent root blocks with a shared explicit ID.
    Changed or nonadjacent copies require manual review.
  • The 10,000-block case used 952.55 MiB sampled server RSS. There is no matching
    prior profile or approved memory threshold for this operation.
  • The owner's original Note was not inspected. The fixture proves this failure
    mechanism; it does not establish every step in that Note's edit history.
  • Visual quality review remains with the orchestrator.

Decisions

  • Use the existing Pill and toast for explicit repair; add no repair page.
  • Track only the repair transaction for Undo and use the existing eight-second
    Undo duration. Keep subsequent local and remote edits.
  • Retain the live block ID on conflict. Derive a unique ID for the inserted
    version so its identity is stable on replay.
  • Use 64 and 10,000 blocks for the new profile. Keep these results separate
    from import and route-load metrics in the existing baseline.

Cleanup: cargo clean completed; web build output and fixture runtime/temp output were removed. Screenshots remain in ignored artifacts and are attached above.

Production fixture captures for orchestrator review:

Finished dupwrite-634. Head: `bab16f0dabc139bead1ac27dd47f1305588352f3`. Branch: `job/dupwrite-634`. No push or deploy. The required fetch/merge of origin/dev ran once and was already up to date at 3f258302a. The owner's affected Note was not accessed. # Duplicate Note writes: issue 634 Issue: https://git.kayg.org/kayg/calternal/issues/634 ## Change and evidence An editor root-block deletion keeps a pre-delete mirror for conflict recovery. MCP or API writes can change another paragraph while that mirror stays live. Typing then enters both live branches. The old block merge checked equality only for a whole conflict chunk. If one paragraph differed, it inserted the shared typed block again. The old code fails the fixture regression. The saved result has two copies of `last Gopal ^last634`, with `first Files` between them. The fixed code keeps one first paragraph and one last paragraph. The owner's Note was not accessed. The fix aligns new and live content inside each conflict chunk. It applies shared insertions once and keeps intentional repeated content. Before choosing common anchors, a shared novel replacement is checked by insertion order and removed-content counts. Two seeded tests cover 2,500 cases each. Existing moves and block-order tests keep their expectations. A true same-block conflict keeps both contents. The live block keeps its ID. The inserted copy gets a deterministic unique ID. Replay reuses the same copy. An existing different block with a derived ID forces another derivation. Paragraph, quote, list and occupied-ID tests cover this rule. The operator's read-only check lists identical adjacent blocks with a shared explicit ID. The editor offers Remove duplicates. A dedicated UndoManager tracks only that repair, so Undo preserves text typed later. The production fixture checks that the scanner does not change the Markdown. ## Files - `crates/calternal-collab/src/lib.rs`: merge plan and conflict identities. - `crates/calternal-collab/src/markdown.rs`: replace an owned anchor token. - `crates/calternal-collab/src/session.rs`: pre-delete mirror regression. - `crates/calternal-collab/src/repeats.rs`: read-only candidate detector. - `crates/calternal-collab/tests/block_apply_property.rs`: seeded shared edits. - `crates/calternal-collab/Cargo.toml`, `Cargo.lock`: existing workspace BLAKE3. - `crates/calternal-server/src/wire.rs`: operator candidate output. - `apps/web/src/lib/notes/NoteView.svelte`: repair notice and scoped Undo. - `apps/web/src/lib/notes/anchors.ts`, `anchors.test.ts`: exact-block repair. - `apps/web/src/lib/notes/collaborationUndo.svelte.test.ts`: mounted repair Undo. - `apps/web/e2e/notes.mjs`: production surface checks and profile. - `tests/adversarial/editor.mjs`: live MCP and WebDAV regression. - `bench/notes-external-write.sh`: external-write hot path. - `docs/testing/notes-external-writes.md`: consistency matrix and commands. - `docs/perf/notes-external-write-634.json`: measured release profile. - `docs/audits/dupwrite-634.md`: this report. ## Validation Before final gates, `git fetch origin && git merge origin/dev` ran once. `origin/dev` was already up to date at `3f258302a`. All Rust gates ran per crate with four build jobs and no incremental output. Tests used one thread per crate. No existing expectation changed. `cargo fmt --check` exited 0 with no output. The following gate excerpts are verbatim. The issue also has these results. ```text $ cargo clippy -p calternal-collab --all-targets -- -D warnings Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.21s $ cargo test -p calternal-collab -- --test-threads=1 test result: ok. 30 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 3.02s test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 1.44s test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 6.14s test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 43.55s test result: ok. 11 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 14.47s test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 1.47s test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 2.38s test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 11.41s test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 2.43s test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 16.50s test result: ok. 15 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.05s test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 15.75s test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s $ cargo clippy -p calternal-plugin-notes --all-targets -- -D warnings Finished `dev` profile [unoptimized + debuginfo] target(s) in 49.15s $ cargo test -p calternal-plugin-notes -- --test-threads=1 test result: ok. 162 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 80.30s test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.30s test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s $ cargo clippy -p calternal-notes-core --all-targets -- -D warnings Finished `dev` profile [unoptimized + debuginfo] target(s) in 11.02s $ cargo test -p calternal-notes-core -- --test-threads=1 test result: ok. 519 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.32s test result: ok. 19 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 5.89s test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.08s test result: ok. 7 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.61s test result: ok. 12 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.02s test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s $ cargo clippy -p calternal-server --all-targets -- -D warnings Finished `dev` profile [unoptimized + debuginfo] target(s) in 46.85s $ cargo test -p calternal-server -- --test-threads=1 test result: ok. 107 passed; 0 failed; 3 ignored; 0 measured; 0 filtered out; finished in 44.06s $ bun run check (web) svelte-check found 0 errors and 0 warnings $ bun run test (web) Test Files 148 passed (148) Tests 1013 passed (1013) $ bun run check (editor) svelte-check found 0 errors and 0 warnings $ bun run test (editor) Test Files 18 passed (18) Tests 402 passed (402) $ NOTES_E2E_EXTERNAL_ONLY=1 bun apps/web/e2e/notes.mjs notes external-write #634: MCP twice + API + editor + WebDAV + editor passed notes duplicate check #634: lists fixture and leaves Markdown unchanged notes duplicate repair #634: explicit removal and Undo preserve later text CSP REPORTS notes: 0 across 1 pages $ EDITOR_ONLY=1 EDITOR_AREA=external-write tests/adversarial/run.sh PASS editor external Note body write during a live room seed=25608414 ms=12391 PASS editor all areas seed=25608414 historySeeds=25608414 ``` The production build has six repair screenshots: 390, 820 and 1440 pixels, in light and dark schemes. They are attached to the issue. The orchestrator reviews visual quality. The captures are not in Git. One time-boxed robustness round ran the external-write area on a real fixture server. It passed. The final server gate passed the live-app child-process checks, which timed out in an earlier parallel test run on the shared host. ## Performance The run used a prebuilt release server on perf-test under `/root/perf.lock`. No build ran on the VM. Load average inside the lock was 0.14/0.12/0.08 before and 1.13/0.34/0.16 after the run. The fixture Instance was removed. Each case has 12 sequential writes and one eight-write burst. CPU and RSS refer to the server process. RSS is sampled, not a peak measure. | Metric | 64 blocks | 10,000 blocks | Matching baseline | |---|---:|---:|---| | Request-to-editor p50 | 71.70 ms | 546.10 ms | None | | Request-to-editor p95 | 159.10 ms | 755.58 ms | None | | Server CPU during sequential samples | 24.03% | 223.99% | None | | Server RSS after sequential samples | 111.84 MiB | 952.36 MiB | None | | Eight-write burst duration | 92.92 ms | 630.02 ms | None | | Server RSS after burst | 112.41 MiB | 952.55 MiB | None | | Successful / stale burst writes | 1 / 7 | 1 / 7 | None | `docs/perf/baseline.json` is at `369ab6a2f9fc673e3564b94857fbecfeb04df404`. It measures Notes import and route load, which are different operations. This is the first comparable external-write-to-editor profile. It does not establish a regression against that baseline. The first profile attempt stopped at its caret assertion: the fixture tail was below the viewport. The harness now scrolls before finding the hit point. The corrected profile passed, including one copy of text typed in each tail. ## Known gaps - Repair flags only identical adjacent root blocks with a shared explicit ID. Changed or nonadjacent copies require manual review. - The 10,000-block case used 952.55 MiB sampled server RSS. There is no matching prior profile or approved memory threshold for this operation. - The owner's original Note was not inspected. The fixture proves this failure mechanism; it does not establish every step in that Note's edit history. - Visual quality review remains with the orchestrator. ## Decisions - Use the existing Pill and toast for explicit repair; add no repair page. - Track only the repair transaction for Undo and use the existing eight-second Undo duration. Keep subsequent local and remote edits. - Retain the live block ID on conflict. Derive a unique ID for the inserted version so its identity is stable on replay. - Use 64 and 10,000 blocks for the new profile. Keep these results separate from import and route-load metrics in the existing baseline. Cleanup: `cargo clean` completed; web build output and fixture runtime/temp output were removed. Screenshots remain in ignored artifacts and are attached above. Production fixture captures for orchestrator review: - [duplicates-review-1440-paper-white.png](https://git.kayg.org/attachments/5029701b-24cc-4933-9137-dbe7d8cd806a) - [duplicates-review-1440-tokyo-night.png](https://git.kayg.org/attachments/5f035f59-7f29-4d2c-916c-491a1f7b242d) - [duplicates-review-390-paper-white.png](https://git.kayg.org/attachments/61602cfb-6572-4afc-a5d3-214e6861185d) - [duplicates-review-390-tokyo-night.png](https://git.kayg.org/attachments/1996406e-a6a2-4fb9-9ed8-59c1759020c5) - [duplicates-review-820-paper-white.png](https://git.kayg.org/attachments/a99297e7-bae6-4059-b12c-8593a6897bac) - [duplicates-review-820-tokyo-night.png](https://git.kayg.org/attachments/b9209ef5-bfd3-45ab-b1a8-94cd7ba0813a)
Author
Owner

Final head: a0a89f5fc719305108e465c6e0ac18817d645abf on job/dupwrite-634. Worktree is clean. The final documentation-only commit explains why the server-owned conflict-anchor helper does not change the shared Rust/TypeScript Markdown dialect. Functional code and gate results in the report above are unchanged.

cargo clean removed 26,611 files (17.2 GiB). Web build output and fixture runtime/temp output are removed. The perf VM fixture was removed and its lock released. No push or deploy.

Final head: `a0a89f5fc719305108e465c6e0ac18817d645abf` on `job/dupwrite-634`. Worktree is clean. The final documentation-only commit explains why the server-owned conflict-anchor helper does not change the shared Rust/TypeScript Markdown dialect. Functional code and gate results in the report above are unchanged. `cargo clean` removed 26,611 files (17.2 GiB). Web build output and fixture runtime/temp output are removed. The perf VM fixture was removed and its lock released. No push or deploy.
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#634
No description provided.