Skip to content

Rewrite-storm validation — 2026-09-24

Issue #179 is exercised by two real sync engines over the strict fake server, with the host plugin from S89 represented by a one-second debounced front matter rewrite. stamper.test.mjs covers both explicit Resume orders, an editor receiving twenty keystrokes while the hold propagates, sequential background answers, and a fixed-width rewrite that leaves modification time and size unchanged. The paused note stops publishing on both updated devices; each device gives one pause notice, and the main note contains every typed character after Resume. The tests also cover ordinary two-editor typing, restart persistence, failed resume, and the status dialog's actual Resume button. pause.test.mjs checks control binding, replay, deduplication, concurrent opposite controls, and the guards that preserve held content.

The opening results are automated engine and dialog evidence. Native desktop observations follow below; no iPhone S89 result is inferred from them.

Native finding and correction

Two unmodified Obsidian 1.13.7 instances used isolated synthetic vaults and a locally installed fixture plugin calling processFrontMatter once per second. Twenty letters were typed through the native editor while both stampers ran. Both devices paused the note. During a measured five-minute hold, the server received no further note-version POST, the note's stored record stayed fixed, and the local stampers continued changing their timestamps. All twenty typed letters remained in the main note.

After both stampers were disabled through Community plugins, Resume was pressed on the first device and then the second. The main note converged with all twenty letters, but two conflict copies remained, exceeding S89's one-copy limit. One copy preserved the other device's earlier server head; the second preserved its later held timestamp. Another five-minute quiet window showed stable records and copies. This is a failed copy-count result, not a native pass inferred from the automated suite.

The reproduction is now included in the follow-up work, covering both Resume orders, an upload already in flight at pause, and stampers that continue locally during the five-minute hold. The final corrected desktop rerun is recorded below; these earlier failed observations are retained.

A subsequent native run on bundle 7557e642f146659c8dfe3cf0e974f405d8ca904d6364898102ddaf5a92d3efed found a second case before the final acceptance run: both devices had the target note open, but only the first person typed. Both timestamp fixtures were verified enabled. The note did not pause, its main file held sixteen of the twenty letters, and six conflict copies remained. Complete twenty-letter text survived in four copies, so the observation is not total text loss; it still fails the common-main-note and one-copy conditions. The fixtures were then disabled and every file retained for diagnosis. The detector's open-editor exemption was corrected to distinguish actual input from a passive view.

Final native desktop rerun

Bundle SHA-256 bd68df43b5ab64beaffc13803bcc6d95bfafea2ccf96664414c6102afc879fc1 passed the desktop reproduction on a fresh target note with no pre-existing conflict copy. Two isolated profiles of unmodified Obsidian 1.13.7 on one macOS 27 laptop used the one-second front-matter fixture. Both editors were open. Native key presses entered ABCDEFGHIJKLMNOPQRST, with a 500 ms delay after each key, into the first editor while both fixtures were enabled.

Both clients held the note. All twenty characters stayed together in the first main note, and no conflict copy appeared during the hold. Eleven samples over 300.22 seconds showed zero new note-version POSTs and unchanged stored file records while the local timestamp fixtures continued rewriting.

Both fixtures were then disabled through Community plugins and their disabled state verified. One Sync now on B, followed by one on A, cleared both holds. Both main notes contained all twenty characters; both devices held exactly one identical conflict copy preserving B's held text. The main-note SHA-256 on both was 762f3a9e222fdcfee0d0776e9830591c94d95ebd2a8b73e63fb52ceb44220d79; the copy was 3d03c59cd3da366bf52bdfa40ccbb8d2343437fe7313106b93d7a350a3bc43dd. Eleven further samples over 300.32 seconds found zero new note-version POSTs, unchanged main notes and file records, one copy each, and no hold. Both native status bars were idle.

The local evidence files are 179-overlap-typing-times.json, 179-overlap-five-minute-hold.json, 179-overlap-resume-result.json, and 179-overlap-five-minute-quiet.json. The repair record explains the editor-overlap guard and its mutations. Both Resume orders pass the automated suite; this native rerun exercised B first only. Two application instances on one laptop do not establish two physical computers or the required iPhone S89 result. The subsequent desktop and phone run completes that case on the final candidate.

