fix(authoring): stop losing writes that never reach the collab relay - #316
Merged
Conversation
Since persistence moved to the collab relay, a site mutation only survives if it is translated into Y operations. Three paths were not, and each failed the same way: the tool reported success, a readback confirmed the change, and the work was gone on reload. - `filesSlice` mutated the store with a raw `set`, so code assets never produced site patches. A script written through the tools left its runtime config in the database with `site.files` empty, so nothing was ever published — `site_inspect_code_runtime` even reported `applies: true` for a file the server did not have. Every action now routes through `mutateSiteState` like the other shell slices. - Site writes are refused wholesale while any bound doc is still on its first sync, which is correct — an unseeded doc would duplicate server content on merge. But the refusal was invisible to tool callers: a chain that creates a page and immediately fills it lands inside that window nearly every time, and got back the ids it meant to create. In one 13-document build, five documents came back empty this way. Mutating tools now await `whenCollabWritable` and report a real error if the gate never opens, and `site_insert_html` refuses to claim a subtree the store did not gain. - The Content workspace caches its collection roster at mount, so a post type created afterwards — by an import, another admin, or a connector building a site — was invisible and every write against it failed with "not found" until someone reloaded the page. The bridge now refreshes the roster once before rejecting an unknown id. Also: a duplicate row slug surfaced as an opaque 500 with a SQLite constraint error in the logs. It is an ordinary authoring conflict, so it answers 409 with the offending slug and the row that holds it. The write gate and the tool-classification tables move into their own modules to stay under the module-size ceiling. Co-Authored-By: Claude Opus 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.
Follow-up to #315, found by rebuilding the same 13-document site on merged
main. Since persistence moved to the collab relay (#305), a site mutation only survives if it is translated into Y operations — and three paths were not. All three failed identically: the tool reported success, a readback confirmed the change, and the work was gone on reload.The three losses
Code assets never persisted at all.
filesSlicemutated the store with a rawset, producing no site patches. Writing a runtime script left its config insite.runtime.scriptswithsite.filesempty — so nothing published, whilesite_inspect_code_runtimecheerfully reportedapplies: truefor a file the server did not have. The asymmetry is what made it confusing to diagnose: runtime config and files live on the same shell object, but only one went throughmutateSiteState.Writes during the first-sync window vanished. Site writes are refused while any bound doc is unsynced, which is correct — writing into an unseeded doc would duplicate server content on merge. But the refusal was invisible to tool callers. A human never notices a sub-second gate; a tool chain that creates a page and immediately fills it lands inside it nearly every time. In one 13-document build five documents came back empty, each having returned a full list of node ids.
A new collection was invisible until reload. The Content workspace caches its roster at mount, so a post type created afterwards — by an import, another admin, or a connector building a site — failed every write with "Collection not found". This cost a full round of seven failed entry creates on every run.
What changed
filesSliceroutes every action throughmutateSiteState, like the other shell slices.whenCollabWritable()and return an actionable error if the gate never opens;site_insert_htmladditionally refuses to claim a subtree the store did not gain.409naming the slug and the row that holds it, instead of an opaque500with a SQLite constraint error in the logs.The write gate and the tool-classification tables move into their own modules to stay under the module-size ceiling — no behavioral change.
Verification
bun test6457 pass / 0 fail;tsc -bandeslintclean.Still open
FW-001(Core Framework pixel anchors emitting oversized rem scales) andPUB-005(dynamic bindings inside HTML attributes publishing literally) reproduced again and are untouched here — both are their own investigation.🤖 Generated with Claude Code