You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Keep unchanged blocks when a connected app replaces a section #347
Today replace_section deletes every block under a heading and inserts new copies. After #329, a connected app is refused on any section that holds media, even for a one-typo fix. This issue keeps each block the connected app did not change. The refusal then applies only when a changed block holds media, and History keeps the authors of the kept blocks.
This is a Later item. Build it only when the maintainer opens its gate on this issue.
Acceptance criteria
The maintainer has posted on this issue that the gate is open. The post names the half to build: tool, applier, or both.
Tool half: a replace_section call that changes one paragraph in a section with a picture is applied. The picture stays, with its size and alignment.
Type: HITL — the maintainer reads the 30-day count of replace_section:refused-section-media, which #329 adds. The exact count is that field in the mcp:usage:<day> Redis hashes. The admin dashboard page /mcp (30d) shows it inside the replace_section "Refused" column, together with other refusals. The maintainer then posts on this issue which half to build. Proposed gate: 20 refusals in 30 days opens the tool half. One written report of lost text opens the applier half. The maintainer may change these numbers.
Category: refactor
Current behavior:
replace_section parses the Markdown, checks rev against a pre-read, then calls deps.content.apply with mode: 'section' (apps/hocuspocus.server/src/modules/mcp/tools/documentTools.ts:283-305).
The applier clones every new node, deletes the whole section body, and inserts the clones (apps/hocuspocus.server/src/modules/document-content/domain/applyContentToDoc.ts:60-93). Each new item carries the server's own clientID. So History Authors shows "Not recorded" for the whole section (apps/webapp/src/components/pages/history/components/HistoryAuthorsBody.tsx:185).
read_document shows media as a placeholder such as [image] (apps/hocuspocus.server/src/modules/mcp/domain/redactMedia.ts). An agent sees only the placeholder, so a rewrite cannot keep the media.
The section rev hashes the heading and body as key-sorted JSON (document-content/domain/sections.ts:38-62). The applier refuses any mismatch (applyContentToDoc.ts:31-32). So a co-author's typing that reached the room gives conflict, not lost text. Only keystrokes in flight at the moment of the write are lost.
Public PATCH /content accepts only replace and append (document-content/http/schema.ts:24-26). section mode exists only on the internal hop (:44), and its one caller is replace_section. Restore calls the applier in replace mode (document-versions/infra/versionOps.ts:218).
The usage store keeps counts for 35 days, keyed ${tool}:${outcome} (mcp/infra/usageStore.ts:6, :29).
Desired behavior:
Tool half, in replace_section only, before deps.content.apply:
Take the live section body from the pre-read that the rev check already uses.
Round-trip the whole body: redactMedia, then exportMarkdown, then parseMarkdown. Round-trip the whole body, not one block at a time, because two lists side by side can merge when parsed together.
If the round trip gives a different block count than the live body, fall back to today's full replace.
Match the common first blocks, then the common last blocks, by key-sorted JSON. Cap the middle run; above the cap, fall back.
Put the live block's full JSON in place of each matched new block. The applier's rev check proves the pre-read still equals the live section.
Applier half, in section mode only: keep a live Yjs item when its key-sorted JSON equals the new block. Delete and insert only the other runs, from the end backwards. Keep the rule "clone before any mutation" (applyContentToDoc.ts:69-73).
The reply to the connected app still carries only slug and version (documentTools.ts:88-91).
Where to start:
replace_section and parseFragment in apps/hocuspocus.server/src/modules/mcp/tools/documentTools.ts.
applyContentToDoc and locateSection in apps/hocuspocus.server/src/modules/document-content/domain/applyContentToDoc.ts.
canonicalJson and sectionAt in apps/hocuspocus.server/src/modules/document-content/domain/sections.ts.
redactMedia in apps/hocuspocus.server/src/modules/mcp/domain/redactMedia.ts.
exportMarkdown in document-conversion/domain/markdownExport.ts and parseMarkdown in document-conversion/domain/markdownImport.ts.
canonicalJson is not exported today. Export it; do not copy it.
Put the matcher in one pure function in mcp/domain/.
Line numbers are hints as of 2026-09-28; the agent searches by symbol.
Rules that apply:
apps/hocuspocus.server/CLAUDE.md §MCP Connector ("A write keeps its heading").
apps/webapp/CLAUDE.md §Document Version History (block authorship: never write a DocumentClientAuthor row for a server clientID).
The matcher is dense branching logic (trim, cap, count fallback), so AGENTS.md §Test Policy case (c) allows one unit test beside apps/hocuspocus.server/src/modules/mcp/__tests__/unit/outline.test.ts. Cover: one changed middle paragraph, a changed media block, a count mismatch, and the cap fallback. Prove it by sabotage: make the matcher return "all matched", and the test must fail.
cd apps/hocuspocus.server && bun test src/modules/mcp && bun run typecheck, then bun run lint and bun run format from the repo root.
Local end to end: run make dev-local, then claude mcp add --transport http docs-plus-local http://localhost:4000/api/mcp. Run /mcp in Claude Code and sign in as the pad owner. In a pad you own, make a section with two paragraphs and one picture. Ask Claude to fix a typo in the second paragraph. The picture stays in the pad. For the applier half, open History and check that the kept blocks name their author.
Superseded by positioned edits in cb1b54f (on main, deployed 2026-10-06). replace_section is gone. edit_blocks changes only the blocks it names and refuses to remove media, so unchanged blocks are kept by design.
Parent
#328. Related: #229
What to build
Today
replace_sectiondeletes every block under a heading and inserts new copies. After #329, a connected app is refused on any section that holds media, even for a one-typo fix. This issue keeps each block the connected app did not change. The refusal then applies only when a changed block holds media, and History keeps the authors of the kept blocks.This is a Later item. Build it only when the maintainer opens its gate on this issue.
Acceptance criteria
replace_sectioncall that changes one paragraph in a section with a picture is applied. The picture stays, with its size and alignment.PATCH /contentand Restore still replace every node, as today.apps/hocuspocus.server/API.md,docs/mcp/reference.mdandapps/hocuspocus.server/CLAUDE.md§MCP Connector describe the new behaviour.Blocked by
Agent brief
Type: HITL — the maintainer reads the 30-day count of
replace_section:refused-section-media, which #329 adds. The exact count is that field in themcp:usage:<day>Redis hashes. The admin dashboard page/mcp(30d) shows it inside thereplace_section"Refused" column, together with other refusals. The maintainer then posts on this issue which half to build. Proposed gate: 20 refusals in 30 days opens the tool half. One written report of lost text opens the applier half. The maintainer may change these numbers.Category: refactor
Current behavior:
replace_sectionparses the Markdown, checksrevagainst a pre-read, then callsdeps.content.applywithmode: 'section'(apps/hocuspocus.server/src/modules/mcp/tools/documentTools.ts:283-305).apps/hocuspocus.server/src/modules/document-content/domain/applyContentToDoc.ts:60-93). Each new item carries the server's own clientID. So History Authors shows "Not recorded" for the whole section (apps/webapp/src/components/pages/history/components/HistoryAuthorsBody.tsx:185).read_documentshows media as a placeholder such as[image](apps/hocuspocus.server/src/modules/mcp/domain/redactMedia.ts). An agent sees only the placeholder, so a rewrite cannot keep the media.revhashes the heading and body as key-sorted JSON (document-content/domain/sections.ts:38-62). The applier refuses any mismatch (applyContentToDoc.ts:31-32). So a co-author's typing that reached the room givesconflict, not lost text. Only keystrokes in flight at the moment of the write are lost.PATCH /contentaccepts onlyreplaceandappend(document-content/http/schema.ts:24-26).sectionmode exists only on the internal hop (:44), and its one caller isreplace_section. Restore calls the applier inreplacemode (document-versions/infra/versionOps.ts:218).${tool}:${outcome}(mcp/infra/usageStore.ts:6,:29).Desired behavior:
replace_sectiononly, beforedeps.content.apply:revcheck already uses.redactMedia, thenexportMarkdown, thenparseMarkdown. Round-trip the whole body, not one block at a time, because two lists side by side can merge when parsed together.revcheck proves the pre-read still equals the live section.sectionmode only: keep a live Yjs item when its key-sorted JSON equals the new block. Delete and insert only the other runs, from the end backwards. Keep the rule "clone before any mutation" (applyContentToDoc.ts:69-73).slugandversion(documentTools.ts:88-91).Where to start:
replace_sectionandparseFragmentinapps/hocuspocus.server/src/modules/mcp/tools/documentTools.ts.applyContentToDocandlocateSectioninapps/hocuspocus.server/src/modules/document-content/domain/applyContentToDoc.ts.canonicalJsonandsectionAtinapps/hocuspocus.server/src/modules/document-content/domain/sections.ts.redactMediainapps/hocuspocus.server/src/modules/mcp/domain/redactMedia.ts.exportMarkdownindocument-conversion/domain/markdownExport.tsandparseMarkdownindocument-conversion/domain/markdownImport.ts.canonicalJsonis not exported today. Export it; do not copy it.mcp/domain/.Line numbers are hints as of 2026-09-28; the agent searches by symbol.
Rules that apply:
apps/hocuspocus.server/CLAUDE.md§MCP Connector ("A write keeps its heading").apps/webapp/CLAUDE.md§Document Version History (block authorship: never write aDocumentClientAuthorrow for a server clientID)..cursor/skills/tech-writer/SKILL.md§Simplified English.Verify:
apps/hocuspocus.server/src/modules/mcp/__tests__/unit/outline.test.ts. Cover: one changed middle paragraph, a changed media block, a count mismatch, and the cap fallback. Prove it by sabotage: make the matcher return "all matched", and the test must fail.cd apps/hocuspocus.server && bun test src/modules/mcp && bun run typecheck, thenbun run lintandbun run formatfrom the repo root.make dev-local, thenclaude mcp add --transport http docs-plus-local http://localhost:4000/api/mcp. Run/mcpin Claude Code and sign in as the pad owner. In a pad you own, make a section with two paragraphs and one picture. Ask Claude to fix a typo in the second paragraph. The picture stays in the pad. For the applier half, open History and check that the kept blocks name their author.Out of scope
revto avoidconflictin busy rooms. It is a larger design with its own loss risk.PATCHand Restore. They keep today's full clone.