Skip to content

fix(authoring): stop losing writes that never reach the collab relay - #316

Merged
DavidBabinec merged 1 commit into
mainfrom
fix/mcp-collab-authoring-losses
Jul 30, 2026
Merged

fix(authoring): stop losing writes that never reach the collab relay#316
DavidBabinec merged 1 commit into
mainfrom
fix/mcp-collab-authoring-losses

Conversation

@DavidBabinec

Copy link
Copy Markdown
Contributor

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. filesSlice mutated the store with a raw set, producing no site patches. Writing a runtime script left its config in site.runtime.scripts with site.files empty — so nothing published, while site_inspect_code_runtime cheerfully reported applies: true for 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 through mutateSiteState.

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

  • filesSlice routes every action through mutateSiteState, like the other shell slices.
  • Mutating tools await whenCollabWritable() and return an actionable error if the gate never opens; site_insert_html additionally refuses to claim a subtree the store did not gain.
  • The content bridge refreshes the roster once before rejecting an unknown collection id.
  • A duplicate row slug now answers 409 naming the slug and the row that holds it, instead of an opaque 500 with 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 test 6457 pass / 0 fail; tsc -b and eslint clean.
  • New tests: the write gate (waits, resolves, and reports failure rather than hanging) and collection resolution (refreshes once, skips the refresh when already known, still reports genuinely missing ids).
  • Live rebuild of the full pack against a patched instance:
    • 12 of 13 documents persisted on first insert, versus 8 before (the 13th is the seeded starter homepage, correctly untouched);
    • all 7 entries created in a brand-new post type with no workspace reload;
    • the runtime script persisted to the database (6205 bytes), published, and stayed scoped to its one page.

Still open

FW-001 (Core Framework pixel anchors emitting oversized rem scales) and PUB-005 (dynamic bindings inside HTML attributes publishing literally) reproduced again and are untouched here — both are their own investigation.

🤖 Generated with Claude Code

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>
@DavidBabinec
DavidBabinec merged commit c35fd8f into main Jul 30, 2026
9 checks passed
@DavidBabinec
DavidBabinec deleted the fix/mcp-collab-authoring-losses branch July 30, 2026 20:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant