Concurrent & Cross-Session Editing
A single document can be open in more than one place at the same time: two browser tabs, a user editing in your UI while a background job rewrites the same file, or any integration that both writes and reads a document on separate connections. Without coordination, the last writer silently wins and someone’s work disappears. SuperDocs gives you the pieces to keep those copies converged — a lightweight change feed to learn when another session edited a document you hold, and a human autosave endpoint so pure typing is persisted and announced. This guide is the integrator pattern for wiring them together.You only need this guide if the same document can be edited from two places concurrently. If each session owns its own documents, skip it — there’s nothing to reconcile. For the per-section apply discipline this builds on, see Applying AI updates safely.
The model
- Every open document lives in a session and has a stable
document_idand a committed version. - When any session commits a change to a document (an AI turn, or a human autosave), other sessions that hold the same document can learn about it by polling a change feed.
- On a change, you re-fetch that one document and apply it per section onto your live editor — never a whole-document reload, which would wipe whatever the user is typing right now.
window_id (optional) — disambiguate two windows of the same session. chat, chat_async, revert, and redo accept an optional window_id. If you run two windows of the same session_id (rather than two separate sessions), set a stable per-window id so the server can tell concurrent writers apart and merge their edits instead of letting one overwrite the other. It’s optional — the per-section apply discipline in this guide works without it, and most integrators using one session per place don’t need it — but it’s the precise lever for the same-session, multi-window case.Polling for other sessions’ edits
after_id. Poll it roughly every 1–2 seconds while a document is open.
after_idis your cursor. Start at0(or omit it), then pass back the highest eventidyou’ve seen so each poll only returns what’s new.include_own— by default the feed excludes this session’s own writes (you already have your own changes; no need to echo them back). A REST or MCP integrator that writes on one connection and polls on another — or just wants a complete feed for every document it holds — should passinclude_own=true. Otherwise you’d never hear about edits your own background worker made on a different connection.
This feed is REST-only by design. It is intentionally not an MCP tool and not a held SSE connection — it’s a cheap stateless poll, which keeps long-lived connections free. (The SSE stream still carries your own turn’s events like
documents_changed; the doc-events feed is specifically for changes that originate in other sessions.)Event types
Each event names adocument_id and what happened to it:
| Event | Meaning | What to do |
|---|---|---|
document_updated | Another session committed content changes to this document. | Re-fetch the document and apply its changed sections (below). |
document_removed | Another session archived (soft-deleted) the document. | Drop the tab, or — if the user is mid-edit in it — offer a keep-editing choice (see Restoring a removed document). |
document_restored | A previously archived document was brought back. | Re-add the tab and load its current content. |
document_renamed | The document’s title changed. | Update the tab label. No content fetch needed. |
Applying a received change
When you get adocument_updated event:
- Re-fetch that one document. Read it from the roster with the body included —
GET /v1/sessions/{session_id}/documents?include_html=true— or from session history. Fetch only the document the event named, not the whole roster’s bodies. - Apply per section — never whole-replace. Replace only the
data-chunk-idblocks that actually differ from what you currently render, and leave every other block — including the section the user is typing in right now — untouched. Reloading the entire editor would throw away unsaved local edits. This is the same section-level discipline used for AI results; see Applying AI updates safely. - Advance your cursor to the event’s
idso the next poll doesn’t replay it.
Saving human edits (autosave)
Pure typing — edits a human makes without an AI turn — is persisted through the autosave endpoint:html. Critically, also send base_html — the document HTML as it was before this round of edits (the last version you loaded or saved) — so the change is recorded precisely (only what actually changed) instead of as a blunt whole-document overwrite. Omitting base_html still saves but loses that precision.
- Debounce and coalesce per document. Don’t fire a save on every keystroke. Debounce (and save on blur), and collapse rapid edits into one save per document so you’re not racing yourself. After a successful save, the HTML you just sent becomes the new
base_htmlfor the next round. - This endpoint is non-AI and REST-only. It saves typing without running a chat turn, and it isn’t an MCP tool — it’s a UI affordance. The AI-edit flow is unaffected: an AI result is reconciled with the latest saved content, so a concurrent autosave is never silently overwritten.
- A successful save is itself a change other sessions will see on their next
doc-eventspoll — which is why suppressing write-back on received changes (above) matters.
Resume awareness
When a user (or a worker) comes back to a session after being away, you want to know whether anything changed while they were gone. The roster supports this on resume:report_changed=true, each document carries changed_since_seen — true when its committed version is newer than the version this session last saw. Use it to show a one-time “changed since you were last here” notice on resume or when switching back to a session. Set it only on resume / session-switch, not on the ongoing 1–2s poll — the live doc-events feed already covers changes that happen while you’re watching.
Conflict resolution
The hard case is when the same section is edited by both sides at once — the user typed in a paragraph while an AI turn (or another session) rewrote it. Don’t silently pick a winner by reloading.- Merge, keeping the user’s in-progress edits. Apply the incoming change to every section except the one the user is actively editing, and for the conflicted section keep the user’s version and surface the other version as an accept-able suggestion. The user’s keystrokes should win by default; they can accept the alternative explicitly. This is the same conflict posture as applying AI results.
- When the same field gets two different values, keep the user’s — never concatenate the two into one blob.
- Never text-merge a non-text node. Tables, diagrams (Mermaid), drawings, and equations (KaTeX/LaTeX) are structured nodes — splicing one side’s text into the other corrupts them. Compare and resolve those whole: take one version or the other for the node, don’t word-diff their markup. (This is the same rule as rendering and diffing them in an editor; see Rendering visual content.)
Explicit per-section conflict resolution
If you run your own editor and want to resolve a single conflicted section deliberately — rather than relying on the automatic merge above — call:mode:
"redo"(default) — re-apply the AI’s intended change on top of the user’s current text. The user’s edits stay; the AI’s change is layered back over them."merge"— blend the user’s version and the AI’s version into one combined section.
| Field | Required | Meaning |
|---|---|---|
user_current_html | yes | The user’s current version of the section. |
ai_proposed_new_html | — | The AI’s proposed version of the section. |
ai_original_old_html | — | What the AI started from (its before-state). |
ai_explanation | — | The AI’s stated intent for the change. |
mode | — | "redo" (default) or "merge". |
model_tier | — | Optional model-tier override. |
data-chunk-id block the same way you apply any other section update.
This is optional, and it’s a billable AI edit. It runs one AI edit and counts as one billable operation. The automatic merge described above already resolves most conflicts on its own — reach for
re-edit only when you want explicit, deliberate control over one section. It is a REST endpoint, not an MCP/agent tool.Restoring a removed document
If adocument_removed event arrives for a document the user is mid-edit in, archiving it out from under them would lose their unsaved work. Offer a keep-editing choice instead of dropping the tab silently. Choosing “keep editing” restores the document and persists the user’s current content so the document converges for everyone:
html (same body shape as /save). It un-archives the document, re-links it to this session, and saves the latest content — idempotent if another session already restored it. This session-scoped, keep-editing form is non-billable and isn’t an MCP tool — it’s a UI affordance for the mid-edit case, because it persists the in-progress html as part of the restore.
For a plain restore that doesn’t carry editor content — restoring an archived document by its durable id from anywhere — use POST /v1/documents/{document_id}/unarchive, which is also the unarchive_document MCP tool (non-billable). An agent can restore a document with it; the session-scoped form above is the one to use only when you need to preserve a user’s unsaved edits during the restore.
Putting it together
A robust concurrent-editing loop, per open document:- Poll
GET .../doc-events?after_id={cursor}every 1–2s (addinclude_own=trueif you write on a separate connection). Advance the cursor. - On
document_updated, re-fetch that document and apply changed sections — with autosave suppressed during the apply. - Debounce + coalesce human edits and autosave them with
html+base_html. - On a same-section clash, merge and keep the user’s edits; resolve non-text nodes whole.
- On resume, read the roster with
report_changed=trueand surface a one-time “changed while away” notice.
Stuck?
If your collaboration shape isn’t covered here — a different conflict policy, presence indicators, true real-time sync — email hello@superdocs.app or book a 15-minute integration call at cal.com/superdocs.Related guides
- Editor Integration — preserving
data-chunk-idand applying updates per section. - Multi-Document Sessions — several documents open in one session, and applying a revert per document.
- SSE Streaming — your own turn’s live events (
documents_changed,proposed_change_batch). - Server Integration — when one of the concurrent writers is a headless backend job.

