Repository navigation
Fix intent log browser freeze on save - #7
Merged
Merged
Conversation
chriszarate
added a commit
that referenced
this pull request
Aug 15, 2026
…apability gaps Adapt the matrix runner to the trunk reorganization: the tests site now comes from its own single-site config (.wp-env.tests.json — cli/wordpress services, WP_ENV_PORT, a -tests- work-dir variant), and websocket combos displace the dev env's auto-started 8787 daemon for the run's duration (it serves the dev database; npm run env start brings it back). de-rtc joins the default engine matrix (9 combos). Its lanes initially reported reproducible divergence on every seed — all of it the DOCUMENTED no-title-sync gap (docs/engine-comparison.md: title edits stay local), so the runner now carries an ENGINE_CAPABILITIES map and disables title actions for engines that cannot sync them; untouched titles stay converged, so the oracle is unchanged. With the gap excluded, de-rtc passes 3 seeds × 12 steps on both http-polling and websocket. Re-verification on the rebased branch: intent-log/websocket 3/3 (the duplicate-delivery fix holds; the final-save hang is gone — fixed upstream by the sync-ID flush retry in #7). One open finding remains: intent-log/http-long-polling seed 1 still diverges reproducibly at step 9. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
chriszarate
added a commit
that referenced
this pull request
Aug 16, 2026
…date-loss bug it found (#13) * Add the engine × transport RTC fuzzer (npm run fuzz) Adapt the Gutenberg RTC browser fuzzing pipeline (danluu/gutenberg try/fuzz) to this plugin's pluggable architecture. tests/fuzzer/ holds the core that finds bugs — a seeded, bounded-action-grammar Playwright spec with per-step convergence, block-validity, and save/reload persistence oracles, plus HTTP-only transient sync-fault injection — and a matrix runner that sweeps it across every engine × transport combination. Per combo the runner flips wp_sync_engine and the transport option via wp-cli on the tests site, wipes wp_sync_storage rooms so no combo inherits another engine's room lineage, and (for websocket combos) runs the wp collaboration sync-server daemon through the generated compose file with the port published and the daemon bound to 0.0.0.0, restarted per combo because a long-running PHP process caches options at boot. Failing seeds are rechecked once with traces on to split reproducible failures from flakes; reproducible ones are grouped by normalized signature with a replay command in summary.md. The deliberate deltas from upstream are documented in the README: the campaign/triage machinery (tmux supervisors, LLM tiers, novelty monitors, operation ledger) is dropped; engine-neutral oracles replace the fork's _crdt_document assertions; discovery over websocket waits on the awareness UI because the fixture's wp-sync HTTP response wait never fires when sync rides WS frames. Smoke-tested across the full 6-combo matrix (1 seed × 4 steps): 5 lanes green, and the sweep immediately surfaced a reproducible finding — under intent-log/websocket a block insert fails to propagate to the peer while the identical seed passes on both HTTP transports (fuzzer artifacts hold the traces). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Fix intent-log update loss over websocket: duplicate row delivery desynced the replica Found by the engine × transport fuzzer on its first sweep (intent-log/ websocket, seed 1): one client's block insert never reached its peer while the identical seed passed over both HTTP transports. WebSocket frame capture showed the server voiding the intent with disposition invalid-payload — its baseSeq was above the server's log head. The head inflation came from duplicate delivery: the daemon answers a client's frame from the CLIENT-supplied after cursor, but a broadcast (triggered by the other socket's message) had already pushed newer rows and advanced the socket's server-side cursor while that frame was in flight. The overlapping windows redelivered a row, and the session layer appends every received row at the replica cursor (the frozen core's clientReceive contract is positional), so the duplicate counted as a new log entry. From then on every local intent based above the server head and voided — silent, permanent divergence. Yjs is immune (CRDT updates are idempotent), which is why only the intent-log lane failed. Two-layer fix: - The daemon clamps each room request's after cursor to the socket's server-side cursor, making per-socket delivery monotonic. Reconnects still resync client-driven: a new socket subscribes at cursor 0, so the client's own cursor is honored. - The intent-log session codec now tracks absorbed rows by intentId (unique per log entry) and skips redelivered intent and proposal rows — defense in depth against any transport that redelivers. The set resets on a horizon-reset re-bootstrap, where post-checkpoint rows are new to the rebuilt replica. The new session test redelivers an accepted row and then authors: without the guard the follow-up intent is voided; with it the sessions converge. Verified end to end: the previously ~50%-failing two-user repro passed 10/10 against the fixed daemon, and the fuzz matrix (3 seeds × 12 steps × 6 combos) shows the original signature gone with yjs-server/websocket unaffected. Jest 340 passed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Fuzzer: target the split tests wp-env, sweep de-rtc, respect engine capability gaps Adapt the matrix runner to the trunk reorganization: the tests site now comes from its own single-site config (.wp-env.tests.json — cli/wordpress services, WP_ENV_PORT, a -tests- work-dir variant), and websocket combos displace the dev env's auto-started 8787 daemon for the run's duration (it serves the dev database; npm run env start brings it back). de-rtc joins the default engine matrix (9 combos). Its lanes initially reported reproducible divergence on every seed — all of it the DOCUMENTED no-title-sync gap (docs/engine-comparison.md: title edits stay local), so the runner now carries an ENGINE_CAPABILITIES map and disables title actions for engines that cannot sync them; untouched titles stay converged, so the oracle is unchanged. With the gap excluded, de-rtc passes 3 seeds × 12 steps on both http-polling and websocket. Re-verification on the rebased branch: intent-log/websocket 3/3 (the duplicate-delivery fix holds; the final-save hang is gone — fixed upstream by the sync-ID flush retry in #7). One open finding remains: intent-log/http-long-polling seed 1 still diverges reproducibly at step 9. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Fuzzer: converge on serialized content, not raw attribute objects The long-polling lane's reproducible 'divergence' (seed 1) was the oracle being stricter than persistence: after a reload, intent-log's snapshot materialization reifies empty rich-text fields as explicit "" attributes (values on lists, value/citation on quote) that createBlock-authored trees omit. Both representations serialize to byte-identical content — verified by rebuilding both captured states in a live editor — so no user or save can distinguish them; only the raw-attribute JSON comparison could. The asymmetry surfaced on long-polling because its traffic pattern keeps the two representations stable through the convergence window, while polling's cadence washes it out. The spec now compares each page's SERIALIZED content + title (what actually persists) instead of the fixture's normalized attribute trees, keeping the invalid-block invariant unchanged. intent-log then passes 3 seeds × 12 steps on all three transports — including the previously failing long-polling seed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Previously when a block editor change arrived during a save or autosave,
flushSyncIds()would reschedule itself with:This runs as a microtask, which then the browser runs next, then reschedules itself, and runs again, refilling the microtask queue indefinitely and not allowing the post save state to resolve:
Click to expand flushSyncIds loop
Instead, on retry use a
setTimeout()to schedule a macro task that allows the post state to resolve. Also added a minor delay (100ms) as it doesn't appear to have an inherent risk to the design and gives more time for state to settle.