@agent-native/core 0.45.1 → 0.46.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/README.md +1 -0
  2. package/dist/client/components/LiveCursorOverlay.d.ts +46 -0
  3. package/dist/client/components/LiveCursorOverlay.d.ts.map +1 -0
  4. package/dist/client/components/LiveCursorOverlay.js +137 -0
  5. package/dist/client/components/LiveCursorOverlay.js.map +1 -0
  6. package/dist/client/components/PresenceBar.d.ts +11 -1
  7. package/dist/client/components/PresenceBar.d.ts.map +1 -1
  8. package/dist/client/components/PresenceBar.js +39 -7
  9. package/dist/client/components/PresenceBar.js.map +1 -1
  10. package/dist/client/components/RemoteSelectionRings.d.ts +43 -0
  11. package/dist/client/components/RemoteSelectionRings.d.ts.map +1 -0
  12. package/dist/client/components/RemoteSelectionRings.js +116 -0
  13. package/dist/client/components/RemoteSelectionRings.js.map +1 -0
  14. package/dist/client/index.d.ts +4 -0
  15. package/dist/client/index.d.ts.map +1 -1
  16. package/dist/client/index.js +5 -0
  17. package/dist/client/index.js.map +1 -1
  18. package/dist/collab/awareness.d.ts +25 -0
  19. package/dist/collab/awareness.d.ts.map +1 -1
  20. package/dist/collab/awareness.js +42 -5
  21. package/dist/collab/awareness.js.map +1 -1
  22. package/dist/collab/client.d.ts +19 -1
  23. package/dist/collab/client.d.ts.map +1 -1
  24. package/dist/collab/client.js +362 -57
  25. package/dist/collab/client.js.map +1 -1
  26. package/dist/collab/follow-mode.d.ts +56 -0
  27. package/dist/collab/follow-mode.d.ts.map +1 -0
  28. package/dist/collab/follow-mode.js +54 -0
  29. package/dist/collab/follow-mode.js.map +1 -0
  30. package/dist/collab/index.d.ts +3 -1
  31. package/dist/collab/index.d.ts.map +1 -1
  32. package/dist/collab/index.js +5 -1
  33. package/dist/collab/index.js.map +1 -1
  34. package/dist/collab/presence.d.ts +56 -0
  35. package/dist/collab/presence.d.ts.map +1 -0
  36. package/dist/collab/presence.js +98 -0
  37. package/dist/collab/presence.js.map +1 -0
  38. package/dist/collab/routes.d.ts.map +1 -1
  39. package/dist/collab/routes.js +33 -6
  40. package/dist/collab/routes.js.map +1 -1
  41. package/dist/collab/struct-routes.d.ts.map +1 -1
  42. package/dist/collab/struct-routes.js +24 -4
  43. package/dist/collab/struct-routes.js.map +1 -1
  44. package/dist/collab/ydoc-manager.d.ts +13 -0
  45. package/dist/collab/ydoc-manager.d.ts.map +1 -1
  46. package/dist/collab/ydoc-manager.js +51 -15
  47. package/dist/collab/ydoc-manager.js.map +1 -1
  48. package/dist/server/collab-plugin.d.ts +6 -0
  49. package/dist/server/collab-plugin.d.ts.map +1 -1
  50. package/dist/server/collab-plugin.js +105 -5
  51. package/dist/server/collab-plugin.js.map +1 -1
  52. package/dist/server/poll-events.d.ts +5 -0
  53. package/dist/server/poll-events.d.ts.map +1 -1
  54. package/dist/server/poll-events.js +27 -4
  55. package/dist/server/poll-events.js.map +1 -1
  56. package/dist/templates/default/.agents/skills/real-time-collab/SKILL.md +185 -37
  57. package/dist/templates/default/.agents/skills/real-time-sync/SKILL.md +12 -2
  58. package/dist/templates/workspace-core/.agents/skills/real-time-collab/SKILL.md +185 -37
  59. package/dist/templates/workspace-core/.agents/skills/real-time-sync/SKILL.md +12 -2
  60. package/docs/content/real-time-collaboration.md +481 -97
  61. package/package.json +1 -1
  62. package/src/templates/default/.agents/skills/real-time-collab/SKILL.md +185 -37
  63. package/src/templates/default/.agents/skills/real-time-sync/SKILL.md +12 -2
  64. package/src/templates/workspace-core/.agents/skills/real-time-collab/SKILL.md +185 -37
  65. package/src/templates/workspace-core/.agents/skills/real-time-sync/SKILL.md +12 -2
@@ -1,55 +1,117 @@
1
1
  ---
2
2
  title: "Real-Time Collaboration"
3
- description: "Multi-user collaborative editing with Yjs CRDT, live cursors, and AI agent real-time edits."
3
+ description: "Multi-user collaborative editing where the AI agent is a first-class peer: CRDT merging, live presence, SSE fast-path, and granular server-side merge — on any SQL database and any host."
4
4
  ---
5
5
 
6
6
  # Real-Time Collaboration
7
7
 
8
- Multi-user collaborative editing where the AI agent and human users are equal participants like Google Docs, but with an AI collaborator.
8
+ Imagine opening a document and seeing a peer's cursor scroll to a paragraph,
9
+ then the text rewrite itself — surgically, without losing your place. That
10
+ peer might be a teammate. It might be the agent. From the framework's
11
+ perspective they are identical: both produce Yjs operations that merge
12
+ conflict-free into the shared document. This is the keystone of the
13
+ agent-native collaboration model.
14
+
15
+ ## Vision {#vision}
16
+
17
+ Editing alongside the agent feels like working in Google Docs or Figma with
18
+ a coworker who is both instant and tireless:
19
+
20
+ - **CRDT merging** — Concurrent edits from humans and agents merge without
21
+ conflicts. You type in one paragraph; the agent rewrites another; both
22
+ land cleanly.
23
+ - **Presence** — A `PresenceBar` shows who is in the document right now,
24
+ including an agent presence indicator when the agent is actively editing.
25
+ - **The agent as a peer editor** — Agent edits flow through the same Yjs
26
+ infrastructure as human edits. They appear live, without disrupting cursor
27
+ positions, selections, or the undo stack.
28
+ - **Works everywhere** — Any SQL database Drizzle supports (SQLite, Postgres).
29
+ Any hosting target Nitro supports, including serverless and edge.
30
+
31
+ ## Architecture {#architecture}
32
+
33
+ The collaboration system has five interlocking layers.
34
+
35
+ ### 1. Yjs Y.Doc (CRDT layer)
36
+
37
+ Each collaborative document is a `Y.Doc` containing shared types — usually a
38
+ `Y.XmlFragment` for rich text (the ProseMirror node tree that TipTap reads) or
39
+ `Y.Map` / `Y.Array` for structured JSON data. Yjs merges concurrent updates
40
+ with no central coordinator; any two clients that exchange their state reach
41
+ the same result regardless of order.
42
+
43
+ ### 2. SQL canonical content (durable source of truth)
44
+
45
+ Yjs state is persisted in a `_collab_docs` table as base64-encoded binary.
46
+ The table is framework-managed and provider-agnostic (SQLite and Postgres use
47
+ identical schemas). Each row carries an optimistic-concurrency version column
48
+ to prevent concurrent write races. Tombstone compaction runs opportunistically
49
+ when the stored blob exceeds 4× the freshly encoded state — no background job
50
+ required.
51
+
52
+ ### 3. `updatedAt`-gated reconcile (agent-edit propagation)
53
+
54
+ Agent actions do not push into Yjs in-process. Instead, the action edits the
55
+ canonical SQL content column and bumps `updatedAt`. The change-sync system
56
+ detects the bump, the open editor refetches the record, and the lead client
57
+ applies the new content into the shared Y.Doc via `setContent`. An `updatedAt`
58
+ gate ensures only genuinely newer content is adopted — lagging poll responses
59
+ cannot revert the edit.
60
+
61
+ ### 4. Lead-client election (deduplication)
62
+
63
+ When multiple tabs are open, exactly one applies an authoritative SQL snapshot
64
+ into the shared Y.Doc. The lead is the tab with the lowest Yjs `clientID`
65
+ among currently visible peers. The agent's awareness entry uses
66
+ `AGENT_CLIENT_ID` (max int) so it can never be the lead. A client editing
67
+ alone is always the lead. The election is deterministic with no coordination
68
+ round-trip (`isReconcileLeadClient` from `@agent-native/core/client`).
69
+
70
+ ### 5. SSE fast-path + polling fallback (transport)
71
+
72
+ Collab update events travel via two paths:
73
+
74
+ - **SSE fast-path** — The client subscribes to `/_agent-native/poll-events`
75
+ (the same `EventSource` used by `useDbSync`). Collab update events arrive
76
+ push-style, typically in tens of milliseconds. While SSE is healthy the
77
+ poll loop relaxes to a slow cadence (~12 s by default).
78
+ - **Polling fallback** — `/_agent-native/poll?since=N` is polled every 2 s
79
+ when SSE is unavailable. This makes collaboration work on any deployment
80
+ target — including serverless functions where persistent connections are
81
+ impossible and different invocations can handle different requests.
82
+
83
+ Local Yjs updates are debounced and coalesced with `Y.mergeUpdates` (~80 ms)
84
+ before being sent to the server, reducing keystroke-level network traffic.
85
+ The batch is flushed immediately on `visibilitychange` or `pagehide`. A
86
+ state-vector diff (`GET /:docId/state?stateVector=…`) is fetched only on
87
+ reconnect, ring-buffer overflow, or every 15th poll cycle — not on every
88
+ cycle.
89
+
90
+ Network errors use exponential backoff with jitter, capped at ~15 s.
9
91
 
10
- ## Overview {#overview}
11
-
12
- The framework provides a Yjs-based collaborative editing system in `@agent-native/core/collab`. Multiple users can edit the same document simultaneously with live cursor positions, and the AI agent can make surgical edits that appear in real-time without disrupting the user's cursor, selection, or undo history.
13
-
14
- This is built on three battle-tested technologies: **Yjs** (CRDT for conflict-free merging), **TipTap** (rich text editor), and **polling-based sync** (works in all deployment environments including serverless and edge).
15
-
16
- ## How it works {#how-it-works}
17
-
18
- The collaboration system has three layers:
19
-
20
- - **Yjs Y.Doc** — stores the document as a `Y.XmlFragment` (ProseMirror node tree). This is the CRDT that enables conflict-free merging of concurrent edits.
21
- - **TipTap Collaboration extension** — binds the editor to the Y.XmlFragment via `ySyncPlugin`. Remote changes are applied as minimal ProseMirror transactions that preserve cursor position.
22
- - **Polling sync** clients poll `/_agent-native/poll` every 2 seconds for Yjs updates. Awareness state (cursor positions, user info) is synced via a separate `/_agent-native/collab/:docId/awareness` endpoint.
23
-
24
- The Yjs state is persisted in a `_collab_docs` SQL table as base64-encoded binary, compatible with both SQLite and Postgres.
25
-
26
- ## Agent + human collaboration {#agent-human-collab}
27
-
28
- The agent and human users are equal participants in collaborative editing. The key insight is that both produce Yjs operations that merge cleanly:
29
-
30
- - **Human edits** flow through TipTap → ySyncPlugin → Y.XmlFragment → server via HTTP
31
- - **Agent edits** flow through the `edit-document` action → server search-replace endpoint → Y.XmlFragment mutation → poll update → all clients
32
-
33
- The agent's `edit-document` action uses surgical search-and-replace on Y.XmlText nodes within the Y.XmlFragment tree. This produces the smallest possible Yjs update — only the changed text is modified, not the entire document. The result: the user sees the agent's change appear in their editor without losing their place.
34
-
35
- ```bash
36
- # Agent makes a surgical edit — user sees it appear live
37
- pnpm action edit-document --id doc123 --find "Big Projects" --replace "Proyectos Grandes"
38
-
39
- # The action:
40
- # 1. Updates SQL content column (for search/API compat)
41
- # 2. Calls POST /_agent-native/collab/doc123/search-replace
42
- # 3. Server walks Y.XmlFragment, finds text, modifies Y.XmlText node
43
- # 4. Minimal Yjs update emitted via poll system
44
- # 5. Client receives update → ySyncPlugin applies targeted PM transaction
45
- # 6. User's cursor stays in place ✓
92
+ ```
93
+ Browser Server SQL
94
+ ────── ────── ───
95
+ [Human edits]
96
+ Y.Doc update
97
+ → debounce ~80ms
98
+ POST /collab/:docId/update ──────→ apply + persist
99
+ emitCollabUpdate
100
+ SSE push ──── poll-events stream
101
+ ← Y.applyUpdate
102
+
103
+ [Agent action]
104
+ action writes SQL content + bumps updatedAt
105
+ ← change-sync detects updatedAt bump
106
+ lead client calls setContent
107
+ → Y.Doc update
108
+ POST /collab/:docId/update ──────→ apply + persist
109
+ ← SSE push to all other clients
46
110
  ```
47
111
 
48
- ## Enabling collaboration {#enabling-collab}
49
-
50
- Templates opt into collaboration with five steps:
112
+ ## Quickstart {#quickstart}
51
113
 
52
- ### 1. Install dependencies
114
+ ### 1. Install packages
53
115
 
54
116
  ```bash
55
117
  pnpm add @tiptap/extension-collaboration @tiptap/extension-collaboration-caret @tiptap/y-tiptap @tiptap/core
@@ -78,6 +140,11 @@ export default defineConfig({
78
140
 
79
141
  ### 3. Add the collab server plugin
80
142
 
143
+ Always set `resourceType` to the name of the shareable resource registered
144
+ via `registerShareableResource`. Without it, collab push events are delivered
145
+ to all authenticated users without document-level scoping, and the server
146
+ logs a one-time warning.
147
+
81
148
  ```typescript
82
149
  // server/plugins/collab.ts
83
150
  import { createCollabPlugin } from "@agent-native/core/server";
@@ -86,22 +153,31 @@ export default createCollabPlugin({
86
153
  table: "documents",
87
154
  contentColumn: "content",
88
155
  idColumn: "id",
89
- autoSeed: false, // Client-side seeding on first load
156
+ resourceType: "document", // required for access-scoped event delivery
90
157
  });
91
158
  ```
92
159
 
93
160
  ### 4. Use the client hook
94
161
 
95
162
  ```typescript
96
- import { useCollaborativeDoc, generateTabId } from "@agent-native/core/client";
97
-
98
- const TAB_ID = generateTabId();
99
-
100
- const { ydoc, awareness, isLoading, activeUsers } = useCollaborativeDoc({
101
- docId: documentId,
102
- requestSource: TAB_ID,
103
- user: { name: "Steve", email: "steve@example.com", color: "#60a5fa" },
104
- });
163
+ import {
164
+ useCollaborativeDoc,
165
+ emailToColor,
166
+ emailToName,
167
+ } from "@agent-native/core/client";
168
+
169
+ const TAB_ID = generateTabId(); // or Math.random().toString(36)
170
+
171
+ const { ydoc, awareness, isLoading, activeUsers, agentActive, agentPresent } =
172
+ useCollaborativeDoc({
173
+ docId: documentId,
174
+ requestSource: TAB_ID,
175
+ user: {
176
+ name: emailToName(session.email),
177
+ email: session.email,
178
+ color: emailToColor(session.email),
179
+ },
180
+ });
105
181
  ```
106
182
 
107
183
  ### 5. Add TipTap extensions
@@ -109,77 +185,385 @@ const { ydoc, awareness, isLoading, activeUsers } = useCollaborativeDoc({
109
185
  ```typescript
110
186
  import Collaboration from "@tiptap/extension-collaboration";
111
187
  import CollaborationCaret from "@tiptap/extension-collaboration-caret";
112
- import { Awareness } from "y-protocols/awareness";
113
-
114
- // Create awareness for cursor sync
115
- const awareness = new Awareness(ydoc);
116
- awareness.setLocalStateField("user", { name, color });
117
188
 
118
189
  const editor = useEditor({
119
190
  extensions: [
120
- StarterKit.configure({ history: false }), // Yjs handles undo
191
+ StarterKit.configure({ history: false }), // Yjs owns undo
121
192
  Collaboration.configure({ document: ydoc }),
122
193
  CollaborationCaret.configure({
123
194
  provider: { awareness },
124
195
  user: { name, color },
125
196
  }),
126
197
  ],
127
- content: initialContent,
198
+ // Do NOT pass content here — Yjs owns the content
128
199
  });
129
200
  ```
130
201
 
131
- ## Live cursors & presence {#live-cursors}
202
+ ### 6. Seed on first load (if content exists)
132
203
 
133
- The `CollaborationCaret` extension renders colored cursor lines with user name labels for each connected user. The `useCollaborativeDoc` hook provides an `activeUsers` array that can be used to render a presence bar with user avatars.
204
+ The Collaboration extension does not auto-seed from a `content` prop. If the
205
+ Y.Doc is empty and the document has existing content, seed it:
134
206
 
135
- User identity is derived from the session email. The framework provides `emailToColor()` and `emailToName()` helpers to generate consistent cursor colors and display names from email addresses.
207
+ ```typescript
208
+ useEffect(() => {
209
+ if (!ydoc || !editor || !isLoaded) return;
210
+ const fragment = ydoc.getXmlFragment("default");
211
+ if (fragment.length === 0 && initialContent) {
212
+ editor.commands.setContent(initialContent);
213
+ }
214
+ }, [ydoc, editor, isLoaded]);
215
+ ```
136
216
 
137
- ## Comments {#comments}
217
+ ---
138
218
 
139
- Templates can add a comments system with threaded discussions on documents. The content template includes a full implementation with:
219
+ ## Presence Kit {#presence-kit}
140
220
 
141
- - `document_comments` SQL table (threads, replies, resolved status)
142
- - REST routes for update/delete at `/api/comments/:id`; create and list run through the `add-comment` / `list-comments` actions
143
- - Comments sidebar with threaded view and reply UI
144
- - Resolve/unresolve threads
145
- - **Send to AI** button — sends the comment thread context to the agent chat via `sendToAgentChat()`
146
- - Agent actions: `list-comments`, `add-comment`
147
- - Notion comment sync: `sync-notion-comments` action for bidirectional pull/push
221
+ The presence kit provides Liveblocks/Figma-grade live-cursor and selection primitives on top of the existing awareness layer.
148
222
 
149
- ## Collab routes {#collab-routes}
223
+ ### Fast awareness {#fast-awareness}
150
224
 
151
- All collab routes are auto-mounted under `/_agent-native/collab/` by the collab plugin:
225
+ Awareness state changes now propagate at ~150ms instead of the 2s poll cycle:
152
226
 
153
- | Route | Purpose |
154
- | ----------------------------- | ---------------------------------------- |
155
- | `GET /:docId/state` | Fetch full Y.Doc state (base64) |
156
- | `POST /:docId/update` | Apply client Yjs update |
157
- | `POST /:docId/text` | Apply full text replacement (diff-based) |
158
- | `POST /:docId/search-replace` | Surgical find/replace in Y.XmlFragment |
159
- | `POST /:docId/awareness` | Sync cursor/presence state |
160
- | `GET /:docId/users` | List active users on a document |
227
+ - **Client → server**: any call to `setPresence()` or `awareness.setLocalStateField()` triggers a throttled POST to `/_agent-native/collab/:docId/awareness` within 150ms, coalescing rapid changes into one request.
228
+ - **Server clients**: the `postAwareness` handler emits an `AWARENESS_CHANGE_EVENT` after storing. The `/_agent-native/poll-events` SSE stream forwards these events push-style to connected peers. Polling-only deployments continue to work — cursors degrade to poll cadence without errors.
161
229
 
162
- ## Agent edit action {#edit-document}
230
+ ### `usePresence(awareness, localClientId)` {#use-presence}
163
231
 
164
- The `edit-document` action is the primary way agents make changes to documents in collaborative mode:
232
+ Returns a reactive list of remote participants and a setter for the local presence payload:
165
233
 
166
- ```bash
167
- # Single edit
168
- pnpm action edit-document --id doc123 --find "old text" --replace "new text"
234
+ ```typescript
235
+ import { usePresence } from "@agent-native/core/client";
236
+
237
+ const { others, setPresence } = usePresence(awareness, ydoc?.clientID);
238
+
239
+ // Publish cursor position (normalized 0–1)
240
+ setPresence({ cursor: { x: 0.4, y: 0.7 }, selection: "#hero" });
241
+
242
+ // others: OtherPresence[]
243
+ // {
244
+ // clientId: number
245
+ // user: { name, email, color }
246
+ // presence: { cursor?, selection?, viewport?, ... }
247
+ // isAgent: boolean ← true for AGENT_CLIENT_ID
248
+ // }
249
+ ```
250
+
251
+ The agent (AGENT_CLIENT_ID) appears as a first-class participant with `isAgent: true`. When `agentUpdateSelection()` is called server-side, its selection metadata flows through `usePresence` like any other participant.
252
+
253
+ ### `LiveCursorOverlay` {#live-cursor-overlay}
254
+
255
+ Renders remote cursors as absolutely-positioned labels over a container element:
256
+
257
+ ```tsx
258
+ import { LiveCursorOverlay } from "@agent-native/core/client";
259
+
260
+ // cursor positions stored as { x, y } normalized 0–1 under presence.cursor
261
+ <div ref={containerRef} style={{ position: "relative" }}>
262
+ {content}
263
+ <LiveCursorOverlay
264
+ others={others} // from usePresence
265
+ containerRef={containerRef}
266
+ cursorKey="cursor" // key in presence payload (default: "cursor")
267
+ />
268
+ </div>;
269
+ ```
270
+
271
+ The agent's cursor renders distinctly with a sparkle icon. Cursors fade out after 10s of inactivity with smooth CSS transitions at 120ms.
272
+
273
+ ### `RemoteSelectionRings` {#remote-selection-rings}
274
+
275
+ Renders colored outline rings + name tags over remotely-selected elements:
276
+
277
+ ```tsx
278
+ import { RemoteSelectionRings } from "@agent-native/core/client";
279
+
280
+ <div ref={containerRef} style={{ position: "relative" }}>
281
+ {content}
282
+ <RemoteSelectionRings
283
+ others={others}
284
+ selectionKey="selection" // key in presence payload (default: "selection")
285
+ resolveRect={(descriptor) =>
286
+ document.querySelector(descriptor)?.getBoundingClientRect() ?? null
287
+ }
288
+ containerRef={containerRef}
289
+ />
290
+ </div>;
291
+ ```
292
+
293
+ ### `useFollowUser` {#follow-user}
294
+
295
+ Invoke a callback whenever the followed participant's viewport changes:
296
+
297
+ ```typescript
298
+ import { useFollowUser } from "@agent-native/core/client";
299
+
300
+ const { isFollowing, stopFollowing } = useFollowUser({
301
+ others,
302
+ followingId, // null to stop following
303
+ viewportKey: "viewport",
304
+ onViewport: (vp) => {
305
+ if (vp.fileId) setActiveFileId(vp.fileId);
306
+ if (vp.zoom) setZoom(vp.zoom);
307
+ },
308
+ });
309
+ ```
310
+
311
+ Participants publish their viewport with `setPresence({ viewport: { fileId, zoom } })`.
312
+
313
+ ### `PresenceBar` follow-mode props {#presence-bar-follow}
314
+
315
+ The `PresenceBar` component now accepts optional follow-mode props:
316
+
317
+ ```tsx
318
+ <PresenceBar
319
+ activeUsers={activeUsers}
320
+ agentActive={agentActive}
321
+ onAvatarClick={(user) => {
322
+ // user is null for the agent avatar
323
+ const email = user?.email ?? "agent@system";
324
+ setFollowing((prev) => (prev === email ? null : email));
325
+ }}
326
+ followingEmail={followingEmail} // highlighted avatar + "Following X" chip
327
+ />
328
+ ```
169
329
 
170
- # Batch edits
171
- pnpm action edit-document --id doc123 --edits '[{"find":"old","replace":"new"}]'
330
+ ### Normalized coordinate helpers {#norm-coords}
172
331
 
173
- # Delete text
174
- pnpm action edit-document --id doc123 --find "delete me" --replace ""
332
+ ```typescript
333
+ import { toNormalized, fromNormalized } from "@agent-native/core/client";
334
+
335
+ // In a pointer event handler:
336
+ const norm = toNormalized(
337
+ e.clientX,
338
+ e.clientY,
339
+ container.getBoundingClientRect(),
340
+ );
341
+ setPresence({ cursor: norm });
342
+
343
+ // In a cursor renderer:
344
+ const px = fromNormalized(norm, container.getBoundingClientRect());
345
+ ```
346
+
347
+ ### Agent cursor plumbing {#agent-cursor}
348
+
349
+ Server-side actions call `agentUpdateSelection()` to publish where the agent is working. The design template's `edit-design` and `generate-design` actions call this automatically. Other templates can do the same:
350
+
351
+ ```typescript
352
+ import {
353
+ agentEnterDocument,
354
+ agentLeaveDocument,
355
+ agentUpdateSelection,
356
+ } from "@agent-native/core/collab";
357
+
358
+ agentEnterDocument(docId);
359
+ agentUpdateSelection(docId, {
360
+ selection: "#target-element",
361
+ editingFile: "index.html",
362
+ });
363
+ try {
364
+ // ... perform edits ...
365
+ } finally {
366
+ agentLeaveDocument(docId);
367
+ }
175
368
  ```
176
369
 
177
- When collab state exists for the document, the action calls the server's `search-replace` endpoint via HTTP (not the collab module directly, since actions run in a separate process). The server walks the Y.XmlFragment tree, finds the text in Y.XmlText nodes, and applies minimal delete/insert operations. The resulting Yjs update is broadcast to all connected clients via the poll system.
370
+ The selection metadata flows through `usePresence` on connected clients as `other.presence.selection`.
371
+
372
+ ---
373
+
374
+ ## Route table {#routes}
375
+
376
+ All routes are auto-mounted under `/_agent-native/collab/` by the collab
377
+ plugin:
378
+
379
+ | Route | Purpose |
380
+ | ----------------------------- | ----------------------------------------------------------- |
381
+ | `GET /:docId/state` | Full Y.Doc state (base64). Accepts `?stateVector=` for diff |
382
+ | `POST /:docId/update` | Apply client Yjs update (base64). Max 2 MB by default |
383
+ | `POST /:docId/text` | Apply full text replacement (diff-based) |
384
+ | `POST /:docId/search-replace` | Surgical find/replace in Y.XmlFragment |
385
+ | `POST /:docId/json` | Apply full JSON diff to Y.Map/Y.Array |
386
+ | `GET /:docId/json` | Read current JSON state |
387
+ | `POST /:docId/patch` | Apply surgical JSON patch ops (upsert/remove/reorder) |
388
+ | `POST /:docId/awareness` | Sync cursor/presence state |
389
+ | `GET /:docId/users` | List active users on a document |
390
+
391
+ ## Transport and performance {#transport}
392
+
393
+ | Property | Value |
394
+ | ---------------------------- | ---------------------------------------------------------- |
395
+ | Update debounce | ~80 ms (coalesces rapid keystrokes via `Y.mergeUpdates`) |
396
+ | Poll interval (no SSE) | 2 s (configurable via `pollInterval`) |
397
+ | Poll interval (SSE healthy) | ~12 s (configurable via `pollIntervalWithSse`) |
398
+ | State-vector fetch frequency | On reconnect, ring-buffer gap, or every 15th poll cycle |
399
+ | Backoff on error | Exponential with jitter, cap ~15 s |
400
+ | Max payload (writes) | 2 MB default, configurable via `maxPayloadBytes` |
401
+ | Compaction threshold | Stored blob > 4× fresh encoding triggers tombstone compact |
402
+ | Per-write DB reads | 1 (CAS version read inside `persistMergedState` only) |
178
403
 
179
- ## Common pitfalls {#pitfalls}
404
+ ## Security {#security}
405
+
406
+ ### Always set `resourceType`
407
+
408
+ ```typescript
409
+ createCollabPlugin({
410
+ resourceType: "document", // the name passed to registerShareableResource
411
+ });
412
+ ```
413
+
414
+ Without `resourceType` the plugin logs a warning and broadcasts collab push
415
+ events to all authenticated users on the deployment without document-level
416
+ scoping. Non-owners fall back to state-vector catch-up (safe but higher
417
+ latency) regardless of whether `resourceType` is set.
418
+
419
+ ### Access checks
420
+
421
+ All collab routes require authentication. When `resourceType` is set, reads
422
+ require at least viewer access and writes require editor access, using the
423
+ same `resolveAccess` / `assertAccess` helpers as the sharing system. A 404
424
+ (not 403) is returned on access failures to avoid leaking document existence.
425
+
426
+ ### Payload limits
427
+
428
+ Write routes (`update`, `text`, `json`, `patch`, `search-replace`) reject
429
+ payloads exceeding the configured limit with HTTP 413. The default is 2 MB.
430
+ Override per-plugin:
431
+
432
+ ```typescript
433
+ createCollabPlugin({
434
+ resourceType: "document",
435
+ maxPayloadBytes: 512 * 1024, // 512 KB
436
+ });
437
+ ```
438
+
439
+ ### Awareness scoping
440
+
441
+ Awareness routes (`POST /awareness`, `GET /users`) are gated by the same
442
+ access check as reads — a user who lacks viewer access cannot learn who else
443
+ is editing a document.
444
+
445
+ ## Patterns {#patterns}
446
+
447
+ ### Granular server-side merge for structured data
448
+
449
+ For structured documents (slide decks, form builders, design files) the Yjs
450
+ body collab model can conflict when two agents or users rewrite the same
451
+ top-level record simultaneously. The safer pattern is **granular server-side
452
+ merge**: define an action that accepts a set of targeted operations and
453
+ applies them atomically, so concurrent edits to different items both survive.
454
+
455
+ **Slides (`patch-deck`)** — Instead of replacing the entire deck JSON on every
456
+ change, the action accepts per-slide operations:
457
+
458
+ ```typescript
459
+ // Conceptual patch-deck action shape
460
+ type PatchDeckOp =
461
+ | { type: "patch"; slideId: string; fields: Partial<SlideFields> }
462
+ | { type: "add"; position: number; slide: SlideData }
463
+ | { type: "delete"; slideId: string }
464
+ | { type: "reorder"; slideId: string; newIndex: number };
465
+ ```
466
+
467
+ Two users editing different slides both succeed; there is no LWW clobber at
468
+ the deck level.
469
+
470
+ **Forms (`patch-form-fields`)** — Field-level merge with upsert/remove/reorder
471
+ ops so concurrent edits to different form fields both survive.
472
+
473
+ Use this pattern when:
474
+
475
+ - The document is structured (items inside a container).
476
+ - Concurrent edits target different items.
477
+ - Body collab (Yjs `Y.XmlFragment`) is overkill or inapplicable.
478
+
479
+ Use body collab (Y.XmlFragment + TipTap) when:
480
+
481
+ - The document is free-form rich text where any region can be edited.
482
+ - Cursor-level CRDT merge matters.
483
+
484
+ ### Collaborative undo scoping (Y.UndoManager)
485
+
486
+ The Design template uses `Y.UndoManager` to scope undo/redo to the local
487
+ user's own edits. Remote peer edits and agent edits are never undone by a
488
+ user's Cmd+Z.
489
+
490
+ ```typescript
491
+ import * as Y from "yjs";
492
+
493
+ const LOCAL_EDIT_ORIGIN = "local";
494
+
495
+ const undoManager = new Y.UndoManager(ydoc.getText("content"), {
496
+ trackedOrigins: new Set([LOCAL_EDIT_ORIGIN]),
497
+ captureTimeout: 800, // coalesce rapid slider drags into one undo step
498
+ });
499
+
500
+ // Wrap local edits with the tracked origin
501
+ ydoc.transact(() => {
502
+ // apply local style change
503
+ }, LOCAL_EDIT_ORIGIN);
504
+
505
+ // Undo/redo — only reverses LOCAL_EDIT_ORIGIN transactions
506
+ undoManager.undo(); // Cmd+Z
507
+ undoManager.redo(); // Shift+Cmd+Z
508
+ ```
180
509
 
181
- - **TipTap version mismatch** — All `@tiptap/*` packages must be the same version. The Collaboration extension requires `editor.utils` which was added in v3.22.2. Add `@tiptap/core` as an explicit dependency.
182
- - **Empty editor on first load** — The Collaboration extension does NOT auto-seed from the `content` prop. Seed manually with `editor.commands.setContent()` when the Y.XmlFragment is empty.
183
- - **Data loss from empty saves** — Guard against saving empty content in the `onUpdate` handler when the editor is in collab mode but hasn't been seeded yet.
184
- - **Vite dep optimization** Always add Yjs-related packages to `optimizeDeps.include` to prevent Vite from re-bundling TipTap in incompatible ways.
185
- - **Separate process for actions** — Actions run via `pnpm action` in a new Node.js process. Use the server's HTTP endpoints (not the collab module directly) so updates reach the poll system.
510
+ Key properties:
511
+
512
+ - `trackedOrigins` must be a `Set`. Only transactions with a matching origin
513
+ are captured in the undo stack.
514
+ - Remote updates (origin `"remote"`) and agent updates (origin `"agent"`) are
515
+ never captured.
516
+ - Recreate and dispose the manager when the active document changes; stale
517
+ managers hold references that can grow unboundedly.
518
+
519
+ ## Known limitations {#limitations}
520
+
521
+ - **Same-region simultaneous rewrite is LWW** — If the agent rewrites a
522
+ passage and a human has unsaved edits in the exact same region, the
523
+ lead-client snapshot can overwrite the human's in-flight changes. Edits in
524
+ different regions merge correctly via the CRDT. Granular server-side merge
525
+ (see above) avoids this for structured documents.
526
+ - **In-process write locks on serverless** — The `_writeLocks` map is
527
+ process-local. Concurrent requests landing on different serverless
528
+ invocations serialize at the SQL CAS layer (optimistic concurrency) rather
529
+ than the in-memory lock. This is safe but means high-throughput scenarios on
530
+ serverless may see more CAS retries.
531
+ - **Awareness is per-process** — The awareness in-memory store is
532
+ process-local. Serverless / multi-process deployments see partial awareness
533
+ state per invocation. Clients still receive full awareness snapshots on each
534
+ poll cycle, so presence indicators update within one poll interval.
535
+
536
+ ## Presence {#presence}
537
+
538
+ The `useCollaborativeDoc` hook returns:
539
+
540
+ - `activeUsers` — array of `CollabUser` (name, email, color) for all peers
541
+ currently in the document (sourced from awareness).
542
+ - `agentActive` — `true` briefly after the agent makes an edit (use for a
543
+ transient visual indicator).
544
+ - `agentPresent` — `true` while the agent has an active awareness entry
545
+ (durable presence heartbeat).
546
+
547
+ Use `emailToColor(email)` and `emailToName(email)` from
548
+ `@agent-native/core/client` to generate consistent cursor colors and display
549
+ names from email addresses.
550
+
551
+ A `PresenceBar` rendered with `activeUsers` shows live human and agent
552
+ collaborators. Per-slide presence (which users are viewing a given slide)
553
+ layers on top of the same awareness state.
554
+
555
+ ## Related docs {#related}
556
+
557
+ - [Real-Time Sync](/docs/real-time-sync) — the `useDbSync` + `useChangeVersion`
558
+ system that delivers the `updatedAt` bump driving editor reconciliation.
559
+ - [Security](/docs/security) — `registerShareableResource`, `resolveAccess`,
560
+ and `assertAccess` for the access model referenced by `resourceType`.
561
+ - [Sharing](/docs/sharing) — how documents are shared and how access is granted.
562
+ - [Template: Content](/docs/template-content) — reference implementation of
563
+ collaborative rich-text editing.
564
+ - [Template: Slides](/docs/template-slides) — granular `patch-deck` action for
565
+ structured concurrent editing.
566
+ - [Template: Forms](/docs/template-forms) — field-level `patch-form-fields`
567
+ server-side merge.
568
+ - [Template: Design](/docs/template-design) — `Y.UndoManager` undo/redo scoped
569
+ to local user edits.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.45.1",
3
+ "version": "0.46.0",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": ">=22"