What happens now
When someone types quickly into a table while collaborating on a post, their own browser tab briefly stops responding — for one to three seconds on a slow or busy computer. Nothing is lost and everything catches up when the pause ends, but during the pause the tab is frozen: the letters stop appearing, and the other person stops receiving updates. This is the small remainder of a much bigger freeze that was fixed in #58 (issue #37): the fix removed about four fifths of the pause, and this issue is about the fifth that is left. The pause grows with the size of the post, so a much larger document could bring the multi-second freezes back even after that fix.
Example
There is no user-visible failure to reproduce today — the pause is short enough that every test passes. It is visible with the measurement tools added on branch loop/37:
- Start the test site and run the collaboration test slowed down three times, with the stall recorder on:
WP_BASE_URL=http://localhost:<tests-port> RTC_E2E_CPU_THROTTLE=3 npm run test:e2e -- collaboration-multi-client-content --trace on -g "intent-log.*mix of block types".
- Open the saved test trace and read the
long-tasks-page2 attachment — a list of every moment the typing user's browser stopped responding, with durations in milliseconds.
What you see: one pause of 3,000-7,400 milliseconds during the table part of the test (at three-times slowdown; roughly 1,000-2,500 milliseconds at normal speed).
What you expected: no pause a person could notice — nothing beyond a couple of hundred milliseconds.
What should happen instead
Typing should never visibly freeze the typist's own browser, no matter how big the post is or how fast they type.
How we will know it is done
The decision below has to be made first, so there is no command yet — needs a test. Once a direction is chosen, the check should be: run the step from the example above and confirm the biggest recorded pause on the typing user's page stays under about one second at three-times slowdown, and the existing gate from #37 keeps passing twice in a row: npm run test:e2e -- --repeat-each=8 -g "mix of block types".
Which decision is missing, and who makes it: the repository owner must pick a direction (or both) before an agent can start, because each one touches code agents may not change on their own. Direction one: make fewer copies (the change lives in the frozen shared code, like the #37 fix did — see notes). Direction two: make the copies smaller by sending less per keystroke (this changes what travels over the network, also reserved for humans).
Notes for whoever picks this up
Where the remaining pause comes from, in detail. The collaboration engine keeps every person's edits consistent by replaying a shared history of small changes. To merge safely, it must be able to look at the document as it stood at several points in that history — so before applying each incoming change, it makes a complete copy of the whole document and applies the change to the copy, leaving the original untouched. That is applyIntent in src/engines/intent-log/reducer.js calling cloneDocument in src/engines/intent-log/document.js, driven by the client's replay in src/engines/intent-log/client.js. Issue #37 made each copy about five times cheaper (the copy used to go through a very slow WordPress-supplied replacement for the browser's structuredClone function; it is now a hand-written walk over plain data). What #37 did NOT change is HOW MANY copies are made and HOW BIG each one is: still one full-document copy per applied change. A post-fix CPU profile (attached to the #37 trail) shows the remaining pause is almost entirely this copying — our own fast copy, just called very many times over a large document.
Why tables make it so much worse. In this engine, one keystroke in an ordinary paragraph produces one recorded change. One keystroke in a table cell or a table caption produces three — and two of those three carry the ENTIRE table's HTML inside them, because the table's markup is tracked as one formatted-text object that gets switched off and back on around every edit. So a 20-keystroke burst in a table caption means roughly 60 changes to replay, each triggering a full copy of a document whose table text is itself large. Copies-per-burst times bytes-per-copy is the multiplication that produces the pause. This three-changes-per-keystroke behavior was confirmed by direct measurement on 2026-08-24 (see #37's comment trail).
The two directions, with their constraints. Direction one, fewer or cheaper copies: for example, copying only the parts of the document a change actually touches and sharing the untouched parts between versions (copy-on-write), or caching document states so replay does not re-copy from scratch. All of that code is in the frozen intent-log core (src/engines/intent-log/), which agents may not modify without an explicit human go-ahead recorded on the issue — the same authorization step #37 went through for its one-line change; this direction is larger. The PHP server has the same replay structure (includes/engines/intent-log/), but PHP copies arrays by value automatically, so the two sides do not need to change in lockstep for this — behavior, not bytes, is the contract. Direction two, smaller changes: stop re-carrying the whole table HTML in the two extra per-keystroke changes. That means changing what a recorded change contains — the shape of what travels over the network and sits in storage — which the loop's rules reserve for humans; #37's notes also record the constraint that merging three changes into one would need either a new payload format or an extension of the frozen change vocabulary (replace_attr_content clears formatting and has no formatting payload).
What is already ruled out. This is NOT the WordPress polyfill problem — that was #37, fixed and verified in #58 with an A/B test. Do not revisit the server's history-trimming either; docs/plan/history.md lists trim-timing fixes as a dead end, and #37's trail shows the trim was not involved. The measurement tools (the slow-down switch RTC_E2E_CPU_THROTTLE, the pause recorder, and the CPU profiler switch RTC_E2E_CPU_PROFILE) live in tests/e2e/config/collaboration-fixtures.ts and attach their findings to every failing test automatically; #37's comments explain how to read them.
Numbers for calibration (all at three-times slowdown, from the #37 verification runs): before the #37 fix the pause was 15,000-37,000 ms; after it, 3,000-7,400 ms; the test suite's timeouts start biting at about 10,000 ms, which is why today's pause passes every test but leaves little headroom for bigger documents or slower machines.
Related: #37, #58, #38 (separate table-rendering symptom, different mechanism).
What happens now
When someone types quickly into a table while collaborating on a post, their own browser tab briefly stops responding — for one to three seconds on a slow or busy computer. Nothing is lost and everything catches up when the pause ends, but during the pause the tab is frozen: the letters stop appearing, and the other person stops receiving updates. This is the small remainder of a much bigger freeze that was fixed in #58 (issue #37): the fix removed about four fifths of the pause, and this issue is about the fifth that is left. The pause grows with the size of the post, so a much larger document could bring the multi-second freezes back even after that fix.
Example
There is no user-visible failure to reproduce today — the pause is short enough that every test passes. It is visible with the measurement tools added on branch
loop/37:WP_BASE_URL=http://localhost:<tests-port> RTC_E2E_CPU_THROTTLE=3 npm run test:e2e -- collaboration-multi-client-content --trace on -g "intent-log.*mix of block types".long-tasks-page2attachment — a list of every moment the typing user's browser stopped responding, with durations in milliseconds.What you see: one pause of 3,000-7,400 milliseconds during the table part of the test (at three-times slowdown; roughly 1,000-2,500 milliseconds at normal speed).
What you expected: no pause a person could notice — nothing beyond a couple of hundred milliseconds.
What should happen instead
Typing should never visibly freeze the typist's own browser, no matter how big the post is or how fast they type.
How we will know it is done
The decision below has to be made first, so there is no command yet — needs a test. Once a direction is chosen, the check should be: run the step from the example above and confirm the biggest recorded pause on the typing user's page stays under about one second at three-times slowdown, and the existing gate from #37 keeps passing twice in a row:
npm run test:e2e -- --repeat-each=8 -g "mix of block types".Which decision is missing, and who makes it: the repository owner must pick a direction (or both) before an agent can start, because each one touches code agents may not change on their own. Direction one: make fewer copies (the change lives in the frozen shared code, like the #37 fix did — see notes). Direction two: make the copies smaller by sending less per keystroke (this changes what travels over the network, also reserved for humans).
Notes for whoever picks this up
Where the remaining pause comes from, in detail. The collaboration engine keeps every person's edits consistent by replaying a shared history of small changes. To merge safely, it must be able to look at the document as it stood at several points in that history — so before applying each incoming change, it makes a complete copy of the whole document and applies the change to the copy, leaving the original untouched. That is
applyIntentinsrc/engines/intent-log/reducer.jscallingcloneDocumentinsrc/engines/intent-log/document.js, driven by the client's replay insrc/engines/intent-log/client.js. Issue #37 made each copy about five times cheaper (the copy used to go through a very slow WordPress-supplied replacement for the browser'sstructuredClonefunction; it is now a hand-written walk over plain data). What #37 did NOT change is HOW MANY copies are made and HOW BIG each one is: still one full-document copy per applied change. A post-fix CPU profile (attached to the #37 trail) shows the remaining pause is almost entirely this copying — our own fast copy, just called very many times over a large document.Why tables make it so much worse. In this engine, one keystroke in an ordinary paragraph produces one recorded change. One keystroke in a table cell or a table caption produces three — and two of those three carry the ENTIRE table's HTML inside them, because the table's markup is tracked as one formatted-text object that gets switched off and back on around every edit. So a 20-keystroke burst in a table caption means roughly 60 changes to replay, each triggering a full copy of a document whose table text is itself large. Copies-per-burst times bytes-per-copy is the multiplication that produces the pause. This three-changes-per-keystroke behavior was confirmed by direct measurement on 2026-08-24 (see #37's comment trail).
The two directions, with their constraints. Direction one, fewer or cheaper copies: for example, copying only the parts of the document a change actually touches and sharing the untouched parts between versions (copy-on-write), or caching document states so replay does not re-copy from scratch. All of that code is in the frozen intent-log core (
src/engines/intent-log/), which agents may not modify without an explicit human go-ahead recorded on the issue — the same authorization step #37 went through for its one-line change; this direction is larger. The PHP server has the same replay structure (includes/engines/intent-log/), but PHP copies arrays by value automatically, so the two sides do not need to change in lockstep for this — behavior, not bytes, is the contract. Direction two, smaller changes: stop re-carrying the whole table HTML in the two extra per-keystroke changes. That means changing what a recorded change contains — the shape of what travels over the network and sits in storage — which the loop's rules reserve for humans; #37's notes also record the constraint that merging three changes into one would need either a new payload format or an extension of the frozen change vocabulary (replace_attr_contentclears formatting and has no formatting payload).What is already ruled out. This is NOT the WordPress polyfill problem — that was #37, fixed and verified in #58 with an A/B test. Do not revisit the server's history-trimming either;
docs/plan/history.mdlists trim-timing fixes as a dead end, and #37's trail shows the trim was not involved. The measurement tools (the slow-down switchRTC_E2E_CPU_THROTTLE, the pause recorder, and the CPU profiler switchRTC_E2E_CPU_PROFILE) live intests/e2e/config/collaboration-fixtures.tsand attach their findings to every failing test automatically; #37's comments explain how to read them.Numbers for calibration (all at three-times slowdown, from the #37 verification runs): before the #37 fix the pause was 15,000-37,000 ms; after it, 3,000-7,400 ms; the test suite's timeouts start biting at about 10,000 ms, which is why today's pause passes every test but leaves little headroom for bigger documents or slower machines.
Related: #37, #58, #38 (separate table-rendering symptom, different mechanism).