Older-client compatibility

The unchanged 1.1.1 source at f420d4d6e0f3071fc2f897ca30d86446fda254e6 was built under Node 26.8.2 and the pinned TypeScript compiler. Its engine received a v3 pause control followed by an ordinary note. The control was refused with reason=version, no file was written at the control's path, the later note was applied, and the feed cursor advanced to 3 (control sequence 2, note sequence 3). The check runs through the old engine and old decoder, not through a replacement decoder in the new client.

Reproduce from this checkout, using a fresh scratch directory:

legacy_dir="$(mktemp -d)"
git archive 1.1.1 plugin/src plugin/tsconfig.json plugin/vendor plugin/test/fake.mjs | tar -x -C "$legacy_dir"
plugin/node_modules/.bin/tsc -p "$legacy_dir/plugin/tsconfig.json"
node plugin/test/legacy-pause-check.mjs "$legacy_dir/plugin"

The receipt is:

{"client":"1.1.1","controlSequence":2,"laterSequence":3,"cursor":3,"unsupportedControl":"refused","followingNote":"applied","feed":"advanced"}

Old clients keep syncing ordinary notes and do not participate in the shared hold. All devices must update to stop a rewrite storm everywhere.

Mutation equivalence

M567 was considered, then omitted before the mutation run: deleting only recorded.deleted || from the resume fallback condition is equivalent. decryptRecordManifest first invokes bindManifestToRecord, which refuses a deleted manifest unless chunks.length === 0 and size === 0. Consequently every validated tombstone necessarily satisfies the same fallback's remaining recorded.chunks.length !== 1 condition. No accepted input can distinguish that deletion. It is not counted as a killed mutant. The deleted, moved and multi-chunk baseline cases remain covered explicitly by pause.test.mjs; the independent path and chunk-count conditions have behavioral probes.

Initial executor verification

Node 26.8.2 and TypeScript 5.9.3: the initial restored build passed, followed by 994/994 plugin tests, with zero failures, cancellations or skips. The inherited worktree baseline passed 966/966. The final composed candidate subsequently passed make check, including 1,100/1,100 plugin tests, 144 core tests, 375 server tests, two CLI tests, 70 dashboard tests, 767 contract tests, 94.73% Rust line coverage, and both secret scans. One core benchmark is intentionally ignored. The evidence is final-overlap-make-check.log. The complete final mutation matrix is recorded separately; the original focused counts below are not presented as its result.

All 69 non-equivalent behavioral probes compile and fail directly relevant tests through plugin/test/mutants/run.sh. M484/M485 were recut to current source context; M555 onward was rerun after replacing an inherited shared compiler symlink with isolated pinned dependencies. No patch failure or compiler failure is counted. The table separates relevant failing tests from collateral failures observed in the parked-note and upgraded-device tests during some mutation runs; the final unmutated full suite is clean.

Probe Relevant failures Collateral failures
M480 5 0
M481 5 0
M482 4 0
M483 1 0
M484 1 0
M485 1 1
M486 2 0
M487 5 0
M488 1 0
M489 2 0
M490 5 0
M491 3 0
M492 1 0
M493 1 0
M494 4 0
M495 3 0
M496 5 0
M497 8 0
M498 1 0
M499 1 0
M530 4 0
M531 4 0
M532 6 0
M533 1 0
M534 2 0
M535 2 1
M536 1 0
M537 1 0
M538 1 0
M539 1 0
M540 1 0
M541 1 0
M542 1 0
M543 1 0
M544 1 0
M545 1 1
M546 1 0
M547 1 1
M548 1 0
M549 1 1
M550 2 1
M551 1 0
M552 5 0
M553 1 0
M554 2 0
M555 1 0
M556 6 0
M557 1 0
M558 1 1
M559 1 0
M560 1 0
M561 1 1
M562 1 0
M563 2 0
M564 2 0
M565 1 0
M566 1 2
M568 1 1
M569 1 0
M570 1 0
M571 1 0
M572 1 0
M573 2 0
M574 1 0
M575 1 0
M576 1 0
M577 1 1
M578 1 0
M579 1 0