fix(agent): merge a model save into the definition it updates - #226
Merged
Conversation
A chat-agent save_model call that carried only the field being added replaced the entire model definition, and auto-merge landed the truncation on main without a check. On Contentrain/iterum an "add one image field" request wiped 38 of 39 fields from home-page while every entry still used them; an earlier save on the same model had dropped pricing_preview the same way. The tool said "create or update", the engine passed the payload straight to planModelSave, and validation looked at the new definition alone. saveModel now reads the definition on contentrain and merges: omitted fields and top-level keys (title_field, description, form) are kept, a sent field is merged property by property, and only removeFields drops one. A removal, type change, kind change or i18n change that still has content behind it is refused with the affected entry count — read from the model's content across its locales, and only when a candidate exists — unless allowBreaking is passed. The result carries what the save did to the field list so the agent reports it exactly. The tool schema now says updates merge, documents label/order and title_field, and adds remove_fields/allow_breaking; the conversation engine splits those flags off the definition and treats a refused save as a hard error like save_content. MCP's own contentrain_model_save still replaces — that is MCP's contract, noted in CLAUDE.md. Closes #212
This was referenced Sep 2, 2026
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.
Summary
Closes #212.
save_modelwith a payload that carried only the field being added replaced the whole definition and auto-merged the truncation tomain(iterum: 38 of 39 fields gone fromhome-page, content untouched). Three things were missing — the tool never said it replaced, nothing merged with the existing definition, and nothing checked the content. All three are addressed at the engine, so the API PATCH route and the ee Conversation API get the same behaviour.content-engine/model-merge.ts,save-model.ts).saveModelreads the definition oncontentrainand merges: omitted fields and top-level keys (title_field,description, a form config) are kept; a sent field is merged property by property, so addinglabeldoes not droprequired; removal is only viaremoveFields.allowBreakingoverrides. The refusal names the field and the count.title_fieldis validated with the same rule as MCP and the PATCH route (must name a text-capable field;keyon dictionaries). Absent is still allowed — models predate the field.save_modeldescription states the merge semantics, documentslabel/order(closing the agent-side gap for Let models declare field order instead of rendering the edit form alphabetically #204/Per-field display label, so editors see "Herkese açık gövde" instead of body_public #205), addstitle_field,remove_fieldsandallow_breakingto the schema; a prompt rule (rules.model_update_merge) tells the agent to ask before retrying withallow_breaking. The conversation engine splits the flags off the definition and turns a refused save into a hard error, assave_contentalready did (before, a refused model save went on to call merge with an empty branch).modelChange(addedFields/changedFields/removedFields/keptFields) so the agent reports exactly what happened.Not changed:
detectBreakingChangesin the health audit keepsfield_removedas a warning — that path scores a project's state after the fact; the gate here is at write time with a precise count. MCP's owncontentrain_model_save(and so MCP Cloud) still replaces per MCP's contract; noted in CLAUDE.md.Test plan
tests/unit/model-merge.test.ts— merge rules, change summary, candidates, usage filtering, messages, title-field ruletests/unit/content-engine.test.ts— the exact iterum payload keeps 39→40 fields; used-field removal refused with count; unused field dropped; confirmed removal proceeds; type change refused counting entries once across locales; bad title field refused; new model created as senttests/unit/agent-tools.test.ts— schema carries the new handlesnuxt typecheck, fullpnpm test(157 files green)🤖 Generated with Claude Code
https://claude.ai/code/session_01Chid3TnLdPHjsKip4n63Ky