Summary
Since the new write guard, diviops_module_update refuses and reverts correct writes whenever the serialized block contains characters that WordPress escapes in block-attribute JSON.
serialize_block_attributes() encodes <, >, &, quotes — and -- sequences (so they can't terminate the --> block comment) — as \u00xx. Each escape makes the stored bytes longer than the bytes the plugin sent. The guard appears to compare its own pre-escape expectation against the post-escape readback, concludes the content is corrupt, and rolls back a write that WordPress performed correctly.
The clearest single piece of evidence is case (C) below: WordPress stored exactly the intended change, and the plugin threw it away.
Environment
|
|
| diviops-agent (WP plugin) |
1.5.41-beta — regression appeared with this update; 1.5.4 wrote the same content fine |
| @diviops/mcp-server |
1.5.35 |
| Divi |
5.9.0 |
| WordPress / PHP |
7.0 / 8.4 |
Evidence
(A) freeForm CSS with child combinators — isolating the cause
Writing css.desktop.value.freeForm on a divi/section. The CSS contained 17 > characters:
{"ok":false,"error":{"code":"module.content_write_corruption",
"message":"Refused page #1901 module update content write because WordPress readback did not match the requested content.",
"hint":"The original content was restored. Inspect the corruption diagnostics before retrying this full-content write.",
"data":{"target":{"post_id":1901,"label":"page #1901 module update"},
"bytes":{"expected":134859,"stored":134944,"previous":135153},
"issues":[{"type":"byte_mismatch","first_mismatch_offset":2206,
"expected_bytes":134859,"stored_bytes":134944}],
"revert":{"attempted":true,"verified":true,"error":null}}}}
Delta = 85 = 17 × 5 — exactly the overhead of > → >.
Same CSS, all > removed from selectors, but one -> left inside a CSS comment:
"bytes":{"expected":135220,"stored":135225,"previous":135153},
"issues":[{"type":"byte_mismatch","first_mismatch_offset":3099,
"expected_bytes":135220,"stored_bytes":135225}]
Delta = 5 = 1 × 5. Still refused.
Removing that last > as well → write succeeded. This isolates the cause unambiguously.
(B) Same failure on a Theme Builder header layout
Writing freeForm CSS containing #menu-main>li>a and many .et-l--header (the -- gets escaped too):
{"ok":false,"error":{"code":"module.content_write_corruption",
"message":"Refused page #1003 module update content write because WordPress readback did not match the requested content.",
"data":{"target":{"post_id":1003,"label":"page #1003 module update"},
"bytes":{"expected":6753,"stored":6853,"previous":7051},
"issues":[{"type":"byte_mismatch","first_mismatch_offset":6213,
"expected_bytes":6753,"stored_bytes":6853}],
"revert":{"attempted":true,"verified":true,"error":null}}}}
Delta = 100.
(C) The serious one — a correct write, thrown away
Removing a font family ("Mulish", 6 characters) from a divi/text module whose innerContent is
<p style="font-family:Mulish;…">…</p>:
{"ok":false,"error":{"code":"module.content_write_corruption",
"message":"Refused page #1901 module update content write because WordPress readback did not match the requested content.",
"data":{"target":{"post_id":1901,"label":"page #1901 module update"},
"bytes":{"expected":135215,"stored":135243,"previous":135249},
"issues":[{"type":"byte_mismatch","first_mismatch_offset":10240,
"expected_bytes":135215,"stored_bytes":135243}],
"revert":{"attempted":true,"verified":true,"error":null}}}}
Look at stored vs previous: 135249 − 135243 = 6 — exactly the 6 characters of "Mulish" that were meant to disappear. WordPress stored precisely the intended result.
The plugin, however, expected 135215 (28 bytes fewer) and reverted it. Those 28 bytes are the escape overhead of that module's own HTML:
2 × < and 2 × > (<p … > … </p … >) at +5 each = 20, plus 2 × " (around the style value) at +4 each = 8. 20 + 8 = 28.
So the guard's expectation is computed on unescaped content, while the readback is escaped.
Impact
- No
divi/text module can be updated any more — virtually all of them carry markup in innerContent.
- No custom CSS containing a child combinator
> can be written. Workaround: descendant selectors plus :not(.wrapper .x .x). It is easy to trip over again — a single -> in a comment is enough.
- Theme Builder layouts are affected identically (case B).
To be fair: the rollback itself is reliable (revert.attempted: true, revert.verified: true) — nothing gets corrupted, no data is lost. The change simply never lands, while the error message suggests corruption. But it silently blocks a large part of the write API.
Steps to reproduce
- Take any
divi/text module whose content contains HTML (e.g. <p style="…">…</p>).
- Call
diviops_module_update with any trivial attribute change.
- →
module.content_write_corruption; the change is reverted although WordPress stored it correctly.
Equivalently: write any css.…freeForm value containing a >.
Suggested fix
Don't compare raw bytes across the escaping boundary. Either
- apply the same encoding WordPress uses (
serialize_block_attributes() — JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_QUOT | JSON_HEX_APOS, plus the -- handling) to the expected content before comparing, or
- compare the parsed blocks / attribute arrays instead of the serialized string.
As it stands, the guard reports corruption in exactly the case where WordPress did its job correctly.
Summary
Since the new write guard,
diviops_module_updaterefuses and reverts correct writes whenever the serialized block contains characters that WordPress escapes in block-attribute JSON.serialize_block_attributes()encodes<,>,&, quotes — and--sequences (so they can't terminate the-->block comment) — as\u00xx. Each escape makes the stored bytes longer than the bytes the plugin sent. The guard appears to compare its own pre-escape expectation against the post-escape readback, concludes the content is corrupt, and rolls back a write that WordPress performed correctly.The clearest single piece of evidence is case (C) below: WordPress stored exactly the intended change, and the plugin threw it away.
Environment
Evidence
(A) freeForm CSS with child combinators — isolating the cause
Writing
css.desktop.value.freeFormon adivi/section. The CSS contained 17>characters:{"ok":false,"error":{"code":"module.content_write_corruption", "message":"Refused page #1901 module update content write because WordPress readback did not match the requested content.", "hint":"The original content was restored. Inspect the corruption diagnostics before retrying this full-content write.", "data":{"target":{"post_id":1901,"label":"page #1901 module update"}, "bytes":{"expected":134859,"stored":134944,"previous":135153}, "issues":[{"type":"byte_mismatch","first_mismatch_offset":2206, "expected_bytes":134859,"stored_bytes":134944}], "revert":{"attempted":true,"verified":true,"error":null}}}}Delta = 85 = 17 × 5 — exactly the overhead of
>→>.Same CSS, all
>removed from selectors, but one->left inside a CSS comment:Delta = 5 = 1 × 5. Still refused.
Removing that last
>as well → write succeeded. This isolates the cause unambiguously.(B) Same failure on a Theme Builder header layout
Writing freeForm CSS containing
#menu-main>li>aand many.et-l--header(the--gets escaped too):{"ok":false,"error":{"code":"module.content_write_corruption", "message":"Refused page #1003 module update content write because WordPress readback did not match the requested content.", "data":{"target":{"post_id":1003,"label":"page #1003 module update"}, "bytes":{"expected":6753,"stored":6853,"previous":7051}, "issues":[{"type":"byte_mismatch","first_mismatch_offset":6213, "expected_bytes":6753,"stored_bytes":6853}], "revert":{"attempted":true,"verified":true,"error":null}}}}Delta = 100.
(C) The serious one — a correct write, thrown away
Removing a font family (
"Mulish", 6 characters) from adivi/textmodule whoseinnerContentis<p style="font-family:Mulish;…">…</p>:{"ok":false,"error":{"code":"module.content_write_corruption", "message":"Refused page #1901 module update content write because WordPress readback did not match the requested content.", "data":{"target":{"post_id":1901,"label":"page #1901 module update"}, "bytes":{"expected":135215,"stored":135243,"previous":135249}, "issues":[{"type":"byte_mismatch","first_mismatch_offset":10240, "expected_bytes":135215,"stored_bytes":135243}], "revert":{"attempted":true,"verified":true,"error":null}}}}Look at
storedvsprevious: 135249 − 135243 = 6 — exactly the 6 characters of"Mulish"that were meant to disappear. WordPress stored precisely the intended result.The plugin, however, expected
135215(28 bytes fewer) and reverted it. Those 28 bytes are the escape overhead of that module's own HTML:2 ×
<and 2 ×>(<p…>…</p…>) at +5 each = 20, plus 2 ×"(around thestylevalue) at +4 each = 8. 20 + 8 = 28.So the guard's expectation is computed on unescaped content, while the readback is escaped.
Impact
divi/textmodule can be updated any more — virtually all of them carry markup ininnerContent.>can be written. Workaround: descendant selectors plus:not(.wrapper .x .x). It is easy to trip over again — a single->in a comment is enough.To be fair: the rollback itself is reliable (
revert.attempted: true,revert.verified: true) — nothing gets corrupted, no data is lost. The change simply never lands, while the error message suggests corruption. But it silently blocks a large part of the write API.Steps to reproduce
divi/textmodule whose content contains HTML (e.g.<p style="…">…</p>).diviops_module_updatewith any trivial attribute change.module.content_write_corruption; the change is reverted although WordPress stored it correctly.Equivalently: write any
css.…freeFormvalue containing a>.Suggested fix
Don't compare raw bytes across the escaping boundary. Either
serialize_block_attributes()—JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_QUOT | JSON_HEX_APOS, plus the--handling) to the expected content before comparing, orAs it stands, the guard reports corruption in exactly the case where WordPress did its job correctly